Commit fe35958e authored by nasia's avatar nasia

features for voms authentication

parent d573d9ab
......@@ -107,7 +107,7 @@ Installation
First, you need to install the required dependencies which can be found here:
* `pyssf <https://code.grnet.gr/attachments/download/1182/pyssf-0.4.5.tar>`_
* `pyssf <https://pypi.python.org/pypi/pyssf>`_
* `kamaki <https://code.grnet.gr/projects/kamaki>`_
Then you can install **snf-occi** API translation server by cloning our latest source code:
......
......@@ -6,9 +6,9 @@ setup(
description='OCCI to Openstack/Cyclades API bridge',
url='http://code.grnet.gr/projects/snf-occi',
license='BSD',
packages = ['snfOCCI'],
entry_points = {
'console_scripts' : ['snf-occi = snfOCCI.APIserver:main']
}
)
packages = ['snfOCCI','snfOCCI.snf_voms','snfOCCI.httpd','snfOCCI.snfServer'],
entry_points = '''
[paste.app_factory]
snf_occi_app = snfOCCI:main
''',
)
\ No newline at end of file
#!/usr/bin/env python
import sys
from optparse import OptionParser, OptionValueError
import string
import sqlite3
import eventlet
from eventlet import wsgi
import os
import json
import uuid
from snfOCCI.registry import snfRegistry
from snfOCCI.compute import ComputeBackend
from snfOCCI.config import SERVER_CONFIG, KAMAKI_CONFIG, VOMS_CONFIG
import snf_voms
from snfOCCI.network import NetworkBackend, IpNetworkBackend, IpNetworkInterfaceBackend, NetworkInterfaceBackend
from snfOCCI.config import SERVER_CONFIG, KAMAKI_CONFIG
from kamaki.clients.compute import ComputeClient
from kamaki.clients.cyclades import CycladesClient
from kamaki.clients import astakos
from kamaki.clients import ClientError
from kamaki.cli import config as kamaki_config
from occi.core_model import Mixin, Resource
from occi.backend import MixinBackend, KindBackend
from occi.backend import MixinBackend
from occi.extensions.infrastructure import COMPUTE, START, STOP, SUSPEND, RESTART, RESOURCE_TEMPLATE, OS_TEMPLATE, NETWORK, IPNETWORK, NETWORKINTERFACE,IPNETWORKINTERFACE
from occi.wsgi import Application
from occi import wsgi
from occi.exceptions import HTTPError
from occi import core_model
from wsgiref.simple_server import make_server
from wsgiref.validate import validator
import uuid
from webob import Request
from pprint import pprint
class MyAPP(Application):
class MyAPP(wsgi.Application):
'''
An OCCI WSGI application.
'''
def __init__(self):
"""
Initialization of the WSGI OCCI application for synnefo
"""
global ENABLE_VOMS, VOMS_DB
ENABLE_VOMS = VOMS_CONFIG['enable_voms']
super(MyAPP,self).__init__(registry=snfRegistry())
self._register_backends()
VALIDATOR_APP = validator(self)
def _register_backends(self):
COMPUTE_BACKEND = ComputeBackend()
NETWORK_BACKEND = NetworkBackend()
NETWORKINTERFACE_BACKEND = NetworkInterfaceBackend()
IPNETWORK_BACKEND = IpNetworkBackend()
IPNETWORKINTERFACE_BACKEND = IpNetworkInterfaceBackend()
self.register_backend(COMPUTE, COMPUTE_BACKEND)
self.register_backend(START, COMPUTE_BACKEND)
self.register_backend(STOP, COMPUTE_BACKEND)
self.register_backend(RESTART, COMPUTE_BACKEND)
self.register_backend(SUSPEND, COMPUTE_BACKEND)
self.register_backend(RESOURCE_TEMPLATE, MixinBackend())
self.register_backend(OS_TEMPLATE, MixinBackend())
# Network related backends
self.register_backend(NETWORK, NETWORK_BACKEND)
self.register_backend(IPNETWORK, IPNETWORK_BACKEND)
self.register_backend(NETWORKINTERFACE,NETWORKINTERFACE_BACKEND)
self.register_backend(IPNETWORKINTERFACE, IPNETWORKINTERFACE_BACKEND)
def refresh_images(self, snf, client):
images = snf.list_images()
for image in images:
IMAGE_ATTRIBUTES = {'occi.core.id': str(image['id'])}
IMAGE = Mixin("http://schemas.ogf.org/occi/infrastructure#", str(image['name']), [OS_TEMPLATE], attributes = IMAGE_ATTRIBUTES)
self.register_backend(IMAGE, MixinBackend())
try:
images = snf.list_images()
for image in images:
IMAGE_ATTRIBUTES = {'occi.core.id': str(image['id'])}
IMAGE = Mixin("http://schemas.ogf.org/occi/os_tpl#", occify_terms(str(image['name'])), [OS_TEMPLATE],title='IMAGE' ,attributes = IMAGE_ATTRIBUTES)
self.register_backend(IMAGE, MixinBackend())
except:
raise HTTPError(404, "Unauthorized access")
def refresh_flavors(self, snf, client):
flavors = snf.list_flavors()
print "Retrieving details for each image id"
for flavor in flavors:
details = snf.get_flavor_details(flavor['id'])
FLAVOR_ATTRIBUTES = {'occi.core.id': flavor['id'],
......@@ -45,24 +94,23 @@ class MyAPP(Application):
'occi.compute.memory': str(details['ram']),
'occi.storage.size': str(details['disk']),
}
FLAVOR = Mixin("http://schemas.ogf.org/occi/infrastructure#", str(flavor['name']), [RESOURCE_TEMPLATE], attributes = FLAVOR_ATTRIBUTES)
FLAVOR = Mixin("http://schemas.ogf.org/occi/resource_tpl#", str(flavor['name']), [RESOURCE_TEMPLATE], attributes = FLAVOR_ATTRIBUTES)
self.register_backend(FLAVOR, MixinBackend())
def refresh_flavors_norecursive(self, snf, client):
flavors = snf.list_flavors(True)
print "Retrieving details for each image id"
for flavor in flavors:
# details = snf.get_flavor_details(flavor['id'])
FLAVOR_ATTRIBUTES = {'occi.core.id': flavor['id'],
'occi.compute.cores': str(flavor['vcpus']),
'occi.compute.memory': str(flavor['ram']),
'occi.storage.size': str(flavor['disk']),
}
FLAVOR = Mixin("http://schemas.ogf.org/occi/infrastructure#", str(flavor['name']), [RESOURCE_TEMPLATE], attributes = FLAVOR_ATTRIBUTES)
FLAVOR = Mixin("http://schemas.ogf.org/occi/resource_tpl#", occify_terms(str(flavor['name'])), [RESOURCE_TEMPLATE], title='FLAVOR',attributes = FLAVOR_ATTRIBUTES)
self.register_backend(FLAVOR, MixinBackend())
def refresh_network_instances(self,client):
networks =client.networks_get(command = 'detail')
network_details = networks.json['networks']
......@@ -90,7 +138,7 @@ class MyAPP(Application):
self.registry.add_resource(netID, snf_net, None)
def refresh_compute_instances(self, snf, client):
'''Syncing registry with cyclades resources'''
......@@ -102,8 +150,7 @@ class MyAPP(Application):
resources = self.registry.resources
occi_keys = resources.keys()
print resources.keys()
print occi_keys
for serverID in occi_keys:
if '/compute/' in serverID and resources[serverID].attributes['occi.compute.hostname'] == "":
self.registry.delete_resource(serverID, None)
......@@ -113,20 +160,22 @@ class MyAPP(Application):
#Compute instances in synnefo not available in registry
diff = [x for x in snf_keys if '/compute/'+x not in occi_keys]
for key in diff:
details = snf.get_server_details(int(key))
flavor = snf.get_flavor_details(details['flavor']['id'])
try:
print "line 65:Finished getting image details for VM with ID" + str(details['flavor']['id'])
print "line 65:Finished getting image details for VM "+key+" with ID" + str(details['flavor']['id'])
image = snf.get_image_details(details['image']['id'])
for i in self.registry.backends:
if i.term == str(image['name']):
if i.term == occify_terms(str(image['name'])):
rel_image = i
if i.term == str(flavor['name']):
if i.term == occify_terms(str(flavor['name'])):
rel_flavor = i
resource = Resource(key, COMPUTE, [rel_flavor, rel_image])
resource.actions = [START]
......@@ -138,9 +187,7 @@ class MyAPP(Application):
resource.attributes['occi.core.title'] = str(details['name'])
networkIDs = details['addresses'].keys()
if len(networkIDs)>0:
#resource.attributes['occi.compute.hostname'] = SERVER_CONFIG['hostname'] % {'id':int(key)}
resource.attributes['occi.compute.hostname'] = str(details['addresses'][networkIDs[0]][0]['addr'])
#resource.attributes['occi.networkinterface.address'] = str(details['addresses'][networkIDs[0]][0]['addr'])
else:
resource.attributes['occi.compute.hostname'] = ""
......@@ -166,7 +213,7 @@ class MyAPP(Application):
'occi.networkinterface.allocation' : allocheme,
'occi.networking.interface': str(item['id']),
'occi.networkinterface.mac' : str(item['mac_address']),
'occi.networkinterface.ip4' : ip4address,
'occi.networkinterface.address' : ip4address,
'occi.networkinterface.ip6' : ip6address
}
elif len(details['addresses'][netKey])>0:
......@@ -174,7 +221,7 @@ class MyAPP(Application):
'occi.networkinterface.allocation' : allocheme,
'occi.networking.interface': '',
'occi.networkinterface.mac' : '',
'occi.networkinterface.ip4' : ip4address,
'occi.networkinterface.address' : ip4address,
'occi.networkinterface.ip6' : ip6address
}
......@@ -183,7 +230,7 @@ class MyAPP(Application):
'occi.networkinterface.allocation' : '',
'occi.networking.interface': '',
'occi.networkinterface.mac' : '',
'occi.networkinterface.ip4' :'',
'occi.networkinterface.address' :'',
'occi.networkinterface.ip6' : '' }
resource.links.append(NET_LINK)
......@@ -192,7 +239,7 @@ class MyAPP(Application):
except ClientError as ce:
if ce.status == 404:
print('Image not found, sorry!!!')
print('Image not found (probably older version')
continue
else:
raise ce
......@@ -203,50 +250,145 @@ class MyAPP(Application):
if '/network/' not in key:
self.registry.delete_resource(key, None)
def __call__(self, environ, response):
# Enable VOMS Authorization
print "snf-occi application has been called!"
req = Request(environ)
auth_endpoint = 'snf-auth uri=\'https://'+SERVER_CONFIG['hostname']+':5000/main\''
if not req.environ.has_key('HTTP_X_AUTH_TOKEN'):
print "Error: An authentication token has not been provided!"
status = '401 Not Authorized'
headers = [('Content-Type', 'text/html'),('Www-Authenticate',auth_endpoint)]
response(status,headers)
return [str(response)]
if ENABLE_VOMS:
if req.environ.has_key('HTTP_X_AUTH_TOKEN'):
environ['HTTP_AUTH_TOKEN']= req.environ['HTTP_X_AUTH_TOKEN']
compClient = ComputeClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
cyclClient = CycladesClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
try:
#Up-to-date flavors and images
self.refresh_images(compClient,cyclClient)
self.refresh_flavors_norecursive(compClient,cyclClient)
self.refresh_network_instances(cyclClient)
self.refresh_compute_instances(compClient,cyclClient)
# token will be represented in self.extras
return self._call_occi(environ, response, security = None, token = environ['HTTP_AUTH_TOKEN'], snf = compClient, client = cyclClient)
except HTTPError:
print "Exception from unauthorized access!"
status = '401 Not Authorized'
headers = [('Content-Type', 'text/html'),('Www-Authenticate',auth_endpoint)]
response(status,headers)
return [str(response)]
else:
#raise HTTPError(404, "Unauthorized access")
status = '401 Not Authorized'
headers = [('Content-Type', 'text/html'),('Www-Authenticate',auth_endpoint)]
response(status,headers)
return [str(response)]
else:
compClient = ComputeClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
cyclClient = CycladesClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
#Up-to-date flavors and images
print "@refresh_images"
self.refresh_images(compClient,cyclClient)
print "@refresh_flavors"
self.refresh_flavors_norecursive(compClient,cyclClient)
self.refresh_network_instances(cyclClient)
print "@refresh_compute_instances"
self.refresh_compute_instances(compClient,cyclClient)
# token will be represented in self.extras
return self._call_occi(environ, response, security = None, token = environ['HTTP_AUTH_TOKEN'], snf = compClient, client = cyclClient)
def application(env, start_response):
print "snf-occi will execute voms authentication"
t =snf_voms.VomsAuthN()
(user_dn, user_vo, user_fqans) = t.process_request(env)
print (user_dn, user_vo, user_fqans)
env['HTTP_AUTH_TOKEN'] = get_user_token(user_dn)
# Get user authentication details
astakosClient = astakos.AstakosClient(KAMAKI_CONFIG['astakos_url'], env['HTTP_AUTH_TOKEN'])
user_details = astakosClient.authenticate()
response = {'access': {'token':{'issued_at':'','expires': user_details['access']['token']['expires'] , 'id':env['HTTP_AUTH_TOKEN']},
'serviceCatalog': [],
'user':{'username': user_dn,'roles_links':user_details['access']['user']['roles_links'],'id': user_details['access']['user']['id'], 'roles':[], 'name':user_dn },
'metadata': {'is_admin': 0, 'roles': user_details['access']['user']['roles']}}}
status = '200 OK'
headers = [('Content-Type', 'application/json')]
start_response(status,headers)
def main():
body = json.dumps(response)
print body
return [body]
APP = MyAPP(registry = snfRegistry())
COMPUTE_BACKEND = ComputeBackend()
NETWORK_BACKEND = NetworkBackend()
NETWORKINTERFACE_BACKEND = NetworkInterfaceBackend()
IPNETWORK_BACKEND = IpNetworkBackend()
IPNETWORKINTERFACE_BACKEND = IpNetworkInterfaceBackend()
APP.register_backend(COMPUTE, COMPUTE_BACKEND)
APP.register_backend(START, COMPUTE_BACKEND)
APP.register_backend(STOP, COMPUTE_BACKEND)
APP.register_backend(RESTART, COMPUTE_BACKEND)
APP.register_backend(SUSPEND, COMPUTE_BACKEND)
APP.register_backend(RESOURCE_TEMPLATE, MixinBackend())
APP.register_backend(OS_TEMPLATE, MixinBackend())
def app_factory(global_config, **local_config):
"""This function wraps our simple WSGI app so it
can be used with paste.deploy"""
return application
def tenant_application(env, start_response):
print "snf-occi will return tenant information"
if env.has_key('SSL_CLIENT_S_DN_ENV'):
print env['SSL_CLIENT_S_DN_ENV'], env['SSL_CLIENT_CERT_ENV']
# Network related backends
APP.register_backend(NETWORK, NETWORK_BACKEND)
APP.register_backend(IPNETWORK, IPNETWORK_BACKEND)
APP.register_backend(NETWORKINTERFACE,NETWORKINTERFACE_BACKEND)
APP.register_backend(IPNETWORKINTERFACE, IPNETWORKINTERFACE_BACKEND)
VALIDATOR_APP = validator(APP)
HTTPD = make_server('', SERVER_CONFIG['port'], VALIDATOR_APP)
HTTPD.serve_forever()
req = Request(env)
if req.environ.has_key('HTTP_X_AUTH_TOKEN'):
env['HTTP_AUTH_TOKEN']= req.environ['HTTP_X_AUTH_TOKEN']
else:
raise HTTPError(404, "Unauthorized access")
# Get user authentication details
print "@ refresh_user authentication details"
astakosClient = astakos.AstakosClient(KAMAKI_CONFIG['astakos_url'], env['HTTP_AUTH_TOKEN'])
user_details = astakosClient.authenticate()
response = {'tenants_links': [], 'tenants':[{'description':'Instances of EGI Federated Clouds TF','enabled': True, 'id':user_details['access']['user']['id'],'name':'EGI_FCTF'}]}
status = '200 OK'
headers = [('Content-Type', 'application/json')]
start_response(status,headers)
body = json.dumps(response)
print body
return [body]
def tenant_app_factory(global_config, **local_config):
"""This function wraps our simple WSGI app so it
can be used with paste.deploy"""
return tenant_application
def occify_terms(term_name):
'''
Occifies a term_name so that it is compliant with GFD 185.
'''
term = term_name.strip().replace(' ', '_').replace('.', '-').lower()
term=term.replace('(','_').replace(')','_').replace('@','_').replace('+','-_')
return term
def get_user_token(user_dn):
config = kamaki_config.Config()
return config.get_cloud("default", "token")
"""
This it the entry point for paste deploy .
Paste config file needs to point to egg:<package name>:<entrypoint name>:
use = egg:snfOCCI#sample_app
sample_app entry point is defined in setup.py:
entry_points='''
[paste.app_factory]
sample_app = snf_voms:main
''',
which point to this function call (<module name>:function).
"""
# W0613:unused args
# pylint: disable=W0613
from snfOCCI import APIserver
#noinspection PyUnusedLocal
def main(global_config, **settings):
"""
This is the entry point for paste into the OCCI OS world.
"""
return APIserver.MyAPP()
\ No newline at end of file
SERVER_CONFIG = {
'port': 8888,
'hostname': '',
'compute_arch': ''
'hostname': '$vm_hostname$',
'compute_arch': 'x86'
}
KAMAKI_CONFIG = {
'compute_url': ''
'compute_url': 'https://cyclades.okeanos.grnet.gr/compute/v2.0/',
'astakos_url': 'https://accounts.okeanos.grnet.gr/identity/v2.0/'
}
VOMS_CONFIG = {
'enable_voms' : 'True',
'voms_policy' : '/etc/snf/voms.json',
'vomsdir_path' : '/etc/grid-security/vomsdir/',
'ca_path': '/etc/grid-security/certificates/',
'cert_dir' : '/etc/ssl/certs/',
'key_dir' : '/etc/ssl/private/'
}
\ No newline at end of file
# snf_voms authentication PasteDeploy configuration file
[composite:main]
use = egg:Paste#urlmap
/:snf_occiapp
/v2.0/tokens:authapp
/v2.0/tenants:tenantapp
[app:snf_occiapp]
use = egg:snf-occi#snf_occi_app
[app:authapp]
paste.app_factory = snfOCCI.APIserver:app_factory
[app:tenantapp]
paste.app_factory = snfOCCI.APIserver:tenant_app_factory
\ No newline at end of file
import os
from paste import deploy
import logging
LOG = logging.getLogger(__name__)
# NOTE(ldbragst): 'application' is required in this context by WSGI spec.
# The following is a reference to Python Paste Deploy documentation
# http://pythonpaste.org/deploy/
application = deploy.loadapp('config:/home/synnefo/snf_voms-paste.ini')
# snf_voms authentication PasteDeploy configuration file
[composite:main]
use = egg:Paste#urlmap
/v2.0/tokens:authapp
/v2.0/tenants:tenantapp
[app:authapp]
paste.app_factory = snfOCCI.APIserver:app_factory
[app:tenantapp]
paste.app_factory = snfOCCI.APIserver:tenant_app_factory
\ No newline at end of file
import os
from paste import deploy
import logging
LOG = logging.getLogger(__name__)
# NOTE(ldbragst): 'application' is required in this context by WSGI spec.
# The following is a reference to Python Paste Deploy documentation
# http://pythonpaste.org/deploy/
application = deploy.loadapp('config:/home/synnefo/snf_voms_auth-paste.ini')
\ No newline at end of file
......@@ -2,6 +2,8 @@ from kamaki.clients.compute import ComputeClient
from kamaki.clients.cyclades import CycladesClient
from kamaki.cli.config import Config
from snfOCCI.config import SERVER_CONFIG
from occi import registry
from occi.core_model import Mixin
from occi.backend import MixinBackend
......@@ -15,3 +17,7 @@ class snfRegistry(registry.NonePersistentRegistry):
resource.identifier = key
super(snfRegistry, self).add_resource(key, resource, extras)
def set_hostname(self, hostname):
hostname = "https://" + SERVER_CONFIG['hostname'] + ":" + str(SERVER_CONFIG['port'])
super(snfRegistry, self).set_hostname(hostname)
\ No newline at end of file
# Copyright 2012 Spanish National Research Council
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import M2Crypto
import ast
from logging import getLogger
import voms_helper
from kamaki.clients import ClientError
LOG = getLogger(__name__)
# Environment variable used to pass the request context
CONTEXT_ENV = 'snf.context'
SSL_CLIENT_S_DN_ENV = "SSL_CLIENT_S_DN"
SSL_CLIENT_CERT_ENV = "SSL_CLIENT_CERT"
SSL_CLIENT_CERT_CHAIN_ENV_PREFIX = "SSL_CLIENT_CERT_CHAIN_"
"""Global variables that contain VOMS related paths
"""
VOMS_POLICY = "/etc/snf/voms.json"
VOMSDIR_PATH = "/etc/grid-security/vomsdir/"
CA_PATH = "/etc/grid-security/certificates/"
VOMSAPI_LIB = "/usr/lib/libvomsapi.so.1"
PARAMS_ENV = 'snf_voms.params'
class VomsAuthN():
"""Filter that checks for the SSL data in the reqest.
Sets 'ssl' in the context as a dictionary containing this data.
"""
def __init__(self, *args, **kwargs):
# VOMS stuff
try:
self.voms_json = json.loads(
open(VOMS_POLICY).read())
except ValueError:
raise ClientError(
'Bad Formatted VOMS json',
details='The VOMS json data was not corectly formatted in file %s' % VOMS_POLICY)
except:
raise ClientError(
'No loading of VOMS json file',