APIserver.py 18.2 KB
Newer Older
John Giannelos's avatar
John Giannelos committed
1 2
#!/usr/bin/env python

3 4 5
import json
import uuid

John Giannelos's avatar
John Giannelos committed
6
from snfOCCI.registry import snfRegistry
7
from snfOCCI.compute import ComputeBackend, SNFBackend
8 9
from snfOCCI.config import (
    SERVER_CONFIG, KAMAKI_CONFIG, VOMS_CONFIG, KEYSTONE_URL)
10
from snfOCCI import snf_voms
11 12 13
from snfOCCI.network import (
    NetworkBackend, IpNetworkBackend, IpNetworkInterfaceBackend,
    NetworkInterfaceBackend)
14 15 16 17
from kamaki.clients.cyclades import CycladesNetworkClient
from snfOCCI.extensions import snf_addons

from kamaki.clients.cyclades import CycladesComputeClient as ComputeClient
18
from kamaki.clients.cyclades import CycladesClient
19
from kamaki.clients import astakos
20
from kamaki.clients import ClientError
John Giannelos's avatar
John Giannelos committed
21

22
from occi.core_model import Mixin, Resource
23
from occi.backend import MixinBackend
24 25 26
from occi.extensions.infrastructure import (
    COMPUTE, START, STOP, SUSPEND, RESTART, RESOURCE_TEMPLATE, OS_TEMPLATE,
    NETWORK, IPNETWORK, NETWORKINTERFACE, IPNETWORKINTERFACE)
27 28 29
from occi import wsgi
from occi.exceptions import HTTPError
from occi import core_model
John Giannelos's avatar
John Giannelos committed
30 31

from wsgiref.validate import validator
32
from webob import Request
John Giannelos's avatar
John Giannelos committed
33

34

35
class MyAPP(wsgi.Application):
36
    """An OCCI WSGI application"""
John Giannelos's avatar
John Giannelos committed
37

38
    def __init__(self):
39
        """Initialization of the WSGI OCCI application for synnefo"""
40 41
        global ENABLE_VOMS, VOMS_DB
        ENABLE_VOMS = VOMS_CONFIG['enable_voms']
42
        super(MyAPP, self).__init__(registry=snfRegistry())
43 44
        self._register_backends()
        VALIDATOR_APP = validator(self)
45

46 47 48
    def _register_backends(self):
        print "Inside Register Backends"
        COMPUTE_BACKEND = ComputeBackend()
49
        NETWORK_BACKEND = NetworkBackend()
50 51 52
        NETWORKINTERFACE_BACKEND = NetworkInterfaceBackend()
        IPNETWORK_BACKEND = IpNetworkBackend()
        IPNETWORKINTERFACE_BACKEND = IpNetworkInterfaceBackend()
53

54 55 56 57 58 59 60
        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())
61

62 63 64
        # Network related backends
        self.register_backend(NETWORK, NETWORK_BACKEND)
        self.register_backend(IPNETWORK, IPNETWORK_BACKEND)
65
        self.register_backend(NETWORKINTERFACE, NETWORKINTERFACE_BACKEND)
66
        self.register_backend(IPNETWORKINTERFACE, IPNETWORKINTERFACE_BACKEND)
67 68 69
        self.register_backend(snf_addons.SNF_USER_DATA_EXT, SNFBackend())
        self.register_backend(snf_addons.SNF_KEY_PAIR_EXT,  SNFBackend())

70
    def refresh_images(self, snf, client):
71 72 73 74 75 76 77 78
        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],
79
                    title='IMAGE', attributes=IMAGE_ATTRIBUTES)
80 81 82
                self.register_backend(IMAGE, MixinBackend())
        except:
            raise HTTPError(404, "Unauthorized access")
83

84
    def refresh_flavors(self, snf, client):
John Giannelos's avatar
John Giannelos committed
85
        flavors = snf.list_flavors()
86
        print "Retrieving details for each flavor"
John Giannelos's avatar
John Giannelos committed
87 88
        for flavor in flavors:
            details = snf.get_flavor_details(flavor['id'])
89 90 91 92 93 94 95 96 97 98 99
            FLAVOR_ATTRIBUTES = {
                'occi.core.id': flavor['id'],
                'occi.compute.cores': str(details['vcpus']),
                '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)
John Giannelos's avatar
John Giannelos committed
100
            self.register_backend(FLAVOR, MixinBackend())
101

102 103 104 105
    def refresh_flavors_norecursive(self, snf, client):
        flavors = snf.list_flavors(True)
        print "@ Retrieving details for each flavor"
        for flavor in flavors:
106 107 108 109 110 111 112 113 114 115 116
            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/resource_tpl#",
                occify_terms(str(flavor['name'])),
                [RESOURCE_TEMPLATE],
                title='FLAVOR', attributes=FLAVOR_ATTRIBUTES)
117
            self.register_backend(FLAVOR, MixinBackend())
118

119
    def refresh_network_instances(self, client):
120 121 122 123
        print "@ refresh NETWORKS"
        network_details = client.list_networks(detail='True')
        resources = self.registry.resources
        occi_keys = resources.keys()
124

125 126
        for network in network_details:
            if '/network/'+str(network['id']) not in occi_keys:
127
                netID = '/network/'+str(network['id'])
128
                snf_net = core_model.Resource(netID, NETWORK, [IPNETWORK])
129 130 131
                snf_net.attributes['occi.core.id'] = str(network['id'])

                # This info comes from the network details
132 133 134
                snf_net.attributes['occi.network.state'] = str(
                    network['status'])
                snf_net.attributes['occi.network.gateway'] = ''
135
                if network['public'] is True:
136 137 138
                    snf_net.attributes['occi.network.type'] = "Public = True"
                else:
                    snf_net.attributes['occi.network.type'] = "Public = False"
139
                self.registry.add_resource(netID, snf_net, None)
140

141
    def refresh_compute_instances(self, snf, client):
142
        """Syncing registry with cyclades resources"""
143
        print "@ Refresh COMPUTE INSTANCES"
144

145 146 147 148 149 150 151
        servers = snf.list_servers()
        snf_keys = []
        for server in servers:
            snf_keys.append(str(server['id']))

        resources = self.registry.resources
        occi_keys = resources.keys()
152

153 154
        print occi_keys
        for serverID in occi_keys:
155 156
            if '/compute/' in serverID and resources[serverID].attributes[
                    'occi.compute.hostname'] == "":
157 158
                self.registry.delete_resource(serverID, None)
        occi_keys = resources.keys()
159

160 161
        # Compute instances in synnefo not available in registry
        diff = [x for x in snf_keys if '/compute/'+x not in occi_keys]
162 163
        for key in diff:
            details = snf.get_server_details(int(key))
164 165
            flavor = snf.get_flavor_details(details['flavor']['id'])
            try:
166 167
                print "Get image of flavor {flavor}, VM {vm}".format(
                    flavor=details['flavor']['id'], vm=key)
168 169
                image = snf.get_image_details(details['image']['id'])
                for i in self.registry.backends:
170
                    if i.term == occify_terms(str(image['name'])):
171
                        rel_image = i
172
                    if i.term == occify_terms(str(flavor['name'])):
173
                        rel_flavor = i
174

175 176 177 178
                resource = Resource(key, COMPUTE, [rel_flavor, rel_image])
                resource.actions = [START]
                resource.attributes['occi.core.id'] = key
                resource.attributes['occi.compute.state'] = 'inactive'
179 180 181 182
                resource.attributes['occi.compute.architecture'] = (
                    SERVER_CONFIG['compute_arch'])
                resource.attributes['occi.compute.cores'] = str(
                    flavor['vcpus'])
183 184 185
                resource.attributes['occi.compute.memory'] = str(flavor['ram'])
                resource.attributes['occi.core.title'] = str(details['name'])
                networkIDs = details['addresses'].keys()
186
                if len(networkIDs) > 0:
187 188
                    resource.attributes['occi.compute.hostname'] = str(
                        details['addresses'][networkIDs[0]][0]['addr'])
189 190
                else:
                    resource.attributes['occi.compute.hostname'] = ""
191 192 193 194 195
                self.registry.add_resource(key, resource, None)

                net_str = (
                    "http://schemas.ogf.org/occi/infrastructure#"
                    "networkinterface{0}".format(link_id))
196 197
                for netKey in networkIDs:
                    link_id = str(uuid.uuid4())
198 199 200 201 202
                    NET_LINK = core_model.Link(
                        net_str,
                        NETWORKINTERFACE, [IPNETWORKINTERFACE], resource,
                        self.registry.resources['/network/'+str(netKey)])

203
                    for version in details['addresses'][netKey]:
204 205
                        ip4address = ''
                        ip6address = ''
206
                        if version['version'] == 4:
207 208
                            ip4address = str(version['addr'])
                            allocheme = str(version['OS-EXT-IPS:type'])
209 210
                        elif version['version'] == 6:
                            ip6address = str(version['addr'])
211 212
                        allocheme = str(version['OS-EXT-IPS:type'])

213 214
                    if 'attachments' in details.keys():
                        for item in details['attachments']:
215
                            NET_LINK.attributes = {
216 217
                                'occi.core.id': link_id,
                                'occi.networkinterface.allocation': allocheme,
218
                                'occi.networking.interface': str(item['id']),
219
                                'occi.networkinterface.mac': str(
220
                                    item['mac_address']),
221 222
                                'occi.networkinterface.address': ip4address,
                                'occi.networkinterface.ip6':  ip6address}
223
                    elif len(details['addresses'][netKey]) > 0:
224 225 226
                        NET_LINK.attributes = {
                            'occi.core.id': link_id,
                            'occi.networkinterface.allocation': allocheme,
227
                            'occi.networking.interface': '',
228 229 230
                            'occi.networkinterface.mac': '',
                            'occi.networkinterface.address': ip4address,
                            'occi.networkinterface.ip6':  ip6address}
231
                    else:
232 233 234
                        NET_LINK.attributes = {
                            'occi.core.id': link_id,
                            'occi.networkinterface.allocation': '',
235
                            'occi.networking.interface': '',
236 237 238
                            'occi.networkinterface.mac': '',
                            'occi.networkinterface.address': '',
                            'occi.networkinterface.ip6': ''}
239 240 241
                    resource.links.append(NET_LINK)
                    self.registry.add_resource(link_id, NET_LINK, None)
            except ClientError as ce:
242
                print ce.status
243 244 245 246 247
                if ce.status == 404 or ce.status == 500:
                    print('Image not found, sorry!!!')
                    continue
                else:
                    raise ce
248 249

        # Compute instances in registry not available in synnefo
250 251
        diff = [x for x in occi_keys if x[9:] not in snf_keys]
        for key in diff:
252 253
            if '/network/' not in key:
                self.registry.delete_resource(key, None)
254

John Giannelos's avatar
John Giannelos committed
255
    def __call__(self, environ, response):
256
        """Enable VOMS Authorization"""
257 258
        print "SNF_OCCI application has been called!"
        req = Request(environ)
259

260
        if 'HTTP_X_AUTH_TOKEN' not in req.environ:
261 262
            print "An authentication token has NOT been provided!"
            status = '401 Not Authorized'
263 264 265
            headers = [
                ('Content-Type', 'text/html'),
                ('Www-Authenticate', 'Keystone uri=\'{uri}\''.format(
266
                    uri=KEYSTONE_URL))]
267
            response(status, headers)
268
            print 'Ask for redirect to URL {uri}'.format(uri=KEYSTONE_URL)
269 270 271 272 273 274 275 276
            return [str(response)]
        print 'An authentication token has been provided'
        environ['HTTP_AUTH_TOKEN'] = req.environ['HTTP_X_AUTH_TOKEN']
        try:
            print "Get project"
            snf_project = req.environ['HTTP_X_SNF_PROJECT']
        except KeyError:
            print "No project provided, go to plan B"
277 278
            astakosClient = astakos.AstakosClient(
                KAMAKI_CONFIG['astakos_url'], environ['HTTP_AUTH_TOKEN'])
279 280 281 282 283 284 285 286 287
            projects = astakosClient.get_projects()
            user_info = astakosClient.authenticate()
            user_uuid = user_info['access']['user']['id']
            snf_project = '6d9ec935-fcd4-4ae1-a3a0-10e612c4f867'
            for project in projects:
                if project['id'] != user_uuid:
                    snf_project = project['id']
                    print "Project found"
                    break
288 289 290 291 292 293 294
        if ENABLE_VOMS:
            compClient = ComputeClient(
                KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
            cyclClient = CycladesClient(
                KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
            netClient = CycladesNetworkClient(
                KAMAKI_CONFIG['network_url'], environ['HTTP_AUTH_TOKEN'])
295
            try:
296
                # Up-to-date flavors and images
297 298 299 300 301
                self.refresh_images(compClient, cyclClient)
                self.refresh_flavors_norecursive(compClient, cyclClient)
                self.refresh_network_instances(netClient)
                self.refresh_compute_instances(compClient, cyclClient)
                # token will be represented in self.extras
302 303 304 305 306
                return self._call_occi(
                    environ, response,
                    security=None, token=environ['HTTP_AUTH_TOKEN'],
                    snf=compClient, client=cyclClient,
                    snf_network=netClient, snf_project=snf_project)
307 308 309 310 311 312
            except HTTPError:
                print "Exception from unauthorized access!"
                status = '401 Not Authorized'
                headers = [
                    ('Content-Type', 'text/html'),
                    ('Www-Authenticate', 'Keystone uri=\'{uri}\''.format(
313
                        uri=KEYSTONE_URL))]
314
                response(status, headers)
315
                print 'Ask for redirect to {uri}'.format(uri=KEYSTONE_URL)
316 317 318
                return [str(response)]
        else:
            print 'I have a token and a project, we can proceed'
319 320 321 322 323 324
            compClient = ComputeClient(
                KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
            cyclClient = CycladesClient(
                KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
            netClient = CycladesNetworkClient(
                KAMAKI_CONFIG['network_url'], environ['HTTP_AUTH_TOKEN'])
325

326 327 328 329
            # Up-to-date flavors and images
            self.refresh_images(compClient, cyclClient)

            self.refresh_flavors_norecursive(compClient, cyclClient)
330
            self.refresh_network_instances(cyclClient)
331 332
            self.refresh_compute_instances(compClient, cyclClient)

333
            # token will be represented in self.extras
334 335 336 337 338 339
            return self._call_occi(
                environ, response,
                security=None, token=environ['HTTP_AUTH_TOKEN'],
                snf=compClient, client=cyclClient, snf_network=netClient,
                snf_project=snf_project)

340 341 342 343

def application(env, start_response):
    """/v2.0/tokens"""
    print "In /v2.0/tokens"
344
    t = snf_voms.VomsAuthN()
345 346
    user_dn, user_vo, user_fqans, snf_token, snf_project = t.process_request(
        env)
347 348 349 350 351 352 353

    print (user_dn, user_vo, user_fqans)
    env['HTTP_AUTH_TOKEN'] = snf_token
    env['SNF_PROJECT'] = snf_project
    # Get user authentication details
    print "@ refresh_user authentication details"
    pool = False
354
    astakosClient = astakos.AstakosClient(
355
        KAMAKI_CONFIG['astakos_url'], env['HTTP_AUTH_TOKEN'], use_pool=pool)
356
    user_details = astakosClient.authenticate()
357

358 359 360 361
    response = {
        'access': {
            'token': {
                'issued_at': '',
362 363
                'expires': user_details['access']['token']['expires'],
                'id': env['HTTP_AUTH_TOKEN']
364 365 366 367
            },
            'serviceCatalog': [],
            'user': {
                'username': user_dn,
368
                'roles_links': user_details['access']['user']['roles_links'],
369
                'id': user_details['access']['user']['id'],
370 371
                'roles': [],
                'name': user_dn
372 373 374 375 376 377
            },
            'metadata': {
                'is_admin': 0,
                'roles': user_details['access']['user']['roles']
            }
        }
378
    }
379
    status = '200 OK'
380 381
    headers = [('Content-Type', 'application/json')]
    start_response(status, headers)
382 383 384
    body = json.dumps(response)
    print body
    return [body]
John Giannelos's avatar
John Giannelos committed
385

386

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

392

393 394 395 396
def tenant_application(env, start_response):
    """/v2.0/tennants"""
    print "In /v2.0/tennants"
    req = Request(env)
397
    if 'HTTP_X_AUTH_TOKEN' in req.environ:
398
        env['HTTP_AUTH_TOKEN'] = req.environ['HTTP_X_AUTH_TOKEN']
399
    else:
400 401
        raise HTTPError(404, "Unauthorized access")

402 403 404
    # Get user authentication details
    print "@ refresh_user authentication details"
    pool = False
405 406
    astakosClient = astakos.AstakosClient(
        KAMAKI_CONFIG['astakos_url'], env['HTTP_AUTH_TOKEN'], use_pool=pool)
407
    user_details = astakosClient.authenticate()
408

409 410
    response = {
        'tenants_links': [],
411
        'tenants': [
412 413 414 415 416 417 418 419
            {
                'description': 'Instances of EGI Federated Clouds TF',
                'enabled': True,
                'id': user_details['access']['user']['id'],
                'name': 'ops'
            },
        ]
    }
420
    status = '200 OK'
421
    headers = [('Content-Type', 'application/json'), ]
422
    start_response(status, headers)
423 424 425
    body = json.dumps(response)
    print body
    return [body]
John Giannelos's avatar
John Giannelos committed
426

427

428 429 430 431
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
432

433

434 435 436 437
def occify_terms(term):
    """:return: Occified term, compliant with GFD 185"""
    return term.strip().lower().replace(' ', '_').replace('.', '-').replace(
        '(', '_').replace(')', '_').replace('@', '_').replace('+', '-_')