Jump to: navigation, search

OpenStack-SDK-DotNet/HighLevelArch

< OpenStack-SDK-DotNet
Revision as of 21:24, 29 April 2014 by Wayne-foley (talk | contribs) (Extending the API)


High Level Goals

  1. Provide a simple, easily consumable, and intuitive surface area for the end user.
  2. Provide a flexible code base that can be easily extended by third parties, without altering the overall usage pattern of the end user.
  3. Provide a decoupled architecture, that allows for rapid and distributed development.
  4. Strive for a SOLID and sustainable design, that can grow with the project.


Current Design

HighLevelArch.PNG


The basic flow for consuming the API is:

  1. Create the appropriate credential based on the desired OpenStack instance.
  2. Ask the Client Factory for an instance of the appropriate client object using the credential.
  3. Connect the client.
  4. Ask the acquired client for the desired service client (i.e. storage, or identity, or compute).
  5. use the provided service client to communicate with the remote services.


A simple code example is:

   var authUri = new Uri("https://region.identity.host.com:12345/v2.0/tokens");
   var userName = "user name";
   var password = "password";
   var tenantId = "XXXXXXXXXXXXXX-Project";
   
   var credential = new OpenStackCredential(authUri, userName, password, tenantId);
   var client = OpenstackClientFactory.Create(credential);
   await client.Connect();
   var storageServiceClient = client.CreateServiceClient<IStorageServiceClient>();
   var storageAccount = await storageServiceClient.GetStorageAccount();
   foreach(var container in storageAccount.Containers)
   {
       Console.WriteLine(container.Name);
   }

Extending the API

While the main goal is to create an easy to use API for the end user, it is equally important to provide a flexible code base that can be extended, and modified to provide additional features and functionality for the end user. The current design illustrated above is designed to introduce extensibility points throughout the stack. This design enables a number of extensibility scenarios (listed below), and empowers third parties to build on top of the core API.

Third-parties should take a binary or Nuget dependency on the OpenStack .NET API, and should refrain from taking a source dependency. Ideally a third-party that wishes to extend or modify the OpenStack API should take a dependency on the API, and distribute their modified/extended version of the API as a separate package. This maintains a clear separation of concerns, and provides a clear distinction to the end user as to what package/library they are using, while still maintaining a consistent surface area and flow.

There are two main ways to extend the API. The first method is to simply create types that implement the key interfaces (IOpenStackClient, IOpenStackServiceClient, etc). and use the Client and Service Client Manager classes to register the new implementations. The second method is to use the built in service location infrastructure to override (replace) the existing implementations for internal interfaces and alter the behavior of specific pieces of the stack while leaving others untouched. There are scenarios (see below) where each of these methods may or may not be needed. By using both methods, most if not all common extensibility scenarios can be achieved.

Scenario 1 - Extending authentication

'As a third-party, I have an identity service that is different then the "stock" OpenStack identity service. I want to extend the API such that my custom authentication service can be used.'

For this scenario, the third-party would create classes that derive from and implement the IOpenStackCredential, IIdentityServiceClient, and IOpenStackServiceClientDefinition interfaces. The third-party would then use the Service Client Manager class to register their implementation of the IIdentityServiceClient using their derived service client definition. The end user would then need to create an instance of the third-party's implementation of the IOpenStackCredential interface, pass it to the Client Manger to get an instance of a client. This is the same basic flow illustrated above. Once the user calls the Connect method on the instance of the client the stock code turns to the Service Client Manager class and asks it for an IIdentityServiceClient that knows how to handle the third parties credential object. The third-party's implementation of the IIdentityServiceClient would be returned, and it's Authenticate method is called. To the end user, there is no difference to the standard flow, other then the construction of a specific credential for the system they wish to connect to.

Note:
If the third-party needs to make more extensive changes to how the client interacts with the IIdentityServiceClient interface, the client itself can be extended, and registered with the Client Manager object.

Scenario 2 - Supporting third-party services

'As a third-party, I have a service that is not a part of the standard OpenStack group of services. I want to extend the API to allow the user to interact with my custom service.'

For this scenario, the third-party would create two classes. One class would derive from the IOpenStackServiceClient interface, and contain the implementation details for interacting with the custom service. The second class would derive from and implement the IOpenStackServiceDefinition interface. Using the derived service definition, the third-party would register their service client with the Service Client Manager class. The end user, after creating a credential, getting a client, and connecting the client would then request the third-party's service client.

 var thirdPartyServiceClient = client.CreateServiceClient<IThirdPartyServiceClient>();

This follows the same end user flow that was illustrated above, and offers a consistent way to access services (stock, third party, or otherwise).

Scenario 3 - Supporting customized OpenStack services

'As a third-party, I have customized the stock OpenStack API for an existing service. I want to extend the API so that it will be compatible with my service.'

  • If the third-party's customization includes changes to how a REST call is made to the remote server (i.e. additional headers are required, the content body requires additional elements or the response codes differ from those in the OpenStack services spec):
In this case the third-party will need to create an alternate implementation of the services REST client. To do this, the third-party would derive a class from and implement the associated REST client and it's factory ( IStorageServiceRestClient and IStorageServiceRestClientFactory). Then, by using the service locator, the third-party would override/replace the registered implementation of those interfaces with their own concrete implementations.
Note: Depending on how extensive the customization is, the whole service client could, and should in some cases, be extended/replaced.
  • If the third-party's customization includes changes to the REST call's response (i.e. additional or modified headers and/or content body):
In this case the third-party will need to create an alternate implementation of the appropriate payload converter (i.e. IStorageObjectPayloadConverter). To do this, the third-party would derive a class from and implement the appropriate converter interface. Then, by using the service locator, the third-party would override/replace the registered implementation of the converter interfaces with their own concrete implementations. Additionally the third-party can derive a class from the StorageItem base class, and expose their own properties.
Note: Depending on how extensive the customization is, the Poco/Model client (i.e. IStorageServicePocoClient) may need to be extended/replaced as well.


Interacting with the service location infrastructure

To interact with the service location infrastructure the third-party needs to create a class that is derived from and implements the IServiceRegistrar interface. This interface acts as an entry point for the service location infrastructure and delegates the responsibility of further registration and initialization to the third-party. The interface contains only a single Register method, and provides references to the service locator and it's run time manager. These two references along with the OpenStackClientManager and OpenStackServiceClientManager classes give the third-party control over where and how to register their customization and extensions.

There are two ways for a third-party to be introduced in to the service location infrastructure, semi-automatic discovery and explicit registration. While semi-automatic discovery is strongly suggested either method can be used, and depending on the third-party's use case, one may have more advantages then the other.

Semi-automatic discovery:
At a very high level, the end users first point of contact with the SDK is the set of credentials that they are using to connect to the remote server. It sets a clear expectation for the end user as far as what system they are connecting to if they are required to construct a specific credential in order to acquire a reference to a client that will be used to perform actions on the remote services. For this reason, it is strongly suggested that a third-party that wants to extend the API also create their own implementation of the IOpenStackCredential interface, and expose it to the end user for consumption. Even if customized authentication is not required, simply inheriting from the interface, or the base OpenStackCredential class will provide a starting point for the end user to access a third-party's extensions.
Creating a third-party specific credential also provides a starting point for the API to discover and delegate to the third-party. When the Client Manager is asked to create a client that supports a given credential, it first ensures that the assembly that contains the given credential has been registered with the service location infrastructure. If the assembly that contains the third-party's credential type has inherited from and implemented the IServiceRegistrar interface, the service location infrastructure will discover the third party's registrar, and ensure that it is given a chance to interact with the service locator, before a client is given back out to the end user.
 //The OpenStackClientManager used by the OpenStackClientFactory will ensure that the assembly containing the ThirdPartyCredential type has been registered with the service location infrastructure.
 var credential = new ThirdPartyCredential(new Uri("authUri"),"username","password","tenentId");
 var client = OpenStackClientFactory.CreateClient(credential);
If the third-party does not want to create a custom credential object, creating a custom client or service client can also be used to introduce the third-party's assembly to the service location infrastructure. For example, the end user request a specific client from the OpenStackClientFactory. The client factory will insure that the assembly that contains the requested client type has been registered with the service location infrastructure, and if a registrar is present, the third-party will have a chance to register itself with the service location infrastructure.
 //The OpenStackClientManager used by the OpenStackClientFactory will ensure that the assembly that contains the IThirdPartyClient type has been registered with the service location infrastructure.
 var client = OpenStackClientFactory.CreateClient<IThirdPartyClient>(credential);
Note: The OpenStackServiceClientManager class also provides the same workflow for third-party developed service clients and is accessed via the client's CreateServiceClient method.
Explicit registration:
If the third-party is encapsulating the API or wishes to present an entirely custom surface area to the end user (i.e. the third party is creating a larger application,and wishes to extend the API as a sub-set or component of the larger application), then the third party can use explicit registration. The third-party can register itself with the service location infrastructure explicitly and ensure that it will be given an chance to initialize itself and register any desired components, clients, or service clients.


Putting it all together

In the code below, lets assume that a third-party wants to extend the API by adding a new service client. First the third-party would create a credential object to make it clear to the end user what remote service they are using.
 public class ThirdPartyCredential : IOpenStackCredential
 {
   //interface implementation... 
 }
Then the third-party would add a new service client definition.
 public class ThirdPartyServiceClientDefinition :IOpenStackServiceClientDefinition
 {
   //interface implementation...
 }
Next the third-party creates the actual service client.
 public interface IThirdPartyServiceClient
 {
   string Echo(string msg);
 }
 public class ThirdPartyServiceClient : IThirdPartyServiceClient, IOpenStackServiceClient
 {
   public string Echo(string msg)
   {
     return msg;
   }    
 }
Finally the third-party creates a service registrar, and wires up their service client with the ServiceClientManager.
 public class ThirdPartyServiceRegistrar : IServiceRegistrar
 {
   public void Register(IServiceLocationManager manager, IServiceLocator locator)
   {
     var serviceManager = locator.Locate<IOpenStackServiceClientManager>();
     serviceManager.RegisterServiceClient<IThirdPartyServiceClient>(new ThirdPartyServiceClientDefinition());
   }
 }
When the end user wants to consume the new service, the code looks something like this:
 private static void Main(string[] args)
 {
   var credential = new ThirdPartyCredential(new Uri("authUri"),"username","password","tenentId");
   var client = OpenStackClientFactory.CreateClient(credential);
   client.Connect();
   var serviceClient = client.CreateServiceClient<IThirdPartyServiceClient>();
   Console.WriteLine(serviceClient.Echo("Hello world!");
 }