MOSS MVP

I've moved my blog to http://blog.falchionconsulting.com!. Please update your links. This blog is no longer in use--you can find all posts and comments at my new blog; I will no longer be posting to this site and comments have been disabled.

Monday, March 31, 2008

Extend Web Application

I had some free time one night and decided to work on something that I'd had several people ask me about - extending a web application programmatically. Honestly I was surprised at how many people had specifically asked me to create this command. To accomplish this I created a new command: gl-extendwebapp. Note that I'm starting to prefix my commands (something I should always have been doing) and I will eventually set all commands to have this prefix so expect that breaking change to come soon. The code is actually not too bad - you basically create a new SPIisSettings object and add an SPServerBinding or SPSecureBinding object based on whether it's an SSL site or not. The only odd piece of my code is that I use a little bit of reflection so that I can fire the timer job using the same code that Microsoft uses when executed via the browser:

   1: using System;
   2: using System.DirectoryServices;
   3: using System.Globalization;
   4: using System.IO;
   5: using System.Reflection;
   6: using System.Text;
   7: using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
   8: using Lapointe.SharePoint.STSADM.Commands.SPValidators;
   9: using Microsoft.SharePoint;
  10: using Microsoft.SharePoint.Administration;
  11:  
  12: namespace Lapointe.SharePoint.STSADM.Commands.WebApplications
  13: {
  14:     public class ExtendWebApplication : SPOperation
  15:     {
  16:         /// <summary>
  17:         /// Initializes a new instance of the <see cref="ExtendWebApplication"/> class.
  18:         /// </summary>
  19:         public ExtendWebApplication()
  20:         {
  21:             SPParamCollection parameters = new SPParamCollection();
  22:             parameters.Add(new SPParam("url", "url", true, null, new SPUrlValidator()));
  23:             parameters.Add(new SPParam("vsname", "vsname", true, null, new SPNonEmptyValidator()));
  24:             parameters.Add(new SPParam("allowanonymous", "anon"));
  25:             parameters.Add(new SPParam("exclusivelyusentlm", "ntlm"));
  26:             parameters.Add(new SPParam("usessl", "ssl"));
  27:             parameters.Add(new SPParam("hostheader", "hostheader", false, null, new SPNonEmptyValidator()));
  28:             parameters.Add(new SPParam("port", "p", false, "80", new SPIntRangeValidator(0, int.MaxValue)));
  29:             parameters.Add(new SPParam("path", "path", true, null, new SPNonEmptyValidator()));
  30:             SPEnumValidator zoneValidator = new SPEnumValidator(typeof (SPUrlZone));
  31:             parameters.Add(new SPParam("zone", "zone", false, SPUrlZone.Custom.ToString(), zoneValidator));
  32:             parameters.Add(new SPParam("loadbalancedurl", "lburl", true, null, new SPUrlValidator()));
  33:  
  34:             StringBuilder sb = new StringBuilder();
  35:             sb.Append("\r\n\r\nExtends a web application onto another IIS web site.  This allows you to serve the same content on another port or to a different audience\r\n\r\nParameters:");
  36:             sb.Append("\r\n\t-url <url of the web application to extend>");
  37:             sb.Append("\r\n\t-vsname <web application name>");
  38:             sb.Append("\r\n\t-path <path>");
  39:             sb.Append("\r\n\t-loadbalancedurl <the load balanced URL is the domain name for all sites users will access in this SharePoint Web application>");
  40:             sb.AppendFormat("\r\n\t[-zone <{0} (defaults to Custom)>]", zoneValidator.DisplayValue);
  41:             sb.Append("\r\n\t[-port <port number (default is 80)>]");
  42:             sb.Append("\r\n\t[-hostheader <host header>]");
  43:             sb.Append("\r\n\t[-exclusivelyusentlm]");
  44:             sb.Append("\r\n\t[-allowanonymous]");
  45:             sb.Append("\r\n\t[-usessl]");
  46:  
  47:             Init(parameters, sb.ToString());
  48:  
  49:         }
  50:  
  51:         /// <summary>
  52:         /// Gets the help message.
  53:         /// </summary>
  54:         /// <param name="command">The command.</param>
  55:         /// <returns></returns>
  56:         public override string GetHelpMessage(string command)
  57:         {
  58:             return HelpMessage;
  59:         }
  60:  
  61:         /// <summary>
  62:         /// Runs the specified command.
  63:         /// </summary>
  64:         /// <param name="command">The command.</param>
  65:         /// <param name="keyValues">The key values.</param>
  66:         /// <param name="output">The output.</param>
  67:         /// <returns></returns>
  68:         public override int Execute(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)
  69:         {
  70:             output = string.Empty;
  71:  
  72:             SPWebApplication webApplication = SPWebApplication.Lookup(new Uri(Params["url"].Value.TrimEnd('/')));
  73:             string description = Params["vsname"].Value;
  74:             bool useSsl = Params["usessl"].UserTypedIn;
  75:             string hostHeader = Params["hostheader"].Value;
  76:             int port = int.Parse(Params["port"].Value);
  77:             bool allowAnonymous = Params["allowanonymous"].UserTypedIn;
  78:             bool useNtlm = Params["exclusivelyusentlm"].UserTypedIn;
  79:             string path = Params["path"].Value;
  80:             SPUrlZone zone = (SPUrlZone) Enum.Parse(typeof (SPUrlZone), Params["zone"].Value, true);
  81:             string loadBalancedUrl = Params["loadbalancedurl"].Value;
  82:  
  83:             ExtendWebApp(webApplication, description, hostHeader, port, loadBalancedUrl, path, allowAnonymous, useNtlm, useSsl, zone);
  84:  
  85:             return OUTPUT_SUCCESS;
  86:         }
  87:  
  88:         /// <summary>
  89:         /// Extends the web app.
  90:         /// </summary>
  91:         /// <param name="webApplication">The web application.</param>
  92:         /// <param name="description">The description.</param>
  93:         /// <param name="hostHeader">The host header.</param>
  94:         /// <param name="port">The port.</param>
  95:         /// <param name="loadBalancedUrl">The load balanced URL.</param>
  96:         /// <param name="path">The path.</param>
  97:         /// <param name="allowAnonymous">if set to <c>true</c> [allow anonymous].</param>
  98:         /// <param name="useNtlm">if set to <c>true</c> [use NTLM].</param>
  99:         /// <param name="useSsl">if set to <c>true</c> [use SSL].</param>
 100:         /// <param name="zone">The zone.</param>
 101:         public static void ExtendWebApp(SPWebApplication webApplication, string description, string hostHeader, int port, string loadBalancedUrl, string path, bool allowAnonymous, bool useNtlm, bool useSsl, SPUrlZone zone)
 102:         {
 103:             SPServerBinding serverBinding = null;
 104:             SPSecureBinding secureBinding = null;
 105:             if (!useSsl)
 106:             {
 107:                 serverBinding = new SPServerBinding();
 108:                 serverBinding.Port = port;
 109:                 serverBinding.HostHeader = hostHeader;
 110:             }
 111:             else
 112:             {
 113:                 secureBinding = new SPSecureBinding();
 114:                 secureBinding.Port = port;
 115:             }
 116:  
 117:             SPIisSettings settings = new SPIisSettings(description, allowAnonymous, useNtlm, serverBinding, secureBinding, new DirectoryInfo(path.Trim()));
 118:             settings.PreferredInstanceId = GetPreferredInstanceId(description);
 119:  
 120:             webApplication.IisSettings.Add(zone, settings);
 121:             webApplication.AlternateUrls.SetResponseUrl(new SPAlternateUrl(new Uri(loadBalancedUrl), zone));
 122:             webApplication.AlternateUrls.Update();
 123:             webApplication.Update();
 124:             webApplication.Provision();
 125:             if (SPFarm.Local.TimerService.Instances.Count > 1)
 126:             {
 127:                 // SPWebApplicationProvisioningJobDefinition definition = new SPWebApplicationProvisioningJobDefinition(currentItem, false);
 128:                 Type webAppProvisionJobDefType = Type.GetType("Microsoft.SharePoint.Administration.SPWebApplicationProvisioningJobDefinition, Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
 129:                 ConstructorInfo webAppProvisionConstructor =
 130:                     webAppProvisionJobDefType.GetConstructor(Utilities.AllBindings, null, new Type[] { webApplication.GetType(), typeof(bool) }, null);
 131:                 object definition = webAppProvisionConstructor.Invoke(new object[] { webApplication, false });
 132:  
 133:                 //definition.Schedule = new SPOneTimeSchedule(DateTime.Now);
 134:                 Utilities.SetPropertyValue(definition, "Schedule", new SPOneTimeSchedule(DateTime.Now));
 135:  
 136:                 //definition.Update();
 137:                 Utilities.ExecuteMethod(definition, "Update", new Type[] {}, new object[] {});
 138:             }
 139:         }
 140:  
 141:         /// <summary>
 142:         /// Gets the preferred instance id.
 143:         /// </summary>
 144:         /// <param name="iisServerComment">The IIS server comment.</param>
 145:         /// <returns></returns>
 146:         private static int GetPreferredInstanceId(string iisServerComment)
 147:         {
 148:             try
 149:             {
 150:                 int num;
 151:                 if (!LookupByServerComment(iisServerComment, out num))
 152:                 {
 153:                     return GetUnusedInstanceId(0);
 154:                 }
 155:                 return num;
 156:             }
 157:             catch
 158:             {
 159:                 return GetUnusedInstanceId(0);
 160:             }
 161:         }
 162:  
 163:         /// <summary>
 164:         /// Lookups the by server comment.
 165:         /// </summary>
 166:         /// <param name="serverComment">The server comment.</param>
 167:         /// <param name="instanceId">The instance id.</param>
 168:         /// <returns></returns>
 169:         private static bool LookupByServerComment(string serverComment, out int instanceId)
 170:         {
 171:             instanceId = -1;
 172:             using (DirectoryEntry entry = new DirectoryEntry("IIS://localhost/w3svc"))
 173:             {
 174:                 foreach (DirectoryEntry entry2 in entry.Children)
 175:                 {
 176:                     if (entry2.SchemaClassName != "IIsWebServer")
 177:                     {
 178:                         continue;
 179:                     }
 180:                     string str = (string) entry2.Properties["ServerComment"].Value;
 181:                     if (!Utilities.StsCompareStrings(str, serverComment))
 182:                         continue;
 183:  
 184:                     instanceId = int.Parse(entry2.Name, NumberFormatInfo.InvariantInfo);
 185:                     return true;
 186:                 }
 187:             }
 188:             return false;
 189:         }
 190:  
 191:         /// <summary>
 192:         /// Gets the unused instance id.
 193:         /// </summary>
 194:         /// <param name="preferredInstanceId">The preferred instance id.</param>
 195:         /// <returns></returns>
 196:         private static int GetUnusedInstanceId(int preferredInstanceId)
 197:         {
 198:             Random random = new Random();
 199:             int num = 0;
 200:             int num2 = preferredInstanceId;
 201:             if (num2 < 1)
 202:             {
 203:                 num2 = random.Next(1, 0x7fffffff);
 204:             }
 205:  
 206:             while (true)
 207:             {
 208:                 if (++num >= 0x19)
 209:                 {
 210:                     throw new InvalidOperationException(SPResource.GetString("CannotFindUnusedInstanceId", new object[0]));
 211:                 }
 212:                 if (DirectoryEntry.Exists("IIS://localhost/w3svc/" + num2))
 213:                 {
 214:                     num2 = random.Next(1, 0x7fffffff);
 215:                 }
 216:                 else
 217:                     break;
 218:             }
 219:             return num2;
 220:         }
 221:     }
 222: }

The syntax of the command can be seen below. Note that the vsname parameter is just the display name within IIS (also known as the server comment). The other fields are pretty self explanatory and match the fields seen via the browser:

C:\>stsadm -help gl-extendwebapp

stsadm -o gl-extendwebapp

Extends a web application onto another IIS web site.  This allows you to serve the same content on another port or to a different audience

Parameters:
        -url <url of the web application to extend>
        -vsname <web application name>
        -path <path>
        -loadbalancedurl <the load balanced URL is the domain name for all sites users will access in this SharePoint Web application>
        [-zone <default | intranet | internet | custom | extranet (defaults to Custom)>]
        [-port <port number (default is 80)>]
        [-hostheader <host header>]
        [-exclusivelyusentlm]
        [-allowanonymous]
        [-usessl]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-extendwebapp WSS v3, MOSS 2007 Released: 3/31/2008
Updated: 11/5/2008

Parameter Name Short Form Required Description Example Usage
url   Yes The URL of the existing web application to extend. -url http://portal
vsname   Yes The virtual server name to use - this is the name that will appear in the IIS manager. -vsname "New Portal - 80"
path   Yes The physical path to store the web files in. -path c:\moss\webs\newportal
loadbalancedurl lburl Yes The load balanced URL is the domain name for all sites users will access in this SharePoint web application. -loadbalancedurl http://newportal

-lburl http://newportal
zone   No The zone to use.  Valid values are: default, intranet, internet, custom, extranet.  If omitted defaults to custom.  If a value is already in use then the following error will be returned: "An item with the same key has already been added." -zone intranet
port p N The port to bind the web application to.  If not specified defaults to 80. -port 80

-p 80
hostheader   N The host header to use. -hostheader newportal
exclusivelyusentlm ntlm N Specifies to exclusively use NTLM authentication instead of Negotiate (Kerberos). -exclusivelyusentlm

-ntlm
allowanonymous anon N

Specifies the default state for anonymous access during virtual server provisioning. The default setting is off, regardless of the current IIS setting. The administrator needs to explicitly turn on anonymous access.

IIS anonymous access must be on for pluggable authentication. Anonymous requests must make it through IIS to get to the ASP.NET authentication system.

There is no anonymous access choice when provisioning with forms-based authentication.

NoteNote:

Allowing anonymous access in IIS does not automatically make all Microsoft Office SharePoint Server 2007 sites anonymously accessible. There is Web-level anonymous access control as well, which is also off by default. However, disabling anonymous access in IIS does disable anonymous access to all Office SharePoint Server 2007 sites on the Web application because IIS rejects the request before code even runs.

-allowanonymous

-anon
usessl ssl No Use Secure Sockets Layer (SSL).  If you choose to use SSL, you must add the certificate on each server using the IIS administration tools.  Until this is done, the web application will be inaccessible from this IIS Web Site. -usessl

-ssl

Here's an example of how to extend an existing web application:

stsadm -o gl-extendwebapp -url http://portal -vsname "New Portal - 80" -path c:\moss\webs\newportal -loadbalancedurl http://newportal -zone custom -port 80 -hostheader newportal

Update 11/5/2008: I fixed an issue where the PreferredInstanceId was not being set.  Thanks to Michael (see comments) for pointing out the issue.

8 comments:

Tony S said...

Hi Garry,

I've had a go at using the gl-extendwebapp stsadm command and seem to observe a strange result. It creates all the appropriate mappings in IIS but when I browse to the location, it prompts me for credentials 3 times and then just displays an empty screen.

It's running within a larger batch file with the following parameters:

-o gl-extendwebapp -url %NameAndPort% -vsname %centraladminhostheader% -path %webapppath%%centraladminhostheader% -loadbalancedurl %centraladminurl% -zone Intranet -port 80 -hostheader %centraladminhostheader% >> %LOGFILENAME%

It's on IIS 7 and Server08 whether that's anything specific. Do you have any suggestions about where I might go hunting? It returns no error messages and says operation successful.

Gary Lapointe said...

Tony - sounds like your issue is related to this post: http://www.harbar.net/archive/2008/05/18/Using-Kerberos-with-SharePoint-on-Windows-Server-2008.aspx. You basically need to disable kernel-mode authentication.

Michael said...

Hi Gary,

I'm hoping to use this command to simplify provisioning – so far it is very helpful. However, I do have a custom feature that depends on the “PreferredInstanceId” of each of the Web Application’s zones.

When I extend a web application with this command, it doesn’t appear that “PreferredInstanceId” property gets set.

Using reflection, I dug around in the codebehind for “extendwebfarm” and found that SPIisSettings is created by calling SPWebApplication.CreateIisSettingsObject (an internal method). I believe the InstanceID is set within that method.

Do you see similar results (PreferredInstanceId not being set)? If so, do you think reflection could be used to create the SPIisSettings object – or set the InstanceID itself?

Thanks,
Michael

Gary Lapointe said...

Michael - nice catch - I'd forgotten that little piece of code when I was reverse engineering on how to do this - I've just pushed a new build out which properly sets the preferred ID when the web app is extended.

Ivan said...

Hi Gary,

I have a Go Live situation of a client's web site and as obvious the deployment must be fool proof. In doing so, I am creating a replica of existing web app on live server and restoring it on a new web app. Once the sanity is verified, the AAM will be changed to access the new site.
The original site is extended to be a public facing site with custom auth. My question is, is it by any means possible either programmatically or by tweaking the config dabtase that I can point the existing extended web app to the new web app/database that I have created? This will minimize any downtime as oppose to delete the existing extended web app, extend the new web app on same same public url and then deploy custom auth.

Waiting for your expert opinion...

Gary Lapointe said...

I may not be entirely following what you are wanting to do but one thing that may work for you would be to make a copy of the original database (backup and restore to a new database) then add that new database as a content database to your new web application and then remove the old content database. The thing you've got me confused on is that if you extend a web application it's using the same content database as the original - if you want different content databases you create a new web application.

joe mcpeak said...

Great stuff Garry, thanks! One question though: while using Reflector to poke around in the central admin's _admin/extendwebfarm.aspx code behind (specifically Microsoft.SharePoint.ApplicationPages.ExtendWebFarmPage's BtnSubmit_Click() method) I notice that the code there includes the following:
SPAdministrationWebApplication local = SPAdministrationWebApplication.Local;
if (local != null)
{
local.RemoveAdministrativeTask("WSSCreateWebApplication");
}

Can you comment on why you omitted this in your version?

Gary Lapointe said...

Not sure if that particular bit of code was there when I originally wrote this - perhaps it was, just don't remember.