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.

Wednesday, September 26, 2007

Upgrade2

This is one of those things that I just shouldn't have had to create. So I'm going through my upgrade and I come to the point where I'm ready to start scripting the upgrade of the individual team sites (I'd been focusing on the upgraded Areas up until now). I figured no problem - this will be easy, all the documentation says I just need to use the built-in "upgrade" command - simply pass in the url you want to upgrade or an xml file containing multiple urls and you're good to go. So I started off with a command like this:

C:\>stsadm -o upgrade -sidebyside -reghost -sitelistpath "c:\sites.xml"

The sites.xml file contained a list of about 700 team sites that needed to be upgraded. The problem was that my test environment is too damn slow so the upgrade was continuously failing due to a SQL timeout.

As the timeout is set via code in some buried down deep unmanaged class I didn't see that there was much I could do to bump it up (if anyone knows differently please pass along the info). So I figured, no problem - I'll just upgrade one site at a time rather than passing all 700 sites into the upgrade tool. So I went to the command line and tried the following:

C:\>stsadm -o upgrade -sidebyside -reghost -url "http://intranet/sites/site1"

To my dismay I got this error in return: "Missing required argument: sitelistpath.". This made no sense to me - all the documentation I've read has made it clear that sitelistpath is optional and that you can pass in an url parameter to upgrade a single site.

So I decided to decompile the SPUpgrade class contained within stsadm itself. After spending a significant amount of time studying the code I realized that the url parameter is not utilized at all - I've dug through every line of the code and it's simply not there (with the exception of the Validation method which sets the Enabled property on the SPParam object which basically has no effect on anything as the parameter is enabled by default anyways).

So why is it listed as a parameter and why does all the documentation make it so obvious that it can be used? If anyone can find evidence to the contrary please let me know as this makes no sense to me at all.

For my purposes I didn't want to have to create 700 xml files just so that I could upgrade one site at a time (I can autogenerate my batch file using a simple SQL statement but generating xml files would be problematic without writing a separate tool to do it and I really didn't want to do that). So I decided to create this command, gl-upgrade2, which is really nothing more than a convenience hack - it functions basically the same as the original upgrade command but I've made it so that the url parameter actually works. I did this by simply creating a temporary xml file and then passing in all the parameters to the built-in upgrade command - so in a sense I'm just acting as a shell for the built-in command but now you don't have to create that xml file to just upgrade a single site - the command will create it for you behind the scenes (I also added a targetdb parameter so that you can specify the target database as well).

My gut tells me that I'm missing something - either that or the built-in upgrade command is just really misleading and a lot of people a lot smarter than me have been getting it wrong (something I doubt) - so if anyone out there knows differently as to the function of the url parameter in regards to the built-in upgrade command please, please, pass whatever you can along.

The core code is shown below - as you can see almost all the code is just building the command to make the call to the built-in upgrade command with the addition of the file generation:

   1: public override int Execute(string command, StringDictionary keyValues, out string output)
   2: {
   3:     output = string.Empty;
   4:  
   5:     /* stsadm.exe -o upgrade
   6:            {-inplace |
   7:             -sidebyside}
   8:            [-url <url>]
   9:            [-forceupgrade]
  10:            [-quiet]
  11:            [-farmuser <farm user>]
  12:            [-farmpassword <farm user password>]
  13:            [-reghost]
  14:            [-sitelistpath <sites xml file>]
  15:     */
  16:     StringBuilder cmd = new StringBuilder();
  17:     cmd.Append(" -o upgrade");
  18:  
  19:     if (Params["forceupgrade"].UserTypedIn)
  20:         cmd.Append(" -forceupgrade");
  21:  
  22:     if (Params["quiet"].UserTypedIn)
  23:         cmd.Append(" -quiet");
  24:  
  25:     if (Params["inplace"].UserTypedIn)
  26:         cmd.Append(" -inplace");
  27:     else if (Params["sidebyside"].UserTypedIn)
  28:         cmd.Append(" -sidebyside");
  29:  
  30:     if (Params["reghost"].UserTypedIn)
  31:         cmd.Append(" -reghost");
  32:  
  33:     if (Params["farmuser"].UserTypedIn)
  34:         cmd.AppendFormat(" -farmuser \"{0}\"", Params["farmuser"].Value);
  35:  
  36:     if (Params["farmpassword"].UserTypedIn)
  37:         cmd.AppendFormat(" -farmpassword \"{0}\"", Params["farmpassword"].Value);
  38:  
  39:     if (Params["sidebyside"].UserTypedIn && Params["sitelistpath"].UserTypedIn)
  40:         cmd.AppendFormat(" -sitelistpath \"{0}\"", Params["sitelistpath"].Value);
  41:  
  42:     if (Params["inplace"].UserTypedIn && Params["url"].UserTypedIn)
  43:         cmd.AppendFormat(" -url \"{0}\"", Params["url"].Value);
  44:  
  45:     string filename = null;
  46:     if (Params["sidebyside"].UserTypedIn && Params["url"].UserTypedIn)
  47:     {
  48:         filename = Path.GetTempFileName();
  49:         string xml = string.Format("<RedirectedSites Count=\"1\"><Site Url=\"{0}\"", Params["url"].Value);
  50:         if (Params["targetdb"].UserTypedIn)
  51:         {
  52:             xml += string.Format(" TargetDatabase=\"{0}\"", Params["targetdb"].Value);
  53:         }
  54:         xml += " /></RedirectedSites>";
  55:         File.WriteAllText(filename, xml);
  56:  
  57:         cmd.AppendFormat(" -sitelistpath \"{0}\"", filename);
  58:     }
  59:  
  60:     Console.WriteLine("Upgrading site...");
  61:     if (Utilities.RunStsAdmOperation(cmd.ToString(), true) != 0)
  62:         throw new SPException("Error occured upgrading.\r\nCOMMAND: stsadm.exe" + cmd);
  63:     Console.WriteLine("Site upgraded.\r\n");
  64:  
  65:     if (filename != null)
  66:         File.Delete(filename);
  67:  
  68:     string url = Params["url"].Value;
  69:     if (Params["moveto"].UserTypedIn)
  70:     {
  71:         Console.WriteLine("Moving site collection to new location...");
  72:         MoveSite.MoveSiteCollection(url, Params["moveto"].Value);
  73:         url = Params["moveto"].Value;
  74:         Console.WriteLine("Site collection moved.");
  75:     }
  76:  
  77:     if (Params["theme"].UserTypedIn)
  78:     {
  79:         Console.WriteLine("Applying specified theme to site collection...");
  80:         Themes.ApplyTheme.ApplyThemeToWeb(Params["theme"].Value, url, true);
  81:         Console.WriteLine("Theme applied.\r\n");
  82:     }
  83:  
  84:     return OUTPUT_SUCCESS;
  85: }

The syntax of the command I created can be seen below.

c:\>stsadm -help gl-upgrade2

stsadm -o gl-upgrade2


Slightly more flexible version of the built-in upgrade command (addresses the fact that the URL parameter does not do an
ything).

Parameters:
        {-inplace |
         -sidebyside}
        [-url <url>]
        [-forceupgrade]
        [-quiet]
        [-farmuser <farm user>]
        [-farmpassword <farm user password>]
        [-reghost]
        [-sitelistpath <sites xml file>]
        [-targetdb <target database name>]
        [-moveto <url of a new target path to move the upgraded site collection to>]
        [-theme <id of the theme to apply (see C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\Template\Layouts\[LCID]\SPThemes.xml for template IDs>]

Here’s an example of how to upgrade a single site (note that it's the same as what I showed above but instead of upgrade it's upgrade2 and I've added the target database):

stsadm -o gl-upgrade2 -sidebyside -reghost -url "http://intranet/sites/site1" -targetdb "intranet1_site_pair"

Update 6/25/2008: I actually made these changes months ago but forgot to update the post.  I added the moveto and theme parameters so that I could move the site collection and set the them with one statement.  You can of course do all this using the other individual commands I created but I'm lazy and didn't want to have to do with three commands what I could do with one.

Tuesday, September 25, 2007

General Update

I've made numerous changes to most (not all) of the commands. Most of the commands now inherit from SPOperation so they now benefit from the better error handling and command argument processing. As of the time of writing this post the following commands do NOT inherit from SPOperation (still are directly implementing ISPStsadmCommand):

  • DisableUserPermissionForWebApp
  • EnableUserPermissionForWebApp
  • SetSelfServiceSiteCreation
  • CreateQuotaTemplate
  • EditQuotaTemplate
  • ConnectToPortalSite
  • SetMasterSiteDirectory
  • SetSiteDirectoryScanViewUrls
  • EnumTimerJobDefinitions
  • GetJobInfo
  • GetJobInfos
  • RunTimerJob
  • SetJobSchedule
  • Gen2003To2007ProfilePropertyMap
  • Migrate2003ProfilesTo2007

I've also changed all the code that was using site.OpenWeb() to now use site.AllWebs[serverRelativeUrl] wherever I could. The reason for this was that OpenWeb() is extremely misleading - if you pass in an url that you think is correct but actually isn't it will return back the next valid web up in the chain that it can find - this could lead to a whole host of problems so I decided to change the code to use AllWebs which will throw an exception if you attempt to load an invalid url (I think that OpenWeb(url) will do the same).

Update 10/17/2007: I've now updated all of my custom commands to use the SPOperation base class. The only classes that do not inherit from this base class are the original 4 that I did not create: GetJobInfo, GetJobInfos, RunTimerJob, and SetJobSchedule. Note that out of respect for the original author I will not be updating these remaining four commands despite the benefits that would be gained.

My Site Settings

This command replaces the gl-setmysitesnamingformat command as it includes the same functionality along with additional capabilities.

I had originally created the gl-setmysitesnamingformat command to address a need to set the naming format of personal sites and I originally didn't think I'd have to worry about the other settings on the My Sites Settings page but turns out that I did need to set one other field. So rather than create another command for just one field I decided to create a command that would allow me to set any value on that particular page.

I was originally thinking I would exclude the naming format as I already had a command for that but then decided that I'd just include it so that this command became a one-stop-shop and the other could be deprecated (I'll leave it in but there's not much need for it now that this command exists).

Like the original command I had to use reflection to set the internal properties (I just don't understand why Microsoft chose not to make the UserProfileApplication object public). Beyond using the internal UserProfileApplication class the command also uses an internal method called CommitPersonalSiteSettings which is part of the UserProfileManager class (I had to use reflection to call this method but at least I'm not accessing the database directly).

I'm hoping that Microsoft will either make these methods and objects public or will create the ability to set these values via the existing properties. The core code is shown below (if using reflection scares you I'd suggest clicking away):

   1: /// <summary>
   2: /// Runs the specified command.
   3: /// </summary>
   4: /// <param name="command">The command.</param>
   5: /// <param name="keyValues">The key values.</param>
   6: /// <param name="output">The output.</param>
   7: /// <returns></returns>
   8: public override int Run(string command, StringDictionary keyValues, out string output)
   9: {
  10:  output = string.Empty;
  11:  
  12:  InitParameters(keyValues);
  13:  
  14:  string sspname = Params["sspname"].Value;
  15:  
  16:  ServerContext current = ServerContext.GetContext(sspname);
  17:  UserProfileManager upm = new UserProfileManager(current);
  18:  
  19:  SiteNameFormat nameFormat = upm.PersonalSiteFormat;
  20:  if (Params["nameformat"].UserTypedIn)
  21:   nameFormat = (SiteNameFormat)Enum.Parse(typeof(SiteNameFormat), Params["nameformat"].Value, true);
  22:  
  23:  string location = upm.PersonalSiteInclusion;
  24:  if (Params["location"].UserTypedIn)
  25:   location = Params["location"].Value;
  26:  
  27:  bool chooseLanguage = upm.IsPersonalSiteMultipleLanguage;
  28:  if (Params["chooselanguage"].UserTypedIn)
  29:   chooseLanguage = (Params["chooselanguage"].Value == "enable" ? true : false);
  30:  
  31:  string readers = upm.PersonalSiteReaders;
  32:  if (Params["readers"].UserTypedIn)
  33:   readers = Params["readers"].Value;
  34:  
  35:  CommitPersonalSiteSettings(upm, chooseLanguage, location, nameFormat, readers);
  36:  
  37:  bool modified = false;
  38:  
  39:  // UserProfileApplication userProfileApplication = current.UserProfileApplication;
  40:  System.Reflection.PropertyInfo userProfileApplicationProp = current.GetType().GetProperty("UserProfileApplication",
  41:             BindingFlags.NonPublic |
  42:             BindingFlags.Instance |
  43:             BindingFlags.InvokeMethod |
  44:             BindingFlags.GetProperty);
  45:  object userProfileApplication = userProfileApplicationProp.GetValue(current, null);
  46:    
  47:  // Set the search center url
  48:  if (Params["searchcenter"].UserTypedIn)
  49:  {
  50:   // userProfileApplication.SearchCenterUrl = Params["searchcenter"].Value;
  51:   System.Reflection.PropertyInfo searchCenterProp = userProfileApplication.GetType().GetProperty("SearchCenterUrl",
  52:                      BindingFlags.NonPublic |
  53:                      BindingFlags.Public |
  54:                      BindingFlags.Instance |
  55:                      BindingFlags.SetProperty |
  56:                      BindingFlags.FlattenHierarchy);
  57:   searchCenterProp.SetValue(userProfileApplication, Params["searchcenter"].Value, null);
  58:   modified = true;
  59:  }
  60:  if (Params["remotemysites"].UserTypedIn)
  61:  {
  62:   //userProfileApplication.EnablePersonalFeaturesForMultipleDeployments = Params["remotemysites"].Value;
  63:   System.Reflection.PropertyInfo remoteProp = userProfileApplication.GetType().GetProperty("EnablePersonalFeaturesForMultipleDeployments",
  64:                      BindingFlags.NonPublic |
  65:                      BindingFlags.Public |
  66:                      BindingFlags.Instance |
  67:                      BindingFlags.SetProperty |
  68:                      BindingFlags.FlattenHierarchy);
  69:   bool remoteMySites = (Params["remotemysites"].Value == "enable" ? true : false);
  70:   remoteProp.SetValue(userProfileApplication, remoteMySites, null);
  71:   modified = true;
  72:  }
  73:  
  74:  if (Params["provider"].UserTypedIn)
  75:  {
  76:   Uri uri = new Uri(Params["provider"].Value);
  77:   //userProfileApplication.MySitePortalUrl = uri.AbsoluteUri;
  78:   System.Reflection.PropertyInfo searchCenterProp = userProfileApplication.GetType().GetProperty("MySitePortalUrl",
  79:                    BindingFlags.NonPublic |
  80:                    BindingFlags.Public |
  81:                    BindingFlags.Instance |
  82:                    BindingFlags.SetProperty |
  83:                    BindingFlags.FlattenHierarchy);
  84:   searchCenterProp.SetValue(userProfileApplication, uri.AbsoluteUri, null);
  85:   modified = true;
  86:  }
  87:  
  88:  if (modified)
  89:  {
  90:   MethodInfo update =
  91:    userProfileApplication.GetType().GetMethod("Update",
  92:              BindingFlags.NonPublic |
  93:              BindingFlags.Public |
  94:              BindingFlags.Instance |
  95:              BindingFlags.InvokeMethod |
  96:              BindingFlags.FlattenHierarchy,
  97:              null,
  98:              new Type[] { }, null);
  99:   // userProfileApplication.Update(true);
 100:   update.Invoke(userProfileApplication, null);
 101:  
 102:   Console.WriteLine("The settings updated may require an iisreset before the changes are visible.");
 103:  }
 104:  
 105:  return 1;
 106: }
 107:  
 108:  
 109: #endregion
 110:  
 111: /// <summary>
 112: /// Commits the personal site settings.
 113: /// </summary>
 114: /// <param name="upm">The user profile manager.</param>
 115: /// <param name="chooseLanguage">if set to <c>true</c> the user may choose a language for their personal site.</param>
 116: /// <param name="location">The personal site location.</param>
 117: /// <param name="nameFormat">The site name format.</param>
 118: /// <param name="readers">The readers site groups (comma separated).</param>
 119: internal static void CommitPersonalSiteSettings(UserProfileManager upm, bool chooseLanguage, string location, SiteNameFormat nameFormat, string readers)
 120: {
 121:  MethodInfo commitPersonalSiteSettings =
 122:   upm.GetType().GetMethod("CommitPersonalSiteSettings",
 123:         BindingFlags.NonPublic | BindingFlags.Public |
 124:         BindingFlags.Instance | BindingFlags.InvokeMethod);
 125:  
 126:  commitPersonalSiteSettings.Invoke(upm,
 127:            new object[]
 128:             {
 129:              location, nameFormat, readers, chooseLanguage
 130:             });
 131: }
The syntax of the command I created to set the various properties can be seen below.

C:\>stsadm -help gl-mysitesettings

stsadm -o gl-mysitesettings

Sets the my site settings for the My Sites web application.

Parameters:
        -sspname <SSP name>
        [-nameformat <Username_CollisionError | Username_CollisionDomain | Domain_Username>]
        [-location <personal site location>]
        [-chooselanguage <enable | disable>]
        [-readers <comma separated list of site group readers>]
        [-searchcenter <preferred search center URL>]
        [-remotemysites <enable | disable>]
        [-provider <personal site provider URL (ex: "http://mysites")>]

Here’s an example of how to set all the properties:

stsadm -o gl-mysitesettings -sspname "SSP1" -nameformat username_collisiondomain -location "personal" -chooselanguage disable -readers "NT AUTHORITY\authenticated users" -searchcenter "http://intranet/searchcenter/pages" -remotemysites disable -provider "http://mysites"

Please note that because this command uses internal only classes and methods directly it could, according to Microsoft, put your environment into an un-supported state (though this is Microsoft's recommended approach over directly manipulating the database). Please make sure you understand what the command is doing and what your support options with Microsoft are.