Blogs  >  Using SMB to Share a Windows Azure Drive among multiple Role Instances

Using SMB to Share a Windows Azure Drive among multiple Role Instances

Using SMB to Share a Windows Azure Drive among multiple Role Instances


UPDATED 11/17/11 with a high availability sample

We often get questions from customers about how to share a drive with read-write access among multiple role instances. A common scenario is that of a content repository for multiple web servers to access and store content. An Azure drive is similar to a traditional disk drive in that it may only be mounted read-write on one system. However using SMB, it is possible to mount a drive on one role instance and then share that out to other role instances which can map the network share to a drive letter or mount point.

In this blog post we’ll cover the specifics on how to set this up and leave you with a simple prototype that demonstrates the concept. We’ll use an example of a worker role (referred to as the server) which mounts the drive and shares it out and two other worker roles (clients) that map the network share to a drive letter and write log records to the shared drive.

Service Definition on the Server role

The server role has TCP port 445 enabled as an internal endpoint so that it can receive SMB requests from other roles in the service. This done by defining the endpoint in the ServiceDefinition.csdef as follows

<Endpoints>
      <InternalEndpoint name="SMB" protocol="tcp" port="445" />
</Endpoints>

Now when the role starts up, it must mount the drive and then share it. Sharing the drive requires the Server role to be running with administrator privileges. Beginning with SDK 1.3 it’s possible to do that using the following setting in the ServiceDefinition.csdef file.

<Runtime executionContext="elevated"> 
</Runtime>

Mounting the drive and sharing it

When the server role instance starts up, it first mounts the Azure drive and executes shell commands to

  1. Create a user account for the clients to authenticate as. The user name and password are derived from the service configuration.
  2. Enable inbound SMB protocol traffic through the role instance firewall
  3. Share the mounted drive with the share name specified in the service configuration and grant the user account previously created full access. The value for path in the example below is the drive letter assigned to the drive.

Here’s snippet of C# code that does that.

String error;
ExecuteCommand("net.exe", "user " + userName + " " + password + " /add", out error, 10000);

ExecuteCommand("netsh.exe", "firewall set service type=fileandprint mode=enable scope=all", out error, 10000);

ExecuteCommand("net.exe", " share " + shareName + "=" + path + " /Grant:"
                    + userName + ",full", out error, 10000);

The shell commands are executed by the routine ExecuteCommand.

public static int ExecuteCommand(string exe, string arguments, out string error, int timeout)
      {
          Process p = new Process();
          int exitCode;
          p.StartInfo.FileName = exe;
          p.StartInfo.Arguments = arguments;
          p.StartInfo.CreateNoWindow = true;
          p.StartInfo.UseShellExecute = false;
          p.StartInfo.RedirectStandardError = true;
          p.Start();
          error = p.StandardError.ReadToEnd();
          p.WaitForExit(timeout);
          exitCode = p.ExitCode;
          p.Close();

          return exitCode;
      }

We haven’t touched on how to mount the drive because that is covered in several places including here.

Mapping the network drive on the client

When the clients start up, they locate the instance of the SMB Server and then identify the address of the SMB endpoint on the server. Next they execute a shell command to map the share served by the SMB server to a drive letter specified by the configuration setting localpath. Note that sharename, username and password must match the settings on the SMB server.

var server = RoleEnvironment.Roles["SMBServer"].Instances[0];
machineIP = server.InstanceEndpoints["SMB"].IPEndpoint.Address.ToString();machineIP = "\\\\" + machineIP + "\\";

string error;
ExecuteCommand("net.exe", " use " + localPath + " " + machineIP + shareName + " " + password + " /user:"+ userName, out error, 20000);

Once the share has been mapped to a local drive letter, the clients can write whatever they want to the share, just as they would to a local drive.  

Note: Since the clients may come up before the server is ready, the clients may have to retry or alternatively poll the server on some other port for status before attempting to map the drive. The prototype retries in a loop until it succeeds or times out.

Enabling High Availability

With a single server role instance, the file share will be unavailable when the role is being upgraded. If you need to mitigate that, you can create a few warm stand-by instances of the server role thus ensuring that there is always one server role instance available to share the Azure Drive to clients.

Another approach would be to make each of your roles a potential host for the SMB share. Each role instance could potentially run an SMB service, but only one of them would get the mounted Azure Drive behind SMB service. The roles can then iterate over all the role instances attempting to map the SMB share with each role instance. The mapping will succeed when the client connects to the instance that has the drive mounted.

Another scheme is to have the role instance that successfully mounted the drive inform the other role instances so that the clients can query to find the active server instance.

Sharing Local Drives within a role instance

It’s also possible to share a local resource drive mounted in a role instance among multiple role instances using similar steps. The key difference though is that writes to the local storage resource are not durable while writes to Azure Drives are persisted and available even after the role instances are shutdown.

Dinesh Haridas

 

Sample Code

Here’s the code for the Server and Client in its entirety for easy reference.

Server – WorkerRole.cs

This file contains the code for the SMB server worker role. In the OnStart() method, the role instance initializes tracing before mounting the Azure Drive. It gets the settings for storage credentials, drive name and drive size from the Service Configuration. Once the drive is mounted, the role instance creates a user account, enables SMB traffic through the firewall and then shares the drive. These operations are performed by executing shell commands using the ExecuteCommand() method described earlier. For simplicity, parameters like account name, password and the share name for the drive are derived from the Service Configuration.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Threading;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Diagnostics;
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.StorageClient;

namespace SMBServer
{
    public class WorkerRole : RoleEntryPoint
    {
        public static string driveLetter = null;
        public static CloudDrive drive = null;

        public override void Run()
        {
            Trace.WriteLine("SMBServer entry point called", "Information");

            while (true)
            {
                Thread.Sleep(10000);
            }
        }

        public override bool OnStart()
        {
            // Set the maximum number of concurrent connections 
            ServicePointManager.DefaultConnectionLimit = 12;

            // Initialize logging and tracing
            DiagnosticMonitorConfiguration dmc = DiagnosticMonitor.GetDefaultInitialConfiguration();
            dmc.Logs.ScheduledTransferLogLevelFilter = LogLevel.Verbose;
            dmc.Logs.ScheduledTransferPeriod = TimeSpan.FromMinutes(1);
            DiagnosticMonitor.Start("Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString", dmc);
            Trace.WriteLine("Diagnostics Setup complete", "Information");

           
            CloudStorageAccount account = CloudStorageAccount.Parse(RoleEnvironment.GetConfigurationSettingValue("StorageConnectionString"));
            try
            {
                CloudBlobClient blobClient = account.CreateCloudBlobClient();
                CloudBlobContainer driveContainer = blobClient.GetContainerReference("drivecontainer");
                driveContainer.CreateIfNotExist();

                String driveName = RoleEnvironment.GetConfigurationSettingValue("driveName");
                LocalResource localCache = RoleEnvironment.GetLocalResource("AzureDriveCache");
                CloudDrive.InitializeCache(localCache.RootPath, localCache.MaximumSizeInMegabytes);

                drive = new CloudDrive(driveContainer.GetBlobReference(driveName).Uri, account.Credentials);
                try
                {
                    drive.Create(int.Parse(RoleEnvironment.GetConfigurationSettingValue("driveSize")));
                }
                catch (CloudDriveException ex)
                {
                    Trace.WriteLine(ex.ToString(), "Warning");
                }

                driveLetter = drive.Mount(localCache.MaximumSizeInMegabytes, DriveMountOptions.None);

                string userName = RoleEnvironment.GetConfigurationSettingValue("fileshareUserName");
                string password = RoleEnvironment.GetConfigurationSettingValue("fileshareUserPassword");

                // Modify path to share a specific directory on the drive
                string path = driveLetter;
                string shareName = RoleEnvironment.GetConfigurationSettingValue("shareName");
                int exitCode;
                string error;

                //Create the user account    
                exitCode = ExecuteCommand("net.exe", "user " + userName + " " + password + " /add", out error, 10000);
                if (exitCode != 0)
                {
                    //Log error and continue since the user account may already exist
                    Trace.WriteLine("Error creating user account, error msg:" + error, "Warning");
                }

                //Enable SMB traffic through the firewall
                exitCode = ExecuteCommand("netsh.exe", "firewall set service type=fileandprint mode=enable scope=all", out error, 10000);
                if (exitCode != 0)
                {
                    Trace.WriteLine("Error setting up firewall, error msg:" + error, "Error");
                    goto Exit;
                }

                //Share the drive
                exitCode = ExecuteCommand("net.exe", " share " + shareName + "=" + path + " /Grant:"
                    + userName + ",full", out error, 10000);

               if (exitCode != 0)
                {
                    //Log error and continue since the drive may already be shared
                    Trace.WriteLine("Error creating fileshare, error msg:" + error, "Warning");
                }

                Trace.WriteLine("Exiting SMB Server OnStart", "Information");
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.ToString(), "Error");
                Trace.WriteLine("Exiting", "Information");
                throw;
            }
            
            Exit:
            return base.OnStart();
        }

        public static int ExecuteCommand(string exe, string arguments, out string error, int timeout)
        {
            Process p = new Process();
            int exitCode;
            p.StartInfo.FileName = exe;
            p.StartInfo.Arguments = arguments;
            p.StartInfo.CreateNoWindow = true;
            p.StartInfo.UseShellExecute = false;
            p.StartInfo.RedirectStandardError = true;
            p.Start();
            error = p.StandardError.ReadToEnd();
            p.WaitForExit(timeout);
            exitCode = p.ExitCode;
            p.Close();

            return exitCode;
        }

        public override void OnStop()
        {
            if (drive != null)
            {
                drive.Unmount();
            }
            base.OnStop();
        }
    }
}

 

Client – WorkerRole.cs

This file contains the code for the SMB client worker role. The OnStart method initializes tracing for the role instance. In the Run() method, each client maps the drive shared by the server role using the MapNetworkDrive() method before writing log records at ten second intervals to the share in a loop.

In the MapNetworkDrive() method the client first determines the IP address and port number for the SMB endpoint on the server role instance before executing the shell command net use to connect to it. As in the case of the server role, the routine ExecuteCommand() is used to execute shell commands. Since the server may start up after the client, the client retries in a loop sleeping 10 seconds between retries and gives up after about 17 minutes. Between retries the client also deletes any stale mounts of the same share.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Threading;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Diagnostics;
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.StorageClient;

namespace SMBClient
{
    public class WorkerRole : RoleEntryPoint
    {
        public const int tenSecondsAsMS = 10000;
        public override void Run()
        {
            // The code here mounts the drive shared out by the server worker role
            // Each client role instance writes to a log file named after the role instance in the logfile directory

            Trace.WriteLine("SMBClient entry point called", "Information");
            string localPath = RoleEnvironment.GetConfigurationSettingValue("localPath");
            string shareName = RoleEnvironment.GetConfigurationSettingValue("shareName");
            string userName = RoleEnvironment.GetConfigurationSettingValue("fileshareUserName");
            string password = RoleEnvironment.GetConfigurationSettingValue("fileshareUserPassword");

            string logDir = localPath + "\\" + "logs";
            string fileName = RoleEnvironment.CurrentRoleInstance.Id + ".txt";
            string logFilePath = System.IO.Path.Combine(logDir, fileName);

            try
            {

                if (MapNetworkDrive(localPath, shareName, userName, password) == true)
                {
                    System.IO.Directory.CreateDirectory(logDir);

                    // do work on the mounted drive here
                    while (true)
                    {
                        // write to the log file
                        System.IO.File.AppendAllText(logFilePath, DateTime.Now.TimeOfDay.ToString() + Environment.NewLine);
                        Thread.Sleep(tenSecondsAsMS);
                    }

                }
                Trace.WriteLine("Failed to mount" + shareName, "Error");
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.ToString(), "Error");
                throw;
            }

        }

        public static bool MapNetworkDrive(string localPath, string shareName, string userName, string password)
        {
            int exitCode = 1;

            string machineIP = null;
            while (exitCode != 0)
            {
                int i = 0;
                string error;

                var server = RoleEnvironment.Roles["SMBServer"].Instances[0];
                machineIP = server.InstanceEndpoints["SMB"].IPEndpoint.Address.ToString();
                machineIP = "\\\\" + machineIP + "\\";
                exitCode = ExecuteCommand("net.exe", " use " + localPath + " " + machineIP + shareName + " " + password + " /user:"
                    + userName, out error, 20000);

                if (exitCode != 0)
                {
                    Trace.WriteLine("Error mapping network drive, retrying in 10 seoconds error msg:" + error, "Information");
                    // clean up stale mounts and retry 
                    ExecuteCommand("net.exe", " use " + localPath + "  /delete", out error, 20000);
                    Thread.Sleep(10000);
                    i++;
                    if (i > 100) break;
                }
            }

            if (exitCode == 0)
            {
                Trace.WriteLine("Success: mapped network drive" + machineIP + shareName, "Information");
                return true;
            }
            else
                return false;
        }

        public static int ExecuteCommand(string exe, string arguments, out string error, int timeout)
        {
            Process p = new Process();
            int exitCode;
            p.StartInfo.FileName = exe;
            p.StartInfo.Arguments = arguments;
            p.StartInfo.CreateNoWindow = true;
            p.StartInfo.UseShellExecute = false;
            p.StartInfo.RedirectStandardError = true;
            p.Start();
            error = p.StandardError.ReadToEnd();
            p.WaitForExit(timeout);
            exitCode = p.ExitCode;
            p.Close();

            return exitCode;
        }

        public override bool OnStart()
        {
            // Set the maximum number of concurrent connections 
            ServicePointManager.DefaultConnectionLimit = 12;

            DiagnosticMonitorConfiguration dmc = DiagnosticMonitor.GetDefaultInitialConfiguration();
            dmc.Logs.ScheduledTransferLogLevelFilter = LogLevel.Verbose;
            dmc.Logs.ScheduledTransferPeriod = TimeSpan.FromMinutes(1);
            DiagnosticMonitor.Start("Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString", dmc);
            Trace.WriteLine("Diagnostics Setup comlete", "Information");

            return base.OnStart();
        }
    } 
}
 

ServiceDefinition.csdef

<?xml version="1.0" encoding="utf-8"?>
<ServiceDefinition name="AzureDemo" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition">
  <WorkerRole name="SMBServer">
    <Runtime executionContext="elevated">
    </Runtime>
    <Imports>
      <Import moduleName="Diagnostics" />
    </Imports>
    <ConfigurationSettings>
      <Setting name="StorageConnectionString" />
      <Setting name="driveName" />
      <Setting name="driveSize" />
      <Setting name="fileshareUserName" />
      <Setting name="fileshareUserPassword" />
      <Setting name="shareName" />
    </ConfigurationSettings>
    <LocalResources>
      <LocalStorage name="AzureDriveCache" cleanOnRoleRecycle="true" sizeInMB="300" />
    </LocalResources>
    <Endpoints>
      <InternalEndpoint name="SMB" protocol="tcp" port="445" />
    </Endpoints>
  </WorkerRole>
  <WorkerRole name="SMBClient">
    <Imports>
      <Import moduleName="Diagnostics" />
    </Imports>
    <ConfigurationSettings>
      <Setting name="fileshareUserName" />
      <Setting name="fileshareUserPassword" />
      <Setting name="shareName" />
      <Setting name="localPath" />
    </ConfigurationSettings>
  </WorkerRole>
</ServiceDefinition>

ServiceConfiguration.cscfg

<?xml version="1.0" encoding="utf-8"?>
<ServiceConfiguration serviceName="AzureDemo" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration" osFamily="1" osVersion="*">
  <Role name="SMBServer">
    <Instances count="1" />
    <ConfigurationSettings>
      <Setting name="StorageConnectionString" value="DefaultEndpointsProtocol=http;AccountName=yourstorageaccount;AccountKey=yourkey" />
      <Setting name="Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString" value="DefaultEndpointsProtocol=https;AccountName=yourstorageaccount;AccountKey=yourkey" />
      <Setting name="driveName" value="drive2" />
      <Setting name="driveSize" value="1000" />
      <Setting name="fileshareUserName" value="fileshareuser" />
      <Setting name="fileshareUserPassword" value="SecurePassw0rd" />
      <Setting name="shareName" value="sharerw" />
    </ConfigurationSettings>
  </Role>
  <Role name="SMBClient">
    <Instances count="2" />
    <ConfigurationSettings>
      <Setting name="Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString" value="DefaultEndpointsProtocol=https;AccountName=yourstorageaccount;AccountKey=yourkey" />
      <Setting name="fileshareUserName" value="fileshareuser" />
      <Setting name="fileshareUserPassword" value="SecurePassw0rd" />
      <Setting name="shareName" value="sharerw" />
      <Setting name="localPath" value="K:" />
    </ConfigurationSettings>
  </Role>
</ServiceConfiguration>

 

The below was added on 11/17/11

High Availability Sample

We can modify this sample to increase the availability of the share by having multiple “Server” worker roles. First, we will modify the instance count on the server role to be 2:

<Role name="SMBServer">
  <Instances count="1" />

 

High Availability Server

Next, we need to modify the server to compete to mount the drive. The server that successfully mounts the drive will share it out as before. The other server will sit in a loop attempting to mount the drive. To support remote breaking of the lease, we will also have the server that owns the lease check whether it still has the lease. To do this, I’ve added a new function “CompeteForMount” to the server.

private void CompeteForMount()
{
    for (; ; )
    {
        try
        {
            driveLetter = drive.Mount(localCache.MaximumSizeInMegabytes, DriveMountOptions.None);
        }
        catch (CloudDriveException)
        {
            // Wait 10 seconds, and try again
            Thread.Sleep(10000);
            continue;
        }

Then we will move all the firewall and sharing code from OnStart() into this new method. After that, we will go into a loop making sure the drive is still mounted.

 

for (; ; )
{
    try
    {
        drive.Mount(localCache.MaximumSizeInMegabytes, DriveMountOptions.None);
        Thread.Sleep(10000);
    }
    catch (Exception)
    {
        // Go back and remount it
        break;
    }
}

Finally, if the drive does get un-mounted, we should also remove the drive share.

 

exitCode = ExecuteCommand("net.exe", " share /d " + path, out error, 10000);

if (exitCode != 0)
{
    //Log error and continue
    Trace.WriteLine("Error creating fileshare, error msg:" + error, "Warning");
}

This method will be called from Run() rather than going into a Sleep loop.

High Availability Client

On the client, we need to iterate through the servers to find the one that has shared out the drive and mount that. We can do that inside the MapNetworkDrive function:

bool found = false;

int countServers = RoleEnvironment.Roles["SMBServer"].Instances.Count;
for (int instance = 0; instance < countServers; instance++)
{
    var server = RoleEnvironment.Roles["SMBServer"].Instances[instance];

Finally, the call to AppendAllText() in the client code will throw an exception when the drive becomes unavailable. That exception will cause the client to exit, and the client will be restarted and attach to the new server. We could further refine the client code by catching this exception and going back to the reconnect loop.

High Availability Server – WorkerRole.cs

The following is a full listing of the high availability server worker role.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Threading;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Diagnostics;
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.StorageClient;

namespace SMBServer
{
    public class WorkerRole : RoleEntryPoint
    {
        public static string driveLetter = null;
        public static CloudDrive drive = null;
        public static LocalResource localCache = null;

        public override void Run()
        {
            Trace.WriteLine("SMBServer entry point called", "Information");

            CompeteForMount();
        }

        public override bool OnStart()
        {
            // Set the maximum number of concurrent connections 
            ServicePointManager.DefaultConnectionLimit = 12;

            // Initialize logging and tracing
            DiagnosticMonitorConfiguration dmc = DiagnosticMonitor.GetDefaultInitialConfiguration();
            dmc.Logs.ScheduledTransferLogLevelFilter = LogLevel.Verbose;
            dmc.Logs.ScheduledTransferPeriod = TimeSpan.FromMinutes(1);
            DiagnosticMonitor.Start("Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString", dmc);
            Trace.WriteLine("Diagnostics Setup complete", "Information");

            SetupDriveObject();

            Trace.WriteLine("Exiting SMB Server OnStart", "Information");

            return base.OnStart();
        }

        private void SetupDriveObject()
        {
            CloudStorageAccount account = CloudStorageAccount.Parse(RoleEnvironment.GetConfigurationSettingValue("StorageConnectionString"));

            try
            {
                CloudBlobClient blobClient = account.CreateCloudBlobClient();
                CloudBlobContainer driveContainer = blobClient.GetContainerReference("drivecontainer");
                driveContainer.CreateIfNotExist();

                String driveName = RoleEnvironment.GetConfigurationSettingValue("driveName");
                localCache = RoleEnvironment.GetLocalResource("AzureDriveCache");
                CloudDrive.InitializeCache(localCache.RootPath, localCache.MaximumSizeInMegabytes);

                drive = new CloudDrive(driveContainer.GetBlobReference(driveName).Uri, account.Credentials);
                try
                {
                    drive.Create(int.Parse(RoleEnvironment.GetConfigurationSettingValue("driveSize")));
                }
                catch (CloudDriveException ex)
                {
                    Trace.WriteLine(ex.ToString(), "Warning");
                }
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.ToString(), "Error");
                Trace.WriteLine("Exiting", "Information");
                throw;
            }
        }

        private void CompeteForMount()
        {
            for (; ; )
            {
                try
                {
                    driveLetter = drive.Mount(localCache.MaximumSizeInMegabytes, DriveMountOptions.None);
                }
                catch (CloudDriveException)
                {
                    // Wait 10 seconds, and try again
                    Thread.Sleep(10000);
                    continue;
                }

                string userName = RoleEnvironment.GetConfigurationSettingValue("fileshareUserName");
                string password = RoleEnvironment.GetConfigurationSettingValue("fileshareUserPassword");

                // Modify path to share a specific directory on the drive
                string path = driveLetter;
                string shareName = RoleEnvironment.GetConfigurationSettingValue("shareName");
                int exitCode;
                string error;

                //Create the user account    
                exitCode = ExecuteCommand("net.exe", "user " + userName + " " + password + " /add", out error, 10000);
                if (exitCode != 0)
                {
                    //Log error and continue since the user account may already exist
                    Trace.WriteLine("Error creating user account, error msg:" + error, "Warning");
                }

                //Enable SMB traffic through the firewall
                exitCode = ExecuteCommand("netsh.exe", "firewall set service type=fileandprint mode=enable scope=all", out error, 10000);
                if (exitCode != 0)
                {
                    throw new Exception("Error setting up firewall, error msg:" + error);
                }

                //Share the drive
                exitCode = ExecuteCommand("net.exe", " share " + shareName + "=" + path + " /Grant:"
                    + userName + ",full", out error, 10000);

                if (exitCode != 0)
                {
                    //Log error and continue since the drive may already be shared
                    Trace.WriteLine("Error creating fileshare, error msg:" + error, "Warning");
                }

                //Now, spin checking if the drive is still accessible.
                for (; ; )
                {
                    try
                    {
                        drive.Mount(localCache.MaximumSizeInMegabytes, DriveMountOptions.None);
                        Thread.Sleep(10000);
                    }
                    catch (Exception)
                    {
                        // Go back and remount it
                        break;
                    }
                }

                //Drive is not accessible.  Remove the share
                exitCode = ExecuteCommand("net.exe", " share /d " + path, out error, 10000);

                if (exitCode != 0)
                {
                    //Log error and continue
                    Trace.WriteLine("Error creating fileshare, error msg:" + error, "Warning");
                }
            }
        }

        public static int ExecuteCommand(string exe, string arguments, out string error, int timeout)
        {
            Process p = new Process();
            int exitCode;
            p.StartInfo.FileName = exe;
            p.StartInfo.Arguments = arguments;
            p.StartInfo.CreateNoWindow = true;
            p.StartInfo.UseShellExecute = false;
            p.StartInfo.RedirectStandardError = true;
            p.Start();
            error = p.StandardError.ReadToEnd();
            p.WaitForExit(timeout);
            exitCode = p.ExitCode;
            p.Close();

            return exitCode;
        }

        public override void OnStop()
        {
            if (drive != null)
            {
                drive.Unmount();
            }
            base.OnStop();
        }
    }
}

High Availability Client – WorkerRole.cs

The following is a listing of the high availability client.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Threading;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Diagnostics;
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.StorageClient;

namespace SMBClient
{
    public class WorkerRole : RoleEntryPoint
    {
        public const int tenSecondsAsMS = 10000;
        public override void Run()
        {
            // The code here mounts the drive shared out by the server worker role
            // Each client role instance writes to a log file named after the role instance in the logfile directory

            Trace.WriteLine("SMBClient entry point called", "Information");
            string localPath = RoleEnvironment.GetConfigurationSettingValue("localPath");
            string shareName = RoleEnvironment.GetConfigurationSettingValue("shareName");
            string userName = RoleEnvironment.GetConfigurationSettingValue("fileshareUserName");
            string password = RoleEnvironment.GetConfigurationSettingValue("fileshareUserPassword");

            string logDir = localPath + "\\" + "logs";
            string fileName = RoleEnvironment.CurrentRoleInstance.Id + ".txt";
            string logFilePath = System.IO.Path.Combine(logDir, fileName);

            try
            {

                if (MapNetworkDrive(localPath, shareName, userName, password) == true)
                {
                    System.IO.Directory.CreateDirectory(logDir);

                    // do work on the mounted drive here
                    while (true)
                    {
                        // write to the log file
                        System.IO.File.AppendAllText(logFilePath, DateTime.Now.TimeOfDay.ToString() + Environment.NewLine);
                        
                        // If the file/share becomes inaccessible, AppendAllText will throw an exception and
                        // the worker role will exit, and then get restarted, and then it fill find the new share
                        
                        Thread.Sleep(tenSecondsAsMS);
                    }

                }
                Trace.WriteLine("Failed to mount" + shareName, "Error");
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.ToString(), "Error");
                throw;
            }

        }

        public static bool MapNetworkDrive(string localPath, string shareName, string userName, string password)
        {
            int exitCode = 1;

            string machineIP = null;
            while (exitCode != 0)
            {
                int i = 0;
                string error;
                bool found = false;

                int countServers = RoleEnvironment.Roles["SMBServer"].Instances.Count;
                for (int instance = 0; instance < countServers; instance++)
                {
                    var server = RoleEnvironment.Roles["SMBServer"].Instances[instance];
                    machineIP = server.InstanceEndpoints["SMB"].IPEndpoint.Address.ToString();
                    machineIP = "\\\\" + machineIP + "\\";
                    exitCode = ExecuteCommand("net.exe", " use " + localPath + " " + machineIP + shareName + " " + password + " /user:"
                        + userName, out error, 20000);

                    if (exitCode != 0)
                    {
                        Trace.WriteLine("Error mapping network drive, retrying in 10 seoconds error msg:" + error, "Information");
                        // clean up stale mounts and retry 
                        ExecuteCommand("net.exe", " use " + localPath + "  /delete", out error, 20000);
                    }
                    else
                    {
                        found = true;
                        break;
                    }
                }

                if (!found)
                {
                    Thread.Sleep(10000);

                    i++;
                    if (i > 100)
                    {
                        break;
                    }
                }
            }

            if (exitCode == 0)
            {
                Trace.WriteLine("Success: mapped network drive" + machineIP + shareName, "Information");
                return true;
            }
            else
                return false;
        }

        public static int ExecuteCommand(string exe, string arguments, out string error, int timeout)
        {
            Process p = new Process();
            int exitCode;
            p.StartInfo.FileName = exe;
            p.StartInfo.Arguments = arguments;
            p.StartInfo.CreateNoWindow = true;
            p.StartInfo.UseShellExecute = false;
            p.StartInfo.RedirectStandardError = true;
            p.Start();
            error = p.StandardError.ReadToEnd();
            p.WaitForExit(timeout);
            exitCode = p.ExitCode;
            p.Close();

            return exitCode;
        }

        public override bool OnStart()
        {
            // Set the maximum number of concurrent connections 
            ServicePointManager.DefaultConnectionLimit = 12;

            DiagnosticMonitorConfiguration dmc = DiagnosticMonitor.GetDefaultInitialConfiguration();
            dmc.Logs.ScheduledTransferLogLevelFilter = LogLevel.Verbose;
            dmc.Logs.ScheduledTransferPeriod = TimeSpan.FromMinutes(1);
            DiagnosticMonitor.Start("Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString", dmc);
            Trace.WriteLine("Diagnostics Setup comlete", "Information");

            return base.OnStart();
        }
    }
}

Comments (22)

  1. Peter kellner says:

    What about getting to the drive from a windows client?

  2. Dinesh Haridas says:

    Currently we don't support accessing Azure Drives from outside the cloud.

  3. David Rodriguez says:

    Hi Dinesh,

    I understand that this code can't be run on development environment, since sharing a folder (or a the drive) on an X-Drive in development is not supported, raising an error "Device or folder does not exist" (see NET HELPMSG 2116) when trying to share. Is this true or I'm losing something? When I try to share the same folder through a windows explorer, I have the same error.

  4. Dinesh Haridas says:

    David, thats correct. This sample won't work in the development environment because the drive created in the storage emulator cannot be shared. You will run into the same issue with Windows Explorer too.

  5. David Rodriguez says:

    Hum…ok, then I'll introduce some conditional compiling like #ifdebug or something similar to do a hard-coded path sharing on development environment to workaround the issue.

    Thanks for your confirmation 🙂

  6. David Rodriguez says:

    Hi again Dinesh,

    Take a look of the job based on the idea that you commented here. 🙂

    http://bit.ly/DNNAzureSMB

  7. yazeem says:

    Hi,

    Is it necessary to call MapNetworkDrive(..) every time Web Role wants to connect to SMBServer? If we create a static variable in our WebRole and after calling MapNetworkDrive(..) shared path is stored in that static variable, then our WebRole can use that path in static variable to perform drive operations.

    I want to know is this a right approach as i have tested it and its working fine. For how long the connection persists if we use it from static variable ?

    Also if we use the path stored in static variable whether SMBServer will be involved in further communications with xdrive ?

  8. Dinesh Haridas says:

    MapNetworkDrive needs to be called only when the share is first being mapped to a drive letter on the client. Once thats done the client can continue to access the share without calling the MapNetworkDrive routine.

  9. David Rodriguez says:

    Hum…I've check that the link of the DNN Azure project is broken. The final solution is on Codeplex here for more info dnnazureaccelerator.codeplex.com

  10. If the SMBServer single instance fails, then all the other instances loose their access to the drive and this will last until Windows Azure restarts a new SMBServer instance which may take ~8 to 15 minutes. That's a long downtime for a DNN web site for instance if this happens once or twice a month…

  11. Andrew Edwards says:

    Hi Benjamin,

    Yes, with a single SMB server instance the downtime could be significant.  As mentioned in the original post, you can improve on the implementation by having multiple server instances running.  I added a 'High Availability" sample to demonstrate how you can get a faster recovery.

    Thanks

    Andrew Edwards

    Windows Azure Storage

  12. Aaron Hayon says:

    There seems to be one issue with the above implementation.  The user you set up will have a password that will expire based on the default group policy.  Is there a way create the user where the password never expires?  I know that you can use the /expire:never parameter for net.exe but that does not set the password to never expire.

    Any Ideas?

  13. Andrew Edwards says:

    Hi Aaron,

    The system policy is that passwords expire in about a month.  You can change that (see 'net accounts /?') for a VMRole, but for Web and Worker roles, the policy will get changed back.  Another option is to update the password with 'net user <username> <password>' on a regular basis.  If you do go this route, I would recommend creating a way that you can change the password from time to time for defense-in-depth security.

    Thanks

    Andrew Edwards

    Windows Azure Storage

  14. Cristhian Amaya says:

    Hi,

    I have a Web Role connected to SMBServer, when I log in via RDP all works fine, but the web application not have access to the disk, even sharing drive to Everyone with all permissions.

    Any ideas?

  15. Andrew Edwards says:

    Hi Cristhian,

    "Net use" connects to the server in the user context in which you call it.  Since the webrole runs with limited access, you have to put the "net use" call into that context. I would recommend trying it from Application_Start.

    Thanks

    Andrew Edwards

    Windows Azure Storage

  16. AzureDeveloper says:

    Hi,

    My webrole is configured as the SMB server. I even ran the net use command from Application_Start. I am getting this error "System error 1332 has occurred.No mapping between account names and security IDs was done". Can you help me out?

  17. Hi,

    I just tried to implement this approach.

    The CLIENT role started working only with executionContext="elevated" like on SERVER (otherwise SecurityException was thrown).

    But now I have stuck with some unexpected behaviour:

    1) SERVER mounts a drive to "b:". Trying to check if directory "b:\logs" ehsists (in a loop each 60 secs) returns FALSE 🙁 .  Is it OK? How can view the content of the DRIVE?

    2) I deployed project with 3 instances of ClientRole. Each mounts a share to M:, creates logs catalog and creates/updates its TXT file. This part is working with some strange fact that I see file really in E:logs catalog. And i see only one file per instance. Tried to write code to look for files which should be created from another instances – File.Exists(???) returns FALSE.

    3) I downloaded the DRIVE with "Azure storage explore", mounted to local disk on Win7 and … see it is EMPTY.

    All these facts are not working the way anybody would expect.

    AM I DOING SOMTHING WRONG?

  18. Andrew Edwards says:

    Hi Aidas,

    I would recommend logging into one of your client instances and running "net use" on the command line, which will list the shares your have mounted.  This might help clarify what is happening with your implementation.

    Thanks

    Andrew Edwards

    Windows Azure Storage

  19. James says:

    We are having the same issue as Aidas Stalmokas in that our two instances (one the server and one the client) get different views of the mounted drive. File.Exists returns false for a particular file on the client but true on the server. I suspect there may be some interaction between the azure cloud drive cacheing and the SMB protocol (possible oplocks related)? I have googled around the SMB protocol and there are many reported issues with multiple clients reading/writing to the same file shared over SMB. I am considering resorting to NFS or another sharing protocol. I would love to know if MS are able to reproduce this problem and if they have any suggestions as to the underlying issues…a potential workaround or resolution would be nice!

    Kind Regards,

    James.

  20. Andrew Edwards says:

    Hi James,

    Can you give me more specifics of your scenario:

      Are these worker roles or web roles?

      Which is the SMB server with the drive mounted, and which is the client?

      Which flavor of the OS are you running?

      Which SDK are you using?

      Have you connected to the machines and checked the files and shares using 'dir' and 'net use'?

      Which data center are you deployed to?

    I believe there may be a small delay (seconds) between when you create a file from one client and when another client will detect it with File.Exists.  This is the normal behavior of SMB, as the clients cache the directory contents for a short period to reduce round trips to the server and improve performance.  However, by the time you connect to the machine later, the files should be visible.  Another option is to disable this cache.  For that, see: technet.microsoft.com/…/ff686200(v=WS.10).aspx

    Thanks

    Andrew Edwards

    Windows Azure Storage

  21. David Hernandez says:

    How about using Windows DFS for high availability? I guess this is a more standard way to implement a fault tolerant network share, and more robust tan a warm stand-by servers….

  22. Jay says:

    I have an MVC application that I deploy to azure as a cloud service as a web role as follows:

    public class WebRole : RoleEntryPoint

    {

       public override bool OnStart()

       {

           // For information on handling configuration changes

           // see the MSDN topic at go.microsoft.com/fwlink.

           try

           {

               // Retrieve an object that points to the local storage resource.

               LocalResource localResource = RoleEnvironment.GetLocalResource("TempZipDirectory"); // Todo name from config instread of hardcoding it

               string path = localResource.RootPath;

               CreateVirtualDirectory("SCORM", localResource.RootPath);

           }

           catch (RoleEnvironmentException e)

           {

               // Debug.WriteLine("Exception caught in RoleEntryPoint:OnStart: " & ex.Message, "Critical")

           }

           return base.OnStart();

       }

       public static void CreateVirtualDirectory(string VDirName, string physicalPath)

       {

           try

           {

               if (VDirName[0] != '/')

                   VDirName = "/" + VDirName;

               using (var serverManager = new ServerManager())

               {

                   string siteName = RoleEnvironment.CurrentRoleInstance.Id + "_" + "Web";

                   //Site theSite = serverManager.Sites[siteName];

                   Site theSite = serverManager.Sites[0];

                   foreach (var app in theSite.Applications)

                   {

                       if (app.Path == VDirName)

                       {

                           // already exists

                           return;

                       }

                   }

                   Microsoft.Web.Administration.VirtualDirectory vDir = theSite.Applications[0].VirtualDirectories.Add(VDirName, physicalPath);

                   serverManager.CommitChanges();

               }

           }

           catch (Exception ex)

           {

               System.Diagnostics.EventLog.WriteEntry("Application", ex.Message, System.Diagnostics.EventLogEntryType.Error);

               //System.Diagnostics.EventLog.WriteEntry("Application", ex.InnerException.Message, System.Diagnostics.EventLogEntryType.Error);

           }

       }

    How can I combine this approach so that I can set the SMB share as the virtual directory onstart of each of my instances. Currently they are configured to point to a folder on the local role environment, this was fine in single instance but on high availability I am getting issues with files not being found when users jump across instances from the instance to which the files where initially uploaded