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="https://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="https://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

  • Anonymous
    April 25, 2011
    What about getting to the drive from a windows client?

  • Anonymous
    April 25, 2011
    Currently we don't support accessing Azure Drives from outside the cloud.

  • Anonymous
    April 26, 2011
    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.

  • Anonymous
    April 26, 2011
    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.

  • Anonymous
    April 27, 2011
    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 :)

  • Anonymous
    May 02, 2011
    Hi again Dinesh, Take a look of the job based on the idea that you commented here. :) http://bit.ly/DNNAzureSMB

  • Anonymous
    June 30, 2011
    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 ?

  • Anonymous
    July 02, 2011
    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.

  • Anonymous
    July 21, 2011
    The comment has been removed

  • Anonymous
    November 16, 2011
    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...

  • Anonymous
    November 17, 2011
    The comment has been removed

  • Anonymous
    February 22, 2012
    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?

  • Anonymous
    March 19, 2012
    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

  • Anonymous
    March 29, 2012
    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?

  • Anonymous
    March 30, 2012
    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

  • Anonymous
    October 11, 2012
    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?

  • Anonymous
    April 22, 2013
    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?
  • Anonymous
    April 26, 2013
    The comment has been removed

  • Anonymous
    May 01, 2013
    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.

  • Anonymous
    May 02, 2013
    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

  • Anonymous
    November 18, 2013
    The comment has been removed

  • Anonymous
    July 23, 2014
    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