Is it possible to use the OpenStack.NET SDK with SoftLayer object storage?

363 views Asked by At

SoftLayer Object Storage is based on the OpenStack Swift object store.

SoftLayer provide SDKs for their object storage in Python, Ruby, Java and PHP, but not in .NET. Searching for .NET SDKs for OpenStack, I came across OpenStack.NET.

Based on this question OpenStack.NET is designed for use with Rackspace by default, but can be made to work with other OpenStack providers using CloudIdentityWithProject and OpenStackIdentityProvider.

SoftLayer provide the following information for connecting to their Object Storage:

Authentication Endpoint
Public: https://mel01.objectstorage.softlayer.net/auth/v1.0/
Private: https://mel01.objectstorage.service.networklayer.com/auth/v1.0/

Username:
SLOS123456-1:[email protected]

API Key (Password):
1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef

It's not obvious how this would map to the fields of CloudIdentityWithProject, and OpenStackIdentityProvider but I tried the following and a few other combinations of project name / username / uri:

var cloudIdentity = new CloudIdentityWithProject()
{
    ProjectName = "SLOS123456-1",
    Username = "[email protected]",
    Password = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
};

var identityProvider = new OpenStackIdentityProvider(
    new Uri("https://mel01.objectstorage.softlayer.net/auth/v1.0/"),
    cloudIdentity);

var token = identityProvider.GetToken(null);

However, in all cases I received the following error:

Unable to authenticate user and retrieve authorized service endpoints

Based on reviewing the source code for SoftLayer's other language libraries and for OpenStack.NET, it looks like SoftLayer's object storage uses V1 auth, while OpenStack.NET is using V2 auth.

Based on this article from SoftLayer and this article from SwiftStack, V1 auth uses a /auth/v1.0/ path (like the one provided by SoftLayer), with X-Auth-User and X-Auth-Key headers as arguments, and with the response contained in headers like the following:

X-Auth-Token-Expires = 83436
X-Auth-Token = AUTH_tk1234567890abcdef1234567890abcdef
X-Storage-Token = AUTH_tk1234567890abcdef1234567890abcdef
X-Storage-Url = https://mel01.objectstorage.softlayer.net/v1/AUTH_12345678-1234-1234-1234-1234567890ab
X-Trans-Id = txbc1234567890abcdef123-1234567890
Connection = keep-alive
Content-Length = 1300
Content-Type = text/html; charset=UTF-8
Date = Wed, 14 Oct 2015 01:19:45 GMT

Whereas V2 auth (identity API V2.0) uses a /v2.0/tokens path, with the request and response in JSON objects in the message body.

Based on the OpenStackIdentityProvider class in OpenStack.NET I hacked together my own SoftLayerOpenStackIdentityProvider like this:

using JSIStudios.SimpleRESTServices.Client;
using net.openstack.Core.Domain;
using net.openstack.Providers.Rackspace;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenStack.Authentication;
using System;
using System.Linq;
using System.Collections.Generic;

namespace OpenStackTest1
{
    public class SoftLayerOpenStackIdentityProvider : CloudIdentityProvider
    {
        public SoftLayerOpenStackIdentityProvider(
            Uri urlBase, CloudIdentity defaultIdentity)
            : base(defaultIdentity, null, null, urlBase)
        {
            if (urlBase == null)
                throw new ArgumentNullException("urlBase");
        }

        public override UserAccess GetUserAccess(
            CloudIdentity identity, bool forceCacheRefresh = false)
        {
            identity = identity ?? DefaultIdentity;

            Func<UserAccess> refreshCallback =
                () =>
                {
                    // Set up request headers.
                    Dictionary<string, string> headers = 
                        new Dictionary<string, string>();
                    headers["X-Auth-User"] = identity.Username;
                    headers["X-Auth-Key"] = identity.APIKey;

                    // Make the request.
                    JObject requestBody = null;
                    var response = ExecuteRESTRequest<JObject>(
                        identity, 
                        UrlBase, 
                        HttpMethod.GET, 
                        requestBody, 
                        headers: headers, 
                        isTokenRequest: true);
                    if (response == null || response.Data == null)
                        return null;

                    // Get response headers.
                    string authToken = response.Headers.Single(
                        h => h.Key == "X-Auth-Token").Value;
                    string storageUrl = response.Headers.Single(
                        h => h.Key == "X-Storage-Url").Value;
                    string tokenExpires = response.Headers.Single(
                        h => h.Key == "X-Auth-Token-Expires").Value;

                    // Convert expiry from seconds to a date.
                    int tokenExpiresSeconds = Int32.Parse(tokenExpires);
                    DateTimeOffset tokenExpiresDate = 
                        DateTimeOffset.UtcNow.AddSeconds(tokenExpiresSeconds);

                    // Create UserAccess via JSON deseralization.
                    UserAccess access = JsonConvert.DeserializeObject<UserAccess>(
                        String.Format(
                            "{{ " +
                            "  token: {{ id: '{0}', expires: '{1}' }}, " +
                            "  serviceCatalog: " +
                            "  [ " +
                            "     {{ " +
                            "        endpoints: [ {{ publicUrl: '{2}' }} ], " +
                            "        type: 'object-store', " +
                            "        name: 'swift' " +
                            "     }} " +
                            "  ], " +
                            "  user: {{ }} " +
                            "}}",
                            authToken,
                            tokenExpiresDate,
                            storageUrl));
                    if (access == null || access.Token == null)
                        return null;

                    return access;
                };

            string key = string.Format("{0}:{1}", UrlBase, identity.Username);
            var userAccess = TokenCache.Get(key, refreshCallback, forceCacheRefresh);

            return userAccess;
        }

        protected override string LookupServiceTypeKey(IServiceType serviceType)
        {
            return serviceType.Type;
        }
    }
}

Because some of the members of UserAccess (like IdentityToken and Endpoint) have no way to set their fields (the objects have only a default constructor and only read-only members), I had to create the UserAccess object by deserializing some temporary JSON in a similar format as returned by the V2 API.

This works, ie I can now connect like this:

var cloudIdentity = new CloudIdentity()
{
    Username = "SLOS123456-1:[email protected]",
    APIKey = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
};

var identityProvider = new SoftLayerOpenStackIdentityProvider(
    new Uri("https://mel01.objectstorage.softlayer.net/auth/v1.0/"),
    cloudIdentity);

var token = identityProvider.GetToken(null);

And then get access to files etc like this:

var cloudFilesProvider = new CloudFilesProvider(identityProvider);

var containers = cloudFilesProvider.ListContainers();

var stream = new MemoryStream();
cloudFilesProvider.GetObject("testcontainer", "testfile.dat", stream);

However, is there a better way than this to use SoftLayer Object Storage from .NET?

I briefly also looked at the OpenStack SDK for .NET (a different library to OpenStack.NET), but it too seems to be based on V2 auth.

0

There are 0 answers