Monday, May 11, 2009

CCNet : how to upgrade to a new version

I've been using CCNet for some years, and here is how I upgrade it :
  • stop all projects, or certainly the CI ones
  • stop ccnet service/console
  • take backup of the server and dashboard folders (zip them) just to be on the safe side
  • copy over the new binaries (including templates, xsl files, ...)
  • start ccnet service/console
  • start the projects again
I never do an install over an existing one, because in the past you would loose your ccnet.config file, which is a pain, and sometimes you lost a .state file, which is VERY painfull. We're in the process of making upgrades less painfull though.

Now if you have only a couple of CCNet projects, you can click stop on each of them, but if you got dozens, this will take some time. And since I'm into automation, I do not like to do repeatative jobs, these are sure to give errors one time. Unfortunately there is no built in mechanism to stop all projects, or a group of projects, but all the functionality exists. I took a look at CCTray, and after some time I figured it out. I'll save you the trouble, just copy the code below. I know, not the most clear code, but it is on the to-do list for a major refactor.
The code is a VB.Net console program, but perfectly usable :-)
Pass start or stop as argument, to start/stop projects not in de Deployment category.
Imports ThoughtWorks.CruiseControl
Imports ThoughtWorks.CruiseControl.Remote

Module Module1

Private BuildServerManager As CCTrayLib.Monitoring.ICruiseServerManager
Private Const BuildServerUrl As String = "tcp://buildserver:21234"

Sub Main()
Dim args As String()

Try
Dim StopProjects As Boolean

args = Environment.GetCommandLineArgs()
If args.Count <> 2 Then
Console.WriteLine("{0} State ", args(0))
Console.WriteLine(" example : {0} stop to stop all CI projects", args(0))
Console.WriteLine(" example : {0} start to start all CI projects", args(0))
Throw New Exception()
End If

StopProjects = args(1).ToLower = "stop"

BuildServerManager = CreateBuildServerManager()

Dim CruiseSnapShot As Remote.CruiseServerSnapshot = BuildServerManager.GetCruiseServerSnapshot

Dim CruiseManager As Remote.ICruiseManager

Dim z As New Remote.RemoteCruiseManagerFactory
CruiseManager = z.GetCruiseManager(BuildServerUrl)


For Each project As Remote.ProjectStatus In CruiseSnapShot.ProjectStatuses

If Not project.Category.StartsWith("Deployment") Then

If project.Activity = Remote.ProjectActivity.Sleeping AndAlso StopProjects Then
Console.WriteLine("Stopping {0} ...", project.Name)
CreateProjectManager(project.Name).StopProject()
End If

If project.Status = ProjectIntegratorState.Stopped AndAlso Not StopProjects Then
Console.WriteLine("Starting {0} ...", project.Name)
CreateProjectManager(project.Name).StartProject()
End If

End If
Next

Catch ex As Exception
Console.WriteLine(ex.ToString)

End Try

Console.ReadKey()

End Sub


Private Function CreateBuildServerManager() As CCTrayLib.Monitoring.ICruiseServerManager
Dim RemoteCruiseManagerFactory As Remote.RemoteCruiseManagerFactory = New Remote.RemoteCruiseManagerFactory
Dim Factory As CCTrayLib.Monitoring.CruiseServerManagerFactory = New CCTrayLib.Monitoring.CruiseServerManagerFactory(RemoteCruiseManagerFactory)
Dim Manager As CCTrayLib.Monitoring.ICruiseServerManager = Factory.Create(CreateBuildServer())

Return Manager

End Function


Private Function CreateProjectManager(ByVal projectName As String) As CCTrayLib.Monitoring.ICruiseProjectManager
Dim server = CreateBuildServer()
Dim remoteCruiseManagerFactory As ICruiseManagerFactory = New RemoteCruiseManagerFactory()
Dim factory As CCTrayLib.Monitoring.CruiseProjectManagerFactory = New CCTrayLib.Monitoring.CruiseProjectManagerFactory(remoteCruiseManagerFactory)
Dim projectConfig As CCTrayLib.Configuration.CCTrayProject = New CCTrayLib.Configuration.CCTrayProject(server, projectName)

Dim serverList As Collections.Generic.Dictionary(Of CCTrayLib.Configuration.BuildServer, CCTrayLib.Monitoring.ICruiseServerManager) = New Dictionary(Of CCTrayLib.Configuration.BuildServer, CCTrayLib.Monitoring.ICruiseServerManager)

serverList.Add(server, CreateBuildServerManager)

Dim manager As CCTrayLib.Monitoring.ICruiseProjectManager = factory.Create(projectConfig, serverList)

Return manager

End Function


Private Function CreateBuildServer() As CCTrayLib.Configuration.BuildServer
Dim serverName = BuildServerUrl.ToLower

If Not serverName.StartsWith("tcp://") Then
Throw New Exception("set up the url as remoting, url used : " + BuildServerUrl)
End If
serverName = serverName.Substring(6)

Return CCTrayLib.Configuration.BuildServer.BuildFromRemotingDisplayName(serverName)

End Function


End Module

Thursday, May 7, 2009

Customizing the code of CCNet : add validation

It is nice if you can inform a user that certain settings are not guaranteed to work. For example the project name : CompanyFramework that will pose no problems, but a name like my funky project in C# for scraping http://www.google.com could be a problem. Now I know this name WILL pose a problem, since the project name is also used for file names, folder names, links, ... as you can see the character / is not allowed in a file nor folder name. So how to solve this?
Options :
  • do nothing, let the program crash
  • replace invalid chars with valid ones, underscore or so
  • throw an exception, so the user must change the invalid name
  • inform the user that the name might cause problems

Option 1 : a no-go, problems must be fixed.
Option 2 : introduces other problems, because for one you have the possibility of 'renaming' a project into another existing one, and the user does not know.
Option 3 : that is a possible approach, but who decides what is a bad character? in 1990 a space was invalid for a file name (yep, I'm THAT old) and now it is not anymore
Option 4 : I think this is the best, you inform the user that a certain setting could pose a problem, but let the program continue. If it is a problem the user knows what to do to make it work, or should now ;-)

Now from version 1.5 onwards, CCNet has a neat validation system. It allows to set certain conditions as an error (throw exception type), or set as warning (just inform the user). And it shows in the log AND in the CCNetValidator program. How to use this neat system ? read on ...

I'll take the publisher from my previous posts as an example, and update it with the validation. Now this is a very basic publisher, but is perfect as a basic example.

The only thing needed to do is implement IConfigurationValidation which can be found in the config namespace : ThoughtWorks.CruiseControl.Core.Config.IConfigurationValidation
This will add a procedure :
public void Validate(IConfiguration configuration, object parent, Core.Config.IConfigurationErrorProcesser errorProcesser)
{

}

For showing a warning use : errorProcesser.ProcessWarning("This is a warning");
For showing an error, and prevent starting ccnet : errorProcesser.ProcessError("This is an error");
Very easy ! A bit more meaningful :

public void Validate(IConfiguration configuration, object parent, Core.Config.IConfigurationErrorProcesser errorProcesser)
{
long MB = 1000000;

if (System.IO.DriveInfo.GetDrives()[0].AvailableFreeSpace < 50 * MB)
errorProcesser.ProcessWarning("drive is getting low on space");
}


An even better idea for a validation is the following :

public virtual void Validate(IConfiguration configuration, object parent, IConfigurationErrorProcesser errorProcesser)
{
if (parent is Project)
{
Project parentProject = parent as Project;

// Attempt to find this publisher in the publishers section
bool isPublisher = false;
foreach (ITask task in parentProject.Publishers)
{
if (task == this)
{
isPublisher = true;
break;
}
}

// If not found then throw a validation exception
if (!isPublisher)
{
errorProcesser.ProcessWarning("This publishers is best placed in the publishers section of the configuration");
}
}
else
{
errorProcesser.ProcessError(
new CruiseControlException("This publisher can only belong to a project"));
}
}