Introduction
With the limited support available on how to use ISCSI (Small Computer System Interface over TCP/IP) features in our application when we require to add any network storage to our machine as a separate device or drive, I thought of writing an article which will make life easy for people who are trying to achieve this. As we already know, there is limited documentation provided by MSDN for the API support for ISCSI. Also, the documentation in itself is so complex that it's very hard for someone new to ISCSI to understand it. So, here is a sample code which will enable you to get familiar with ISCSI on Windows and perform almost all the tasks which can be performed through ISCSI initiator programmatically. The language I have used for development is C++. Here is a brief about the functionality to be achieved.
Understanding the Concept of ISCSI on Windows
As discussed, ISCSI stands for Small Computer System Interface over TCP/IP. The major help we get from ISCSI is that we can have the required space available to us on demand provided we have enough space available in the form of SAN/NAS devices on our network. For ISCSI to work on our machine, we need to have Microsoft ISCSI initiator to be installed on our machines. The good news is that Microsoft has started shipping the initiator from versions after VISTA but for running this code on XP, we need to download the initiator from here depending on the architecture of the system we are using.
After this, we need to have a machine on the network which has ISCSI target installed on it and that also has shared ISCSI device configured on it which can be attached to this machine. One of the tools available for this purpose is provided by starwind and you can always use its free version for your purpose. You can get the free version available here. Now after configuring the target on the network machine and installing Microsoft ISCSI initiator on your base machine, you can test the connection by adding a target portal through ISCSI initiator by entering the IP address of the Network machine on which the ISCSI target has been configured. As soon as the target portal is added, the list of targets available is also shown on the 'Targets' tab. You can select any one of them and log on to the 'Target'. If this device or target is having a file system format supported by Windows, this target is automatically attached to the base machine as a separate logical disc, or else if the format of the target is unknown then the connection is established but the logical disc is not detected. You can however go to the 'Disk Management' section under 'Computer Management ' to see the unformatted space associated with the target available to the base machine.
I think that this explanation will remove your initial bumps while trying to understand ISCSI on Windows platform. But the real question that arises is 'How to do all this programmatically?' This is explained in the next section.
Implementation
As all the functionality which can be performed with ISCSI initiator is available with 'iscsidsc.dll', we will first have to load the DLL in order to make use of the functionality given to us by it. This can be done simply as shown below. The ideal place to load this would be the constructor of the class which is loading the library. For instance I have a class CIscsi
whose constructor and destructor would look something like this:
public:
HMODULE m_hMod;
CIscsi()
{
m_hMod= NULL;
HRESULT hr = S_OK;
m_hMod = LoadLibrary(_T("iscsidsc.dll"));
if ( !m_hMod )
{
::MessageBox(NULL,_T("iscsidsc.dll failed to load "),_T("Iscsi"),
MB_OK|MB_ICONINFORMATION);
}
}
~CIscsi()
{
if (m_hMod)
{
FreeLibrary (m_hMod);
m_hMod = NULL;
}
}
Step By Step Explanation
Now, with the DLL loaded, you will have to make sure that you use the functions available with 'iscsidsc.dll' in a proper manner. Perform these steps:
- In stdafx.h of your project, add these two lines:
#include "iscsierr.h" // This will include all the error codes for ISCSI
#include "iscsidsc.h" // This will bring all function definitions available
- In in the header file of the class
CIscsi
, add the following statements:
public:
STDMETHOD(GetErrorString)( BSTR *mresult);
STDMETHOD(InitializeISCSI)( BSTR bstDllPath );
STDMETHOD(AddIscsiTargetPortal)( BSTR bstIPAddress,BSTR bstUserName ,
BSTR bstPassWord , BSTR* bstTargetList,BSTR* bstStatus );
STDMETHOD(LogOnToTargetPortal)( BSTR bstTargetIdName, BSTR* bstStatus );
STDMETHOD(LogOffTargetPortal)( BSTR bstTargetIdName, BSTR* bstStatus );
STDMETHOD(RemoveIscsiTargetPortal)( BSTR bstIPAddress, BSTR* bstStatus );
- Similarly, add these statements on top of the implementation file or in the header file but in a global namespace:
#define MAX_LOADSTRING 200
WCHAR wchRetVal[4096];
typedef HRESULT(__stdcall *AddTargetPortal)(TCHAR,ULONG,PISCSI_LOGIN_OPTIONS ,
ISCSI_SECURITY_FLAGS,PISCSI_TARGET_PORTAL);
typedef HRESULT(__stdcall *GetSessionList)
( ULONG* , ULONG* , PISCSI_SESSION_INFO );
typedef HRESULT(__stdcall *LogonIscsiTarget) (TCHAR* ,BOOLEAN ,TCHAR*,ULONG ,
PISCSI_TARGET_PORTAL,ISCSI_SECURITY_FLAGS, PISCSI_TARGET_MAPPING,
PISCSI_LOGIN_OPTIONS ,ULONG ,CHAR*,BOOLEAN,
PISCSI_UNIQUE_SESSION_ID ,PISCSI_UNIQUE_CONNECTION_ID );
typedef HRESULT(__stdcall *ReportTargets)(BOOLEAN, ULONG* , TCHAR*);
typedef HRESULT(__stdcall *GetDevices)(PISCSI_UNIQUE_SESSION_ID, ULONG* ,
PISCSI_DEVICE_ON_SESSION);
typedef HRESULT(__stdcall *LogOutOfIscsiTarget)(PISCSI_UNIQUE_SESSION_ID);
typedef HRESULT(__stdcall *RemoveIScsiTargetPortal)
(TCHAR*,ULONG,PISCSI_TARGET_PORTAL);
typedef HRESULT(__stdcall *ReportSendTargetPortals)
(ULONG*, PISCSI_TARGET_PORTAL_INFO);
- Now in the implementation file of the class
CIscsi
i.e., the .cpp file, you will have the implementation of the functions declared in the header file which will allow us to add a target portal, log on to a specified available target and add it as a separate disc to the system. After that, log off from the target and finally detach the target portal from the system. You can make targets as persistent device too (means even if the base machine gets rebooted, the target which was added as a persistent device will remain attached to the system). This is also implemented in the code. All other functionalities can be added on similar guidelines provided these steps are performed systematically.
The first function that is to be added according to the order of calling is 'AddIscsiTargetPortal
' which will be responsible for adding the specified target portal. This function will take the following arguments:
- type (in parameter) '
ipaddress
' which will specify the address of the Target Portal to be added. This will be a string
, i.e. BSTR
- type (in parameter) '
username
' which will again be a BSTR
but will be required only when authentication is required, else this will be NULL
.
- type (in parameter) '
password
' again a BSTR
which again will be required only when authentication is required, else this will also be NULL
.
- type (out parameter) '
targetlist
', As this is an out
parameter, this returns the target list in the form of a pointer to string
after the target portal has been successfully added.
- type (out parameter) '
status
', This is the parameter which is returned as a pointer to the string
indicating the status of the function.
STDMETHODIMP CIscsi::AddIscsiTargetPortal( BSTR bstIPAddress,
BSTR bstUserName ,BSTR bstPassWord,
BSTR* bstTargetList ,
BSTR* bstStatus )
{
CString strFileName(bstIPAddress) ;
CString strUserName(bstUserName); CString strPassWord(bstPassWord); HRESULT hr = S_OK;
if (!m_hMod) {
CString strTemp1 = _T("Failed"); *bstStatus = strTemp1.AllocSysString();
return S_FALSE;
}
else
{
HMODULE hMod = m_hMod;
GetSessionList fpGetSessionlist = NULL; ISCSI_SESSION_INFO m_sessionInfo[250];
if (bstIPAddress == NULL)
{
return S_FALSE;
}
else
{
CString strTargetList;
if (((strFileName.GetString()!= NULL) && (strFileName.GetLength() > 0) ) )
{
AddTargetPortal fpGetTargetPortal = NULL;
fpGetTargetPortal = ( AddTargetPortal )GetProcAddress(hMod,
"AddIScsiSendTargetPortalW");
if ( fpGetTargetPortal!= NULL )
{
ISCSI_LOGIN_OPTIONS sLoginOptions ;
sLoginOptions.Version = ISCSI_LOGIN_OPTIONS_VERSION;
sLoginOptions.AuthType = ISCSI_NO_AUTH_TYPE;
sLoginOptions.DataDigest = ISCSI_DIGEST_TYPE_NONE ;
sLoginOptions.DefaultTime2Retain = 10; sLoginOptions.DefaultTime2Wait = 10; sLoginOptions.HeaderDigest =ISCSI_DIGEST_TYPE_NONE ; sLoginOptions.InformationSpecified =
ISCSI_LOGIN_OPTIONS_DEFAULT_TIME_2_RETAIN |
ISCSI_LOGIN_OPTIONS_AUTH_TYPE |
ISCSI_LOGIN_OPTIONS_DATA_DIGEST |
ISCSI_LOGIN_OPTIONS_DEFAULT_TIME_2_WAIT |
ISCSI_LOGIN_OPTIONS_HEADER_DIGEST ;
sLoginOptions.LoginFlags = ISCSI_LOGIN_FLAG_MULTIPATH_ENABLED;
sLoginOptions.MaximumConnections = 1; sLoginOptions.UsernameLength = NULL; sLoginOptions.Username =(UCHAR*) strUserName.GetString();
sLoginOptions.Password = (UCHAR*) strPassWord.GetString();
sLoginOptions.PasswordLength = NULL ;
ISCSI_TARGET_PORTAL sTargetPortal;
memset(sTargetPortal.Address ,0, 256);
sTargetPortal.Socket = NULL;
memset(sTargetPortal.SymbolicName ,0, 256);
USHORT uSocket = 3260 ; CString cIpAddress = strFileName.GetString(); CString cSymbolicName;
wcscpy(sTargetPortal.Address, cIpAddress.GetString());
wcscpy(sTargetPortal.SymbolicName, cSymbolicName.GetString());
sTargetPortal.Socket = uSocket;
hr = fpGetTargetPortal( NULL, ISCSI_ALL_INITIATOR_PORTS ,
&sLoginOptions,NULL ,&sTargetPortal); if( hr == S_OK )
{
CString strtemp = reportZiscsiTargets();
*bstTargetList = strtemp.AllocSysString();
CString strTemp1 = _T("Success");
*bstStatus = strTemp1.AllocSysString();
return S_OK;
}
else
{
CString strtemp = reportZiscsiTargets();
*bstTargetList = strtemp.AllocSysString();
CString strTemp1 = _T("Failed");
*bstStatus = strTemp1.AllocSysString();
return S_FALSE;
}
}
else
{
}
}
else
{
WCHAR* strtemp = reportZiscsiTargets();
*bstTargetList = strtemp;
return S_FALSE;
}
}
}
WCHAR* CIscsi::reportZiscsiTargets()
{
HRESULT hr = S_OK;
HMODULE hMod = m_hMod;
CString strTargetList;
ReportTargets fpReportTargets =NULL;
CString tTargets;
fpReportTargets = ( ReportTargets )GetProcAddress(hMod, "ReportIScsiTargetsW");
if(fpReportTargets != NULL)
{
ULONG uBuffSizeForTargets = 2048 ;
TCHAR tBuff[2048];
hr = fpReportTargets(TRUE,&uBuffSizeForTargets,
tBuff);
if(hr == S_OK)
{
for (int i = 0 ; i< uBuffSizeForTargets;i++)
{
if (tBuff[i] != '\0')
{
tTargets.AppendChar(tBuff[i]);
}
else if ( tBuff[i] == '\0' && tBuff[i+1] != 0 )
{
tTargets.Append(_T("$*$"));
}
}
memset(wchRetVal,0,4096);
wcscpy(wchRetVal,tTargets.GetString());
return wchRetVal;
}
else
{
return NULL;
}
}
else
{
return NULL;
}
}
- Now as you have already added the target portal, you will need to log on to the specified target which you will like to attach to your base machine as a separate disc or drive. This can be done in a very simple manner as it is depicted below. This function will add the ISCSI target to the machine if the format is recognized by Windows:
STDMETHODIMP CIscsi::LogOnToTargetPortal( BSTR bstTargetIdName, BSTR* bstStatus )
{
HRESULT hr = NULL;
HMODULE hMod =NULL;
UINT uCountForLogicalDrives = 0;
if (!m_hMod)
{
CString strTemp = _T("Failed");
*bstStatus = strTemp.AllocSysString();
return S_FALSE;
}
hMod = m_hMod;
CString strDriveListbeforeLogin ;
CString strDriveListAfterLogin ;
DWORD iLogicalDrives = ::GetLogicalDrives();
for (int j=1, i=0; i< 26; i++)
{
if (((1 << i) & iLogicalDrives) != 0)
{
CString str;
str.Format(_T("%c"), i+'A');
strDriveListbeforeLogin.Append(str);
strDriveListbeforeLogin.Append(_T(","));
j++;
uCountForLogicalDrives++;
} }
CString strTargetSelected(bstTargetIdName);
LogonIscsiTarget fpLogonIscsiTarget = NULL;
ISCSI_UNIQUE_SESSION_ID pUniqueSessionId;
pUniqueSessionId.AdapterSpecific = NULL;
pUniqueSessionId.AdapterUnique = NULL;
ISCSI_UNIQUE_CONNECTION_ID pUniqueConnectionId;
pUniqueConnectionId.AdapterSpecific = NULL;
pUniqueConnectionId.AdapterUnique = NULL;
fpLogonIscsiTarget = ( LogonIscsiTarget )GetProcAddress(hMod,
"LoginIScsiTargetW");
if(fpLogonIscsiTarget != NULL)
{
hr = fpLogonIscsiTarget((TCHAR*)strTargetSelected.GetString()
,
false,NULL ,ISCSI_ANY_INITIATOR_PORT,NULL,
NULL,NULL,NULL,NULL,
NULL,false,&pUniqueSessionId,&pUniqueConnectionId );
if( hr == S_OK )
{
CString strTemp = _T("Success");
*bstStatus = strTemp.AllocSysString();
SetCursor(LoadCursor(NULL, IDC_APPSTARTING));
Sleep(10000);
UINT uCountForLogicalDrivesAfterLogin = 0;
SetCursor(LoadCursor(NULL, IDC_ARROW));
DWORD iLogicalDrivesAfterlogin = ::GetLogicalDrives();
for (int j=1, i=0; i< 26; i++)
{
if (((1 << i) & iLogicalDrivesAfterlogin) != 0)
{
CString str;
str.Format(_T("%c"), i+'A');
strDriveListAfterLogin.Append(str);
strDriveListAfterLogin.Append(_T(","));
j++;
uCountForLogicalDrivesAfterLogin++;
} }
if (uCountForLogicalDrivesAfterLogin == uCountForLogicalDrives)
{
if (strDriveListAfterLogin == strDriveListbeforeLogin)
{
::MessageBox(NULL,_T(
"The logged on ISCSI target could" +
"not be mounted as it has an unknown" +
"format"),_T("CIscsi"),MB_OK);
return S_OK;
}
}
if (uCountForLogicalDrivesAfterLogin != uCountForLogicalDrives)
{
if(strDriveListAfterLogin != strDriveListbeforeLogin)
{
if (strDriveListAfterLogin.GetLength() >
strDriveListbeforeLogin.GetLength())
{
strDriveListAfterLogin.Replace
(strDriveListbeforeLogin.GetString(),_T(""));
WCHAR* tempBuff = (WCHAR*)
strDriveListAfterLogin.GetString();
strDriveListAfterLogin = tempBuff[0];
CString strMsg ;
strMsg.Format(_T(
" Logical drive mounted :: %s .\n Do you want to open" +
"this drive in Explorer?"),
strDriveListAfterLogin.GetString());
tempBuff = NULL;
strDriveListAfterLogin.Append(_T(":\\"));
if (::MessageBox(NULL,strMsg,_T("Kush_Iscsi"),
MB_YESNO) == IDYES)
{
// This will open the drive on the choice of user
ShellExecuteA(NULL, "open",
CW2A(strDriveListAfterLogin.GetString()),
NULL, NULL,
SW_SHOWNORMAL);
}
}
}
}
return S_OK;
}
else
{
CString strTemp = _T("Failed");
*bstStatus = strTemp.AllocSysString();
return S_FALSE;
}
}
else
{
CString strTemp = _T("Failed");
*bstStatus = strTemp.AllocSysString();
return S_FALSE;
}
}
- Being logged in and having performed all the operations, the application now wants to log off this target. Logging off the target will only succeed when the target is logged on. So, first a check to make sure that the target is logged on is done followed by the process of logging out. This can be done as under:
)
STDMETHODIMP CIscsi::LogOffTargetPortal( BSTR bstTargetIdName, BSTR* bstStatus
{
HRESULT hr = S_OK;
GetSessionList fpGetSessionlist = NULL;
ISCSI_SESSION_INFO m_sessionInfo[125];
if (!m_hMod)
{
CString strTemp = _T("Failed");
*bstStatus = strTemp.AllocSysString();
return S_FALSE;
}
HMODULE hMod = m_hMod;
CString strTargetSelected (bstTargetIdName);
LogonIscsiTarget fpLogonIscsiTarget = NULL;
ISCSI_UNIQUE_SESSION_ID pUniqueSessionId;
pUniqueSessionId.AdapterSpecific = NULL;
pUniqueSessionId.AdapterUnique = NULL;
ISCSI_UNIQUE_CONNECTION_ID pUniqueConnectionId;
pUniqueConnectionId.AdapterSpecific = NULL;
pUniqueConnectionId.AdapterUnique = NULL;
fpLogonIscsiTarget = ( LogonIscsiTarget )GetProcAddress(hMod,
"LoginIScsiTargetW");
if(fpLogonIscsiTarget != NULL)
{
hr = fpLogonIscsiTarget((TCHAR*)strTargetSelected.GetString()
,
false,NULL ,ISCSI_ANY_INITIATOR_PORT,NULL,NULL,
NULL,NULL,NULL,
NULL,false,&pUniqueSessionId,&pUniqueConnectionId );
if(hr == S_OK)
{
LogOutOfIscsiTarget fpLogOut;
fpLogOut = ( LogOutOfIscsiTarget )GetProcAddress(hMod,
"LogoutIScsiTarget");
if(fpLogOut)
{
hr = fpLogOut( &pUniqueSessionId);
if (hr == S_OK)
{
CString strTemp = _T("Target Not Connected");
*bstStatus = strTemp.AllocSysString();
return S_FALSE;
}
else
{
}
}
}
else if (hr == -268500929 || hr != S_OK )
{
fpGetSessionlist = ( GetSessionList )GetProcAddress(hMod,
"GetIScsiSessionListW"); if ( fpGetSessionlist != NULL )
{
ULONG uBuffSize = sizeof(m_sessionInfo);
ULONG uSessionCount = 0;
hr = fpGetSessionlist(&uBuffSize,&uSessionCount,
m_sessionInfo);
if( hr == S_OK && uSessionCount != 0)
{
CString strTemp;
int iCmp = 1;
for ( int i = 0 ; i < uSessionCount ; i ++ )
{
strTemp.Empty();
strTemp = m_sessionInfo[i].TargetName;
iCmp = wcscmp(m_sessionInfo[i].TargetName,
strTargetSelected.GetString());
if (iCmp == 0)
{
LogOutOfIscsiTarget fpLogOut;
fpLogOut = ( LogOutOfIscsiTarget )GetProcAddress(hMod,
"LogoutIScsiTarget");
if(fpLogOut)
{
hr = fpLogOut( &m_sessionInfo[i].SessionId);
if (hr == S_OK)
{
CString strTemp = _T("Success"); *bstStatus = strTemp.AllocSysString();
return S_OK;
}
else
{
CString strTemp = _T("Failed");
*bstStatus = strTemp.AllocSysString();
::MessageBox(NULL,_T(
"The specified Target could not be logged" +
"off as the drive associated
with this target" +
"might be in use.\n Close all the windows" +
"opened in the Explorer and try again "),
_T("kush_Iscsi"),MB_OK|MB_ICONINFORMATION);
return S_FALSE;
}
}
else
{
}
}
else
{
}
}
}
else
{
CString strTemp = _T("Failed");
*bstStatus = strTemp.AllocSysString();
return S_FALSE;
}
}
}
}
else
{
CString strTemp = _T("Failed");
*bstStatus = strTemp.AllocSysString();
return S_FALSE;
}
}
- Finally if the user wants to remove the added target portal all together, he can do it freely in the following manner. This is described below with the code:
STDMETHODIMP CIscsi::RemoveIscsiTargetPortal( BSTR
bstIPAddress,BSTR* bstStatus )
{
HRESULT hr = S_OK;
if (!m_hMod) {
CString strtemp = _T("Failed");
*bstStatus = strtemp.AllocSysString();
return S_FALSE;
}
HMODULE hMod = m_hMod;
CString strListText(bstIPAddress);
ReportSendTargetPortals fpSendTargetportals = NULL;
fpSendTargetportals = ( ReportSendTargetPortals )GetProcAddress(hMod,
"ReportIScsiSendTargetPortalsW");
if ( fpSendTargetportals )
{
ULONG uPortalCount = 0;
hr = fpSendTargetportals(&uPortalCount,NULL);
if (hr == S_OK || hr == 122)
{
ISCSI_TARGET_PORTAL_INFO *pTargetPortalInfo =
new ISCSI_TARGET_PORTAL_INFO [uPortalCount];
ISCSI_TARGET_PORTAL pFinalTargetPortal;
ULONG uPortNumber = 0;
int iCount = 0;
hr = fpSendTargetportals(&uPortalCount,pTargetPortalInfo);
if (hr == S_OK && uPortalCount > 0)
{
for ( int i = 0 ; i < uPortalCount ; i++ )
{
int iCmp = wcscmp(pTargetPortalInfo[i].Address,
strListText.GetString());
if ( iCmp == 0 ) {
wcscpy( pFinalTargetPortal.Address ,
pTargetPortalInfo[i].Address );
pFinalTargetPortal.Socket = pTargetPortalInfo[i].Socket ;
wcscpy( pFinalTargetPortal.SymbolicName ,
pTargetPortalInfo[i].SymbolicName );
uPortNumber = pTargetPortalInfo[i].InitiatorPortNumber;
iCount = i;
break;
}
else
{
}
}
RemoveIScsiTargetPortal fpRemoveIscsiTargetPortal = NULL;
fpRemoveIscsiTargetPortal =
( RemoveIScsiTargetPortal )GetProcAddress(hMod,
"RemoveIScsiSendTargetPortalW");
if ( fpRemoveIscsiTargetPortal )
{
hr = fpRemoveIscsiTargetPortal( NULL,
uPortNumber,&pFinalTargetPortal);
if (hr == S_OK )
{
CString strtemp = _T("Success"); *bstStatus = strtemp.AllocSysString();
return S_OK;
}
else
{
CString strtemp = _T("Failed"); *bstStatus = strtemp.AllocSysString();
return S_FALSE;
}
}
else
{
}
}
}
}
}
Conclusion
Thus, these are the basic functionalities which can be achieved through the wonderful tool provided by Microsoft. There is a lot more stuff which can be done though ISCSI which I have not covered here as this was meant to be only an initial tutorial on the basic implementation for ISCSI usage on Windows. I hope I covered the basic explanation from a developer's point of view and also the development part of it. In case of any queries, you are free to email me at tiwari.kushagra@gmail.com. I will get back to you as soon as possible.
Related Links