# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright 2012 OpenStack, LLC
# All Rights Reserved.
#
#    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.
"""
Functional tests for the Swift store interface

Set the GLANCE_TEST_SWIFT_CONF environment variable to the location
of a Glance config that defines how to connect to a functional
Swift backend
"""

import ConfigParser
import os
import os.path
import random
import StringIO
import unittest
import urlparse
import urllib

import nose.plugins.skip

from glance.common import utils
import glance.openstack.common.cfg
import glance.store.swift
import glance.tests.functional.store as store_tests

try:
    import swiftclient
except ImportError:
    swiftclient = None


class SwiftStoreError(RuntimeError):
    pass


def _uniq(value):
    return '%s.%d' % (value, random.randint(0, 99999))


def read_config(path):
    cp = ConfigParser.RawConfigParser()
    cp.read(path)
    return cp


def parse_config(config):
    out = {}
    options = [
        'swift_store_auth_address',
        'swift_store_auth_version',
        'swift_store_user',
        'swift_store_key',
        'swift_store_container',
    ]

    for option in options:
        out[option] = config.defaults()[option]

    return out


def swift_connect(auth_url, auth_version, user, key):
    try:
        return swiftclient.Connection(authurl=auth_url,
                                      auth_version=auth_version,
                                      user=user,
                                      key=key,
                                      snet=False,
                                      retries=1)
    except AttributeError:
        msg = "Could not find swiftclient module"
        raise nose.SkipTest(msg)


def swift_list_containers(swift_conn):
    try:
        _, containers = swift_conn.get_account()
    except Exception, e:
        msg = ("Failed to list containers (get_account) "
               "from Swift. Got error: %s" % e)
        raise SwiftStoreError(msg)
    else:
        return containers


def swift_create_container(swift_conn, container_name):
    try:
        swift_conn.put_container(container_name)
    except swiftclient.ClientException, e:
        msg = "Failed to create container. Got error: %s" % e
        raise SwiftStoreError(msg)


def swift_get_container(swift_conn, container_name, **kwargs):
    return swift_conn.get_container(container_name, **kwargs)


def swift_delete_container(swift_conn, container_name):
    try:
        swift_conn.delete_container(container_name)
    except swiftclient.ClientException, e:
        msg = "Failed to delete container from Swift. Got error: %s" % e
        raise SwiftStoreError(msg)


def swift_put_object(swift_conn, container_name, object_name, contents):
    return swift_conn.put_object(container_name, object_name, contents)


def swift_head_object(swift_conn, container_name, obj_name):
    return swift_conn.head_object(container_name, obj_name)


def keystone_authenticate(auth_url, auth_version, tenant_name,
        username, password):
    assert int(auth_version) == 2, 'Only auth version 2 is supported'

    import keystoneclient.v2_0.client
    ksclient = keystoneclient.v2_0.client.Client(tenant_name=tenant_name,
                                                 username=username,
                                                 password=password,
                                                 auth_url=auth_url)

    auth_resp = ksclient.service_catalog.catalog
    tenant_id = auth_resp['token']['tenant']['id']
    service_catalog = auth_resp['serviceCatalog']
    return tenant_id, ksclient.auth_token, service_catalog


class TestSwiftStore(store_tests.BaseTestCase, unittest.TestCase):

    store_cls = glance.store.swift.Store
    store_name = 'swift'

    def setUp(self):
        config_path = os.environ.get('GLANCE_TEST_SWIFT_CONF')
        if not config_path:
            msg = "GLANCE_TEST_SWIFT_CONF environ not set."
            raise nose.SkipTest(msg)

        glance.openstack.common.cfg.CONF(default_config_files=[config_path])

        raw_config = read_config(config_path)
        config = parse_config(raw_config)

        swift = swift_connect(config['swift_store_auth_address'],
                              config['swift_store_auth_version'],
                              config['swift_store_user'],
                              config['swift_store_key'])

        #NOTE(bcwaldon): Ensure we have a functional swift connection
        swift_list_containers(swift)

        self.swift_client = swift
        self.swift_config = config

        self.swift_config['swift_store_create_container_on_put'] = True

        super(TestSwiftStore, self).setUp()

    def get_store(self, **kwargs):
        store = glance.store.swift.Store(context=kwargs.get('context'))
        store.configure()
        store.configure_add()
        return store

    def get_default_store_specs(self, image_id):
        return {
            'scheme': 'swift+http',
            'auth_or_store_url': self.swift_config['swift_store_auth_address'],
            'user': self.swift_config['swift_store_user'],
            'key': self.swift_config['swift_store_key'],
            'container': self.swift_config['swift_store_container'],
            'obj': image_id,
        }

    def test_object_chunking(self):
        """Upload an image that is split into multiple swift objects.

        We specifically check the case that
        image_size % swift_store_large_object_chunk_size != 0 to
        ensure we aren't losing image data.
        """
        self.config(
            swift_store_large_object_size=2,  # 2 MB
            swift_store_large_object_chunk_size=2,  # 2 MB
        )
        store = self.get_store()
        image_id = utils.generate_uuid()
        image_size = 5242880  # 5 MB
        image_data = StringIO.StringIO('X' * image_size)
        image_checksum = 'eb7f8c3716b9f059cee7617a4ba9d0d3'
        uri, add_size, add_checksum = store.add(image_id,
                                                image_data,
                                                image_size)

        self.assertEqual(image_size, add_size)
        self.assertEqual(image_checksum, add_checksum)

        location = glance.store.location.Location(
                self.store_name,
                store.get_store_location_class(),
                uri=uri,
                image_id=image_id)

        # Store interface should still be respected even though
        # we are storing images in multiple Swift objects
        (get_iter, get_size) = store.get(location)
        self.assertEqual('5242880', get_size)
        self.assertEqual('X' * 5242880, ''.join(get_iter))

        # The object should have a manifest pointing to the chunks
        # of image data
        swift_location = location.store_location
        headers = swift_head_object(self.swift_client,
                                    swift_location.container,
                                    swift_location.obj)
        manifest = headers.get('x-object-manifest')
        self.assertTrue(manifest)

        # Verify the objects in the manifest exist
        manifest_container, manifest_prefix = manifest.split('/', 1)
        container = swift_get_container(self.swift_client,
                                        manifest_container,
                                        prefix=manifest_prefix)
        segments = [segment['name'] for segment in container[1]]

        for segment in segments:
            headers = swift_head_object(self.swift_client,
                                        manifest_container,
                                        segment)
            self.assertTrue(headers.get('content-length'))

        # Since we used a 5 MB image with a 2 MB chunk size, we should
        # expect to see the manifest object and three data objects for
        # a total of 4
        self.assertEqual(4, len(segments), 'Got segments %s' % segments)

        store.delete(location)

        # Verify the segments in the manifest are all gone
        for segment in segments:
            self.assertRaises(swiftclient.ClientException,
                              swift_head_object,
                              self.swift_client,
                              manifest_container,
                              segment)

    def stash_image(self, image_id, image_data):
        container_name = self.swift_config['swift_store_container']
        swift_put_object(self.swift_client,
                         container_name,
                         image_id,
                         'XXX')

        #NOTE(bcwaldon): This is a hack until we find a better way to
        # build this URL
        auth_url = self.swift_config['swift_store_auth_address']
        auth_url = urlparse.urlparse(auth_url)
        user = urllib.quote(self.swift_config['swift_store_user'])
        key = self.swift_config['swift_store_key']
        netloc = ''.join(('%s:%s' % (user, key), '@', auth_url.netloc))
        path = os.path.join(auth_url.path, container_name, image_id)

        # This is an auth url with /<CONTAINER>/<OBJECT> on the end
        return 'swift+http://%s%s' % (netloc, path)

    def test_multitenant(self):
        """Ensure an image is properly configured when using multitenancy."""
        fake_swift_admin = 'd2f68325-8e2c-4fb1-8c8b-89de2f3d9c4a'
        self.config(
            swift_store_multi_tenant=True,
        )

        swift_store_user = self.swift_config['swift_store_user']
        tenant_name, username = swift_store_user.split(':')
        tenant_id, auth_token, service_catalog = keystone_authenticate(
                self.swift_config['swift_store_auth_address'],
                self.swift_config['swift_store_auth_version'],
                tenant_name,
                username,
                self.swift_config['swift_store_key'])

        context = glance.context.RequestContext(
                tenant=tenant_id,
                service_catalog=service_catalog,
                auth_tok=auth_token)
        store = self.get_store(context=context)

        image_id = utils.generate_uuid()
        image_data = StringIO.StringIO('XXX')
        uri, _, _ = store.add(image_id, image_data, 3)

        location = glance.store.location.Location(
                self.store_name,
                store.get_store_location_class(),
                uri=uri,
                image_id=image_id)

        read_tenant = utils.generate_uuid()
        write_tenant = utils.generate_uuid()
        store.set_acls(location, public=False,
                read_tenants=[read_tenant], write_tenants=[write_tenant])

        container_name = location.store_location.container
        container, _ = swift_get_container(self.swift_client, container_name)
        self.assertEqual(read_tenant, container.get('x-container-read'))
        self.assertEqual(write_tenant, container.get('x-container-write'))

        store.set_acls(location, public=True, read_tenants=[read_tenant])

        container_name = location.store_location.container
        container, _ = swift_get_container(self.swift_client, container_name)
        self.assertEqual('.r:*', container.get('x-container-read'))
        self.assertEqual('', container.get('x-container-write', ''))

        (get_iter, get_size) = store.get(location)
        self.assertEqual('3', get_size)
        self.assertEqual('XXX', ''.join(get_iter))

        store.delete(location)
