19
REFACTORING YOUR CODE

Images

In the preceding chapter, you built a VM with a running SQL server using nothing besides an existing hypervisor, an operating system ISO file, and a little bit of code. Doing so meant linking together many of the functions you created in the previous chapters. Here, you’ll do something different: instead of adding new functionality to your PowerLab module, you’ll dig into your code and see if you can make your module a little more modular.

When I say modular, I’m talking about separating the functionality of the code into reusable functions that can handle many situations. The more modular the code, the more generally applicable it will be. And the more generally applicable your code, the more useful it will be. With modular code, you can reuse functions such as New-PowerLabVM or Install-PowerLabOperatingSystem to install many kinds of servers (which you’ll see in the next chapter).

A Second Look at New-PowerLabSqlServer

You created two main functions in Chapter 18: New-PowerLabSqlServer and Install-PowerLabSqlServer. You did so with the goal of setting up an SQL server. But what if you want to make your functions more generally applicable? After all, different servers share a lot of components with SQL ones: virtual machine, virtual disk, Windows OS, and so forth. You could simply copy the function you have and swap out all the specific SQL references for references to the server type you want.

But I’m going to have to advise against this. There’s no need for all that extra code. Instead, you’ll simply refactor your existing code. Refactoring refers to the process of changing a code’s insides without changing its functionality; in other words, refactoring is something for you, the programmer. It helps code be more readable, and it makes sure that you can keep growing your project without running into too many headache-inducing organizational issues.

Let’s start by taking a look at that New-PowerLabSqlServer function you created, shown in Listing 19-1.

function New-PowerLabSqlServer { 
    [CmdletBinding()] 
  param 
    ( 
        [Parameter(Mandatory)] 
        [string]$Name, 
 
        [Parameter(Mandatory)] 
        [pscredential]$DomainCredential, 
 
        [Parameter(Mandatory)] 
        [pscredential]$VMCredential, 
 
        [Parameter()] 
        [string]$VMPath = 'C:PowerLabVMs', 
 
        [Parameter()] 
        [int64]$Memory = 4GB, 
 
        [Parameter()] 
        [string]$Switch = 'PowerLab', 
 
        [Parameter()] 
        [int]$Generation = 2, 
 
        [Parameter()] 
        [string]$DomainName = 'powerlab.local', 
 
        [Parameter()] 
      [string]$AnswerFilePath = "C:Program FilesWindowsPowerShellModules
           PowerLabSqlServer.ini"
    ) 

  ## Build the VM 
    $vmparams = @{  
        Name       = $Name 
        Path       = $VmPath 
        Memory     = $Memory 
        Switch     = $Switch 
        Generation = $Generation 
    } 
    New-PowerLabVm @vmParams 
 
    Install-PowerLabOperatingSystem -VmName $Name 
    Start-VM -Name $Name 
 
    Wait-Server -Name $Name -Status Online -Credential $VMCredential 
 
  $addParams = @{ 
        DomainName = $DomainName 
        Credential = $DomainCredential 
        Restart    = $true 
        Force      = $true 
    } 
    Invoke-Command -VMName $Name -ScriptBlock { Add-Computer
    @using:addParams } -Credential $VMCredential 
 
    Wait-Server -Name $Name -Status Offline -Credential $VMCredential 
 
  Wait-Server -Name $Name -Status Online -Credential $DomainCredential 
 
    $tempFile = Copy-Item -Path $AnswerFilePath -Destination "C:Program
    FilesWindowsPowerShellModulesPowerLab	emp.ini" -PassThru 
     
    Install-PowerLabSqlServer -ComputerName $Name -AnswerFilePath $tempFile
    .FullName -DomainCredential $DomainCredential 
}

Listing 19-1: New-PowerLabSqlServer function

How would you go about refactoring this code? Well for starters, you know that every server needs a virtual machine, a virtual disk, and an operating system; you handle these needs in the code block between and .

If you look at this code, though, you’ll see that you can’t just pull it out and paste it into a new function. Parameters are defined in the New-PowerLabSqlServer function that you use in those lines. Notice that the only parameter that’s specific to SQL here is AnswerFilePath .

Now that you’ve identified the code that isn’t SQL specific, let’s pull it out and use it to create the new function New-PowerLabServer (Listing 19-2).

function New-PowerLabServer { 
    [CmdletBinding()] 
    param 
    ( 
        [Parameter(Mandatory)] 
        [string]$Name, 
 
        [Parameter(Mandatory)] 
        [pscredential]$DomainCredential, 
 
        [Parameter(Mandatory)] 
        [pscredential]$VMCredential, 
 
        [Parameter()] 
        [string]$VMPath = 'C:PowerLabVMs', 
 
        [Parameter()] 
        [int64]$Memory = 4GB, 
 
        [Parameter()] 
        [string]$Switch = 'PowerLab', 
 
        [Parameter()] 
        [int]$Generation = 2, 
 
        [Parameter()] 
        [string]$DomainName = 'powerlab.local' 
    ) 
 
    ## Build the VM 
    $vmparams = @{  
        Name       = $Name 
        Path       = $VmPath 
        Memory     = $Memory 
        Switch     = $Switch 
        Generation = $Generation 
    } 
    New-PowerLabVm @vmParams 
 
    Install-PowerLabOperatingSystem -VmName $Name 
    Start-VM -Name $Name 
 
    Wait-Server -Name $Name -Status Online -Credential $VMCredential 
 
    $addParams = @{ 
        DomainName = $DomainName 
        Credential = $DomainCredential 
        Restart    = $true 
        Force      = $true 
    } 
    Invoke-Command -VMName $Name
    -ScriptBlock { Add-Computer @using:addParams } -Credential $VMCredential 
 
    Wait-Server -Name $Name -Status Offline -Credential $VMCredential 
 
    Wait-Server -Name $Name -Status Online -Credential $DomainCredential 
}

Listing 19-2: A more generic New-PowerLabServer function

At this point, you have a general server-provisioning function, but no way to indicate the kind of server you’re creating. Let’s fix that by using another parameter called ServerType:

[Parameter(Mandatory)] 
[ValidateSet('SQL', 'Web', 'Generic')] 
[string]$ServerType

Notice the new ValidateSet parameter. I’ll give an in-depth explanation of what this does later in the chapter; for now, you just need to know that this ensures that the user can pass in only a server type contained within this set.

Now that you have this parameter, let’s use it. Insert a switch statement at the end of the function to execute different code depending on which server type the user enters:

switch ($ServerType) { 
    'Web' { 
        Write-Host 'Web server deployments are not supported at this time' 
        break 
    } 
    'SQL' { 
        $tempFile = Copy-Item -Path $AnswerFilePath -Destination "C:Program
        FilesWindowsPowerShellModulesPowerLab	emp.ini" -PassThru 
        Install-PowerLabSqlServer -ComputerName $Name -AnswerFilePath
        $tempFile.FullName -DomainCredential $DomainCredential 
        break 
    } 
    'Generic' { 
        break 
    } 
  default { 
        throw "Unrecognized server type: [$_]" 
    } 
}

As you can see, you handle the three types of server input (and use the default case to handle any exceptions ). But there’s a problem. To fill out the SQL code, you copied and pasted code from the New-PowerLabSqlServer function, and now you’re using something you don’t have: the AnswerFilePath variable. Recall that when you moved your generic code to a new function, you left this variable behind, meaning that you can’t use it here . . . or can you?

Using Parameter Sets

In situations like the preceding one, when you have one parameter that determines which other parameter you need, PowerShell has a handy feature called parameter sets. You can think of parameter sets as letting you use conditional logic to control which parameters a user inputs.

In this example, you’ll use three parameter sets: a set for provisioning SQL servers, a set for provisioning web servers, and a default set.

You can define parameter sets by using the ParameterSetName attribute followed by a name. Here’s an example:

[Parameter(Mandatory)] 
[ValidateSet('SQL', 'Web', 'Generic')] 
[string]$ServerType, 
 
[Parameter(ParameterSetName = 'SQL')] 
[string]$AnswerFilePath = "C:Program FilesWindowsPowerShellModulesPowerLabSqlServer.ini", 
 
[Parameter(ParameterSetName = 'Web')] 
[switch]$NoDefaultWebsite

Notice that you haven’t assigned ServerType a parameter set. Parameters that are not part of a parameter set can be used with any set. Because of this, you can use ServerType with either AnswerFilePath or the newly created parameter you’ll be using for web server provisioning: CreateDefaultWebsite.

You can see here that the majority of the parameters stay the same, but you add a final one based on what you pass in for ServerType:

PS> New-PowerLabServer -Name WEBSRV -DomainCredential CredentialHere -VMCredential CredentialHere 
-ServerType 'Web' -NoDefaultWebsite 
PS> New-PowerLabServer -Name SQLSRV -DomainCredential CredentialHere -VMCredential CredentialHere 
-ServerType 'SQL' -AnswerFilePath 'C:OverridingTheDefaultPathSqlServer.ini'

If you try to mix and match, and use parameters from two different parameter sets at the same time, you’ll fail:

PS> New-PowerLabServer -Name SQLSRV -DomainCredential CredentialHere -VMCredential CredentialHere 
-ServerType 'SQL' -NoDefaultWebsite -AnswerFilePath 'C:OverridingTheDefaultPathSqlServer.ini'
 
New-PowerLabServer : Parameter set cannot be resolved using the specified named parameters. 
At line:1 char:1 
+ New-PowerLabServer -Name SQLSRV -ServerType 'SQL' -NoDefaultWebsite - ... 
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
    + CategoryInfo          : InvalidArgument: (:) [New-PowerLabServer], ParameterBindingException 
    + FullyQualifiedErrorId : AmbiguousParameterSet,New-PowerLabServer

What would happen if you did the opposite and used neither the NoDefaultWebsite parameter nor the AnswerFilePath parameter?

PS> New-PowerLabServer -Name SQLSRV -DomainCredential CredentialHere -VMCredential CredentialHere
-ServerType 'SQL' 
New-PowerLabServer : Parameter set cannot be resolved using the specified named parameters. 
At line:1 char:1 
+ New-PowerLabServer -Name SQLSRV -DomainCredential $credential... 
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
    + CategoryInfo          : InvalidArgument: (:) [New-PowerLabServer], ParameterBindingException
    + FullyQualifiedErrorId : AmbiguousParameterSet,New-PowerLabServer
PS> New-PowerLabServer -Name WEBSRV -DomainCredential CredentialHere -VMCredential CredentialHere 
-ServerType 'Web'
New-PowerLabServer : Parameter set cannot be resolved using the specified named parameters. 
At line:1 char:1 
+ New-PowerLabServer -Name WEBSRV -DomainCredential $credential... 
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
    + CategoryInfo          : InvalidArgument: (:) [New-PowerLabServer], ParameterBindingException
    + FullyQualifiedErrorId : AmbiguousParameterSet,New-PowerLabServer

You get the same error about not being able to resolve the parameter set as before. Why? PowerShell doesn’t know which parameter set to use! Earlier, I said you’d be using three sets, but you defined only two. You need to set a default parameter set. As you saw earlier, parameters that are not explicitly assigned to a parameter set can be used in conjunction with any in a set. However, if you do define a default parameter set, PowerShell will use those parameters if no parameters in any set are being used.

As for your default set, you could pick the defined SQL or web parameter set to be your default, or you could simply define a nonspecific parameter set like blah blah, which would create an inherent set for all parameters that do not have an explicit set defined:

[CmdletBinding(DefaultParameterSetName = 'blah blah')]

If you don’t want to set a defined parameter set as default, you can set it to anything, and PowerShell will ignore both parameter sets if no parameter in a parameter set is used. This is what you need to do in this case; it’s perfectly okay to not use a defined parameter set because you have the ServerType parameter to indicate whether or not you’re going to deploy a web server or SQL server.

With your new parameter sets, the parameter portion of New-PowerLabServer looks like Listing 19-3.

function New-PowerLabServer { 
    [CmdletBinding(DefaultParameterSetName = 'Generic')] 
    param 
    ( 
        [Parameter(Mandatory)] 
        [string]$Name, 
 
        [Parameter(Mandatory)] 
        [pscredential]$DomainCredential, 
 
        [Parameter(Mandatory)] 
        [pscredential]$VMCredential, 
 
        [Parameter()] 
        [string]$VMPath = 'C:PowerLabVMs', 
 
        [Parameter()] 
        [int64]$Memory = 4GB, 
 
        [Parameter()] 
        [string]$Switch = 'PowerLab', 
        [Parameter()]
        [int]$Generation = 2, 
 
        [Parameter()] 
        [string]$DomainName = 'powerlab.local', 
 
        [Parameter()] 
        [ValidateSet('SQL', 'Web')] 
        [string]$ServerType, 
 
        [Parameter(ParameterSetName = 'SQL')] 
        [string]$AnswerFilePath = "C:Program FilesWindowsPowerShellModules
        PowerLabSqlServer.ini",
 
        [Parameter(ParameterSetName = 'Web')] 
        [switch]$NoDefaultWebsite 
    )

Listing 19-3: The new New-PowerLabServer function

Notice that you have a reference to the function Install-PowerLabSqlServer. This looks similar to the function (New-PowerLabSqlServer) that got us into this mess. Instead of creating the virtual machine and installing the operating system, Install-PowerLabSqlServer takes over from New-PowerLabServer, installs the SQL server software, and performs basic configuration. You might be inclined to perform this same round of refactoring on this function. You could do this, but as soon as you look at the code that’s inside Install-PowerLabSqlServer, you’ll soon realize there are nearly no commonalities between the installation phase of SQL server and that of other types of servers. It’s a unique process and would be hard to “genericize” for other server deployments.

Summary

Well, now that the code is nice and refactored, you’re left with a function capable of . . . provisioning a SQL server. So back where you started, right? I hope not! Even though you haven’t changed anything about the functionality of the code, you’ve built the foundation you need to easily insert the code for creating a web server (which you’ll do in the next chapter).

As you saw in this chapter, refactoring PowerShell code isn’t a cut-and-dried process. Knowing the ways you can refactor your code, and which of those ways is the best for your present situation, is a skill that comes with experience. But as long as you keep what programmers call the DRY principle (don’t repeat yourself) in mind, you’ll be on the right path. More than anything, abiding by DRY means avoiding duplicate code and redundant functionality. You saw this in this chapter when you chose to create a general function that created new servers, as opposed to another New-PowerLabInsertServerTypeHereServer function.

Your hard work wasn’t for nothing. In the next chapter, you’ll get back to automating, adding the code you need to create IIS web servers.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.121.160