Modified: vcl/trunk/managementnode/lib/VCL/Module/OS/Windows.pm
URL: 
http://svn.apache.org/viewvc/vcl/trunk/managementnode/lib/VCL/Module/OS/Windows.pm?rev=1781479&r1=1781478&r2=1781479&view=diff
==============================================================================
--- vcl/trunk/managementnode/lib/VCL/Module/OS/Windows.pm (original)
+++ vcl/trunk/managementnode/lib/VCL/Module/OS/Windows.pm Thu Feb  2 22:41:33 
2017
@@ -51,12 +51,15 @@ use 5.008000;
 use strict;
 use warnings;
 use diagnostics;
+
+use Encode;
 use English '-no_match_vars';
-use VCL::utils;
 use File::Basename;
+use MIME::Base64;
 use Net::Netmask;
 use Text::CSV_XS;
-use IO::String;
+
+use VCL::utils;
 
 ##############################################################################
 
@@ -415,6 +418,19 @@ sub pre_capture {
 
 =item *
 
+ If computer is part of Active Directory Domain, unjoin it
+
+=cut
+
+       if ($self->ad_get_current_domain()) {
+               if (!$self->ad_unjoin()) {
+                       notify($ERRORS{'WARNING'}, 0, "failed to remove 
computer from Active Directory domain");
+                       return 0;
+               }
+       }
+
+=item *
+
  Set root as the owner of /home/root
 
 =cut
@@ -459,27 +475,6 @@ sub pre_capture {
 
 =item *
 
- Disable ntsyslog service if it exists on the computer - it can prevent Cygwin 
sshd from working
-
-=cut
-
-       if ($self->service_exists('ntsyslog') && 
!$self->set_service_startup_mode('ntsyslog', 'disabled')) {
-               notify($ERRORS{'WARNING'}, 0, "unable to set ntsyslog service 
startup mode to disabled");
-               return 0;
-       }
-
-=item *
-
- Disable dynamic DNS
-
-=cut
-
-       if (!$self->disable_dynamic_dns()) {
-               notify($ERRORS{'WARNING'}, 0, "unable to disable dynamic dns");
-       }
-
-=item *
-
  Disable Shutdown Event Tracker
 
 =cut
@@ -724,6 +719,8 @@ sub post_load {
        }
        
        my $computer_node_name = $self->data->get_computer_node_name();
+       my $imagedomain_domaindnsname = 
$self->data->get_image_domain_dns_name(0);
+       
        my $node_configuration_directory = 
$self->get_node_configuration_directory();
        
        notify($ERRORS{'OK'}, 0, "beginning Windows post-load tasks on 
$computer_node_name");
@@ -897,26 +894,6 @@ sub post_load {
                return 0;
        }
 
-#=item *
-#
-#Disable NetBIOS
-#
-#=cut
-#
-#      if (!$self->disable_netbios()) {
-#              notify($ERRORS{'WARNING'}, 0, "failed to disable NetBIOS");
-#      }
-
-#=item *
-#
-#Disable dynamic DNS
-#
-#=cut
-#
-#      if (!$self->disable_dynamic_dns()) {
-#              notify($ERRORS{'WARNING'}, 0, "failed to disable dynamic DNS");
-#      }
-
 =item *
 
  Remove the Windows root password and other private information from the VCL 
configuration files
@@ -972,40 +949,42 @@ sub post_load {
        if (!$self->install_updates()) {
                notify($ERRORS{'WARNING'}, 0, "failed to run custom post_load 
scripts");
        }
-
+       
 =item *
 
- Set the computer name if the imagemeta.sethostname flag is true
+ Join Active Directory domain if configured for image
 
 =cut
 
-       if ($self->data->get_imagemeta_sethostname(0)) {
-               $self->set_computer_hostname();
+       if ($imagedomain_domaindnsname) {
+               if (!$self->ad_join()) {
+                       notify($ERRORS{'WARNING'}, 0, "failed to join Active 
Directory domain");
+                       return 0;
+               }
+       }
+       elsif ($self->data->get_imagemeta_sethostname(0)) {
+               # Image configured to set hostname
+               if (!$self->set_computer_hostname()) {
+                       notify($ERRORS{'WARNING'}, 0, "failed to rename 
computer");
+                       return 0;
+               }
+               push @{$self->{reboot_required}}, 'computer was renamed';
        }
 
 =item *
 
- Check if the imagemeta postoption is set to reboot, reboot if necessary
+ Reboot the computer if necessary
 
 =cut
-
-       if ($self->data->get_imagemeta_postoption() =~ /reboot/i) {
-               notify($ERRORS{'OK'}, 0, "imagemeta postoption reboot is set 
for image, rebooting computer");
-               
-               # Create a scheduled task to run post_load.cmd when the image 
boots
-               my $task_command = 
"$node_configuration_directory/Scripts/update_cygwin.cmd >> 
$node_configuration_directory/Logs/update_cygwin.log";
-               if ($self->create_startup_scheduled_task('VCL Update Cygwin', 
$task_command, 'root', $root_random_password)) {
-                       if (!$self->reboot('', '', '', 0)) {
-                               notify($ERRORS{'WARNING'}, 0, "failed to reboot 
the computer");
-                               return 0;
-                       }
-               }
-               else {
-                       notify($ERRORS{'WARNING'}, 0, "computer not rebooted, 
failed to create startup scheduled task to executed update_cygwin.cmd, computer 
might not have responded after reboot");
-                       $self->data->set_imagemeta_sethostname(0);
-                       $self->data->set_imagemeta_postoption('');
+       
+       if ($self->{reboot_required}) {
+               notify($ERRORS{'DEBUG'}, 0, "attempting to reboot computer, 
reasons why necessary:\n" . join("\n", @{$self->{reboot_required}}));
+               if (!$self->reboot()) {
+                       notify($ERRORS{'WARNING'}, 0, "failed to reboot after 
renaming computer");
                }
+               delete $self->{reboot_required};
        }
+       
 
 =back
 
@@ -1159,7 +1138,7 @@ sub sanitize {
                notify($ERRORS{'WARNING'}, 0, "failed to delete users from 
$computer_node_name");
                return 0;
        }
-
+       
        notify($ERRORS{'OK'}, 0, "$computer_node_name has been sanitized");
        return 1;
 } ## end sub sanitize
@@ -1852,6 +1831,7 @@ sub create_user {
        
        my $computer_node_name = $self->data->get_computer_node_name();
        my $system32_path = $self->get_system32_path() || return;
+       my $domain_dns_name = $self->data->get_image_domain_dns_name();
        
        my $user_parameters = shift;
        if (!$user_parameters) {
@@ -1869,40 +1849,50 @@ sub create_user {
                return;
        }
        
-       my $password = $user_parameters->{password};
-       if (!defined($password)) {
-               notify($ERRORS{'WARNING'}, 0, "failed to create user on 
$computer_node_name, argument hash does not contain a 'password' key:\n" . 
format_data($user_parameters));
-               return;
-       }
-       
        my $root_access = $user_parameters->{root_access};
        if (!defined($root_access)) {
                notify($ERRORS{'WARNING'}, 0, "failed to create user on 
$computer_node_name, argument hash does not contain a 'root_access' key:\n" . 
format_data($user_parameters));
                return;
        }
        
-       # Check if user already exists
-       if (!$self->user_exists($username)) {
-               # Attempt to create the user account
-               my $add_user_command = "$system32_path/net.exe user 
\"$username\" \"$password\" /ADD /EXPIRES:NEVER /COMMENT:\"Account created by 
VCL\"";
-               my ($add_user_exit_status, $add_user_output) = 
$self->execute($add_user_command, 0);
-               if (!defined($add_user_output)) {
-                       notify($ERRORS{'WARNING'}, 0, "failed to execute 
command create user on $computer_node_name: $username");
+       my $password = $user_parameters->{password};
+       
+       # Check if image is configured for Active Directory and a password 
should NOT be set
+       # OS.pm::add_user_accounts should have already called 
should_set_user_password which checks if AD is configured and if user exists in 
AD
+       # If user exists in AD, password argument should not be set
+       # If for some reason it is set, add local user account
+       if ($domain_dns_name && !$password) {
+               $username .= "@" . $domain_dns_name;
+       }
+       else {
+               if (!defined($password) && !$domain_dns_name) {
+                       notify($ERRORS{'WARNING'}, 0, "failed to create user on 
$computer_node_name, argument hash does not contain a 'password' key:\n" . 
format_data($user_parameters));
                        return;
                }
-               elsif ($add_user_exit_status == 0) {
-                       notify($ERRORS{'OK'}, 0, "created user on 
$computer_node_name: $username, password: $password");
+               
+               # Not an AD image, check if user already exists
+               if (!$self->user_exists($username)) {
+                       # Attempt to create the user account
+                       my $add_user_command = "$system32_path/net.exe user 
\"$username\" \"$password\" /ADD /EXPIRES:NEVER /COMMENT:\"Account created by 
VCL\"";
+                       my ($add_user_exit_status, $add_user_output) = 
$self->execute($add_user_command, 0);
+                       if (!defined($add_user_output)) {
+                               notify($ERRORS{'WARNING'}, 0, "failed to 
execute command create user on $computer_node_name: $username");
+                               return;
+                       }
+                       elsif ($add_user_exit_status == 0) {
+                               notify($ERRORS{'OK'}, 0, "created user on 
$computer_node_name: $username, password: $password");
+                       }
+                       else {
+                               notify($ERRORS{'WARNING'}, 0, "failed to create 
user on $computer_node_name: $username, exit status: $add_user_exit_status, 
output:\n" . join("\n", @$add_user_output));
+                               return 0;
+                       }
                }
                else {
-                       notify($ERRORS{'WARNING'}, 0, "failed to create user on 
$computer_node_name: $username, exit status: $add_user_exit_status, output:\n" 
. join("\n", @$add_user_output));
-                       return 0;
-               }
-       }
-       else {
-               # Account already exists on machine, set password
-               if (!$self->set_password($username, $password)) {
-                       notify($ERRORS{'WARNING'}, 0, "failed to set password 
of existing user on $computer_node_name: $username");
-                       return;
+                       # Account already exists on machine, set password
+                       if (!$self->set_password($username, $password)) {
+                               notify($ERRORS{'WARNING'}, 0, "failed to set 
password of existing user on $computer_node_name: $username");
+                               return;
+                       }
                }
        }
        
@@ -1986,6 +1976,146 @@ sub add_user_to_group {
 
 #/////////////////////////////////////////////////////////////////////////////
 
+=head2 remove_user_from_group
+
+ Parameters  : $username, $group
+ Returns     : boolean
+ Description : Removes a user from a local group on the computer. If an AD user
+               account and local account exist with the same name, both will be
+               removed.
+
+=cut
+
+sub remove_user_from_group {
+       my $self = shift;
+       if (ref($self) !~ /windows/i) {
+               notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
+               return;
+       }
+       
+       my $computer_name = $self->data->get_computer_node_name();
+       my $system32_path = $self->get_system32_path() || return;
+
+       my $username = shift;
+       if (!defined($username)) {
+               notify($ERRORS{'WARNING'}, 0, "username argument was not 
supplied");
+               return;
+       }
+       
+       my $group = shift;
+       if (!defined($group)) {
+               notify($ERRORS{'WARNING'}, 0, "local group name argument was 
not supplied");
+               return;
+       }
+       
+       my @group_members = $self->get_group_members($group);
+       if (!@group_members) {
+               notify($ERRORS{'DEBUG'}, 0, "$username not removed from $group 
local group on $computer_name, group is either empty or membership could not be 
retrieved");
+               return 1;
+       }
+       
+       my @matching_members = grep(/(^|\\)$username$/i, @group_members);
+       if (!@matching_members) {
+               notify($ERRORS{'OK'}, 0, "$username is not a member of $group 
local group on $computer_name");
+               return 1;
+       }
+       for my $matching_member (@matching_members) {
+               # Escape backslashes in domain usernames
+               $matching_member =~ s/\\/\\\\/;
+               my $command = "$system32_path/net.exe localgroup \"$group\" 
\"$matching_member\" /DELETE";
+               my ($exit_status, $output) = $self->execute($command);
+               if (!defined($output)) {
+                       notify($ERRORS{'WARNING'}, 0, "failed to execute 
command to remove $matching_member from $group local group on $computer_name: 
$command");
+                       return;
+               }
+               elsif (grep(/no such/, @$output)) {
+                       # There is no such global user or group: admin.
+                       notify($ERRORS{'OK'}, 0, "$matching_member is not a 
member of $group local group on $computer_name");
+                       return 1;
+               }
+               elsif ($exit_status ne '0') {
+                       notify($ERRORS{'WARNING'}, 0, "failed to remove 
$matching_member from $group local group on $computer_name, exit status: 
$exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
+                       return 0;
+               }
+               else {
+                       notify($ERRORS{'OK'}, 0, "removed $matching_member from 
$group local group on $computer_name");         
+               }
+       }
+       return 1;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 get_group_members
+
+ Parameters  : $group_name
+ Returns     : array
+ Description : Retrieves the names of users who are members of a local Windows
+               group.
+
+=cut
+
+sub get_group_members {
+       my $self = shift;
+       if (ref($self) !~ /windows/i) {
+               notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
+               return;
+       }
+       
+       my $computer_name = $self->data->get_computer_node_name();
+       my $system32_path = $self->get_system32_path() || return;
+       
+       my $group = shift;
+       if (!defined($group)) {
+               notify($ERRORS{'WARNING'}, 0, "local group name argument was 
not supplied");
+               return;
+       }
+       
+       my $command = "$system32_path/net.exe localgroup \"$group\"";
+       my ($exit_status, $output) = $self->execute($command);
+       if (!defined($output)) {
+               notify($ERRORS{'WARNING'}, 0, "failed to execute command to 
retrieve members of $group local group on $computer_name: $command");
+               return;
+       }
+       elsif ($exit_status ne '0') {
+               notify($ERRORS{'WARNING'}, 0, "failed to retrieve members of 
$group local group on $computer_name, exit status: $exit_status, command: 
'$command', output:\n" . join("\n", @$output));
+               return 0;
+       }
+       
+       # Alias name     Remote Desktop Users
+       # Comment        Members in this group are granted the right to logon 
remotely
+       #
+       # Members
+       #
+       # 
-------------------------------------------------------------------------------
+       # AD\admin
+       # admin
+       # AD\domainuser
+       # admin
+       # tester1
+       # ...
+       # test100
+       # The command completed successfully.
+       my @group_members;
+       my $separator_line_found = 0;
+       for my $line (@$output) {
+               if (!$separator_line_found) {
+                       if ($line =~ /---/) {
+                               $separator_line_found = 1;
+                       }
+                       next;
+               }
+               elsif ($line =~ /The command/) {
+                       last;
+               }
+               push @group_members, $line;
+       }
+       notify($ERRORS{'OK'}, 0, "retrieve members of $group local group on 
$computer_name: " . join(", ", @group_members));
+       return @group_members;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
 =head2 delete_user
 
  Parameters  : $node, $user, $type, $osname
@@ -2021,6 +2151,10 @@ sub delete_user {
        }
        elsif (defined($delete_user_exit_status) && $delete_user_exit_status == 
2) {
                notify($ERRORS{'OK'}, 0, "user $username was not deleted 
because user does not exist");
+               
+               # Could be an AD domain user, make sure user is removed from 
groups
+               $self->remove_user_from_group($username, 'Administrators');
+               $self->remove_user_from_group($username, 'Remote Desktop 
Users');
        }
        elsif (defined($delete_user_exit_status)) {
                notify($ERRORS{'WARNING'}, 0, "failed to delete user $username 
from $computer_node_name, exit status: $delete_user_exit_status, 
output:\n@{$delete_user_output}");
@@ -2133,6 +2267,63 @@ sub set_password {
 
 #/////////////////////////////////////////////////////////////////////////////
 
+=head2 should_set_user_password
+
+ Parameters  : $user_id
+ Returns     : boolean
+ Description : Determines if a random password should be set for a user. This 
is
+               the default behavior. A random password will not be set if:
+                                       * The image is configured for Active 
Directory
+                                       * The user exists in the domain
+
+=cut
+
+sub should_set_user_password {
+       my $self = shift;
+       if (ref($self) !~ /VCL::Module/i) {
+               notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
+               return;
+       }
+       
+       my ($user_id) = shift;
+       if (!$user_id) {
+               notify($ERRORS{'WARNING'}, 0, "user ID argument was not 
supplied");
+               return;
+       }
+       
+       if (defined($self->{should_set_user_password}{$user_id})) {
+               return $self->{should_set_user_password}{$user_id};
+       }
+       
+       # Check if image is configured for Active Directory
+       my $domain_dns_name = $self->data->get_image_domain_dns_name();
+       if ($domain_dns_name) {
+               my $user_info = get_user_info($user_id);
+               if (!$user_info) {
+                       notify($ERRORS{'WARNING'}, 0, "unable to determine if 
user password should be set, user info could not be retrieved for user ID 
$user_id");
+                       return;
+               }
+               
+               my $username = $user_info->{unityid};
+               if ($self->ad_user_exists($username)) {
+                       $self->{should_set_user_password}{$user_id} = 0;
+                       notify($ERRORS{'DEBUG'}, 0, "verified user exists in 
$domain_dns_name Active Directory domain: $username (ID: $user_id), random 
password will NOT be set for user");
+               }
+               else {
+                       $self->{should_set_user_password}{$user_id} = 1;
+                       notify($ERRORS{'WARNING'}, 0, "could not verify user 
exists in $domain_dns_name Active Directory domain: $username (ID: $user_id), 
random password will be set");
+               }
+       }
+       else {
+               # Not configured for Active Directory, random password should 
be set
+               $self->{should_set_user_password}{$user_id} = 1;
+       }
+       
+       return $self->{should_set_user_password}{$user_id};
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
 =head2 enable_user
 
  Parameters  : $username (optional
@@ -3377,6 +3568,51 @@ sub create_startup_scheduled_task {
 
 #/////////////////////////////////////////////////////////////////////////////
 
+=head2 create_update_cygwin_startup_scheduled_task
+
+ Parameters  : none
+ Returns     : boolean
+ Description : Creates a scheduled task that runs on startup named 'VCL Update
+               Cygwin' which runs update_cygwin.cmd as root.
+
+=cut
+
+sub create_update_cygwin_startup_scheduled_task {
+       my $self = shift;
+       if (ref($self) !~ /windows/i) {
+               notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
+               return;
+       }
+       
+       # Avoid doing this more than once
+       if ($self->{created_update_cygwin_startup_scheduled_task}) {
+               return 1;
+       }
+       
+       my $root_password = $self->{root_password};
+       if (!$root_password) {
+               $root_password = getpw();
+               $self->{root_password} = $root_password;
+               if (!$self->set_password('root', $root_password)) {
+                       notify($ERRORS{'WARNING'}, 0, "unable to create startup 
scheduled task to update Cygwin, failed to set root password");
+                       return;
+               }
+       }
+       
+       # Create a scheduled task to run post_load.cmd when the image boots
+       my $node_configuration_directory = 
$self->get_node_configuration_directory();
+       my $task_command = 
"$node_configuration_directory/Scripts/update_cygwin.cmd >> 
$node_configuration_directory/Logs/update_cygwin.log";
+       if ($self->create_startup_scheduled_task('VCL Update Cygwin', 
$task_command, 'root', $root_password)) {
+               $self->{created_update_cygwin_startup_scheduled_task} = 1;
+               return 1;
+       }
+       else {
+               return 0;
+       }
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
 =head2 enable_autoadminlogon
 
  Parameters  :
@@ -3555,13 +3791,13 @@ sub reboot {
                                notify($ERRORS{'WARNING'}, 0, "reboot not 
attempted, failed to enable ssh from private IP addresses");
                                return 0;
                        }
-       
+                       
                        # Set sshd service startup mode to auto
                        if (!$self->set_service_startup_mode('sshd', 'auto')) {
                                notify($ERRORS{'WARNING'}, 0, "reboot not 
attempted, unable to set sshd service startup mode to auto");
                                return 0;
                        }
-       
+                       
                        # Make sure ping access is enabled from private IP 
addresses
                        if (!$self->firewall_enable_ping_private()) {
                                notify($ERRORS{'WARNING'}, 0, "reboot not 
attempted, failed to enable ping from private IP addresses");
@@ -3570,6 +3806,9 @@ sub reboot {
                        
                        # Kill the screen saver process, it occasionally 
prevents reboots and shutdowns from working
                        $self->kill_process('logon.scr');
+                       
+                       # Make sure update_cygwin.cmd runs after the computer 
is rebooted with the new hostname
+                       $self->create_update_cygwin_startup_scheduled_task();
                }
                
                # Delete cached network configuration information so it is 
retrieved next time it is needed
@@ -3629,6 +3868,13 @@ sub reboot {
        if ($result) {
                # Reboot was successful, calculate how long reboot took
                notify($ERRORS{'OK'}, 0, "reboot complete on 
$computer_node_name, took $reboot_duration seconds");
+               
+               # Clear any previous reboot_required reasons to prevent 
unnecessary reboots
+               delete $self->{reboot_required};
+               
+               # Clear any imagemeta postoption reboot flag
+               $self->data->set_imagemeta_postoption('');
+               
                return 1;
        }
        else {
@@ -4247,41 +4493,50 @@ sub get_scheduled_task_info {
 
 #/////////////////////////////////////////////////////////////////////////////
 
-=head2 disable_dynamic_dns
+=head2 enable_dynamic_dns
 
- Parameters  :
+ Parameters  : $interface (private or public or both)
  Returns     :
  Description :
 
 =cut
 
-sub disable_dynamic_dns {
+sub enable_dynamic_dns {
        my $self = shift;
        if (ref($self) !~ /windows/i) {
                notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
                return;
        }
-
-       my $system32_path = $self->get_system32_path() || return;
+       
+       my $interface = shift;
+       
+       if (!defined($interface)) {
+               notify($ERRORS{'OK'}, 0, "interface not specified for function 
enable_dynamic_dns defaulting to public interface");
+               $interface = 'public'
+       }
+       
+       my $management_node_keys = $self->data->get_management_node_keys();
+       my $computer_node_name   = $self->data->get_computer_node_name();
+       my $system32_path        = $self->get_system32_path() || return;
        
        my $registry_string .= <<"EOF";
 Windows Registry Editor Version 5.00
 
-; This file disables dynamic DNS updates
+; This file enables dynamic DNS updates
 
 [HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters]
-"DisableDynamicUpdate"=dword:00000001
+"DisableDynamicUpdate"=dword:00000000
 
 [HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters]
-"DisableReverseAddressRegistrations"=dword:00000001
+"DisableReverseAddressRegistrations"=dword:00000000
 EOF
 
        # Import the string into the registry
        if ($self->import_registry_string($registry_string)) {
-               notify($ERRORS{'OK'}, 0, "disabled dynamic dns");
+               notify($ERRORS{'OK'}, 0, "enabled dynamic dns");
        }
        else {
-               notify($ERRORS{'WARNING'}, 0, "failed to disable dynamic dns");
+               notify($ERRORS{'WARNING'}, 0, "failed to enable dynamic dns");
                return;
        }
 
@@ -4298,38 +4553,137 @@ EOF
        
        # Assemble netsh.exe commands to disable DNS registration
        my $netsh_command;
-       $netsh_command .= "$system32_path/netsh.exe interface ip set dns";
-       $netsh_command .= " name = \"$public_interface_name\"";
-       $netsh_command .= " source = dhcp";
-       $netsh_command .= " register = none";
-       $netsh_command .= " ;";
        
-       $netsh_command .= "$system32_path/netsh.exe interface ip set dns";
-       $netsh_command .= " name = \"$private_interface_name\"";
-       $netsh_command .= " source = dhcp";
-       $netsh_command .= " register = none";
-       $netsh_command .= " ;";
+       if ($interface eq 'public') {
+               $netsh_command .= "$system32_path/netsh.exe interface ip set 
dns";
+               $netsh_command .= " name = \"$public_interface_name\"";
+               $netsh_command .= " source = dhcp";
+               $netsh_command .= " register = both";
+               $netsh_command .= " ;";
+       }
+       elsif ($interface eq 'private') {
+               $netsh_command .= "$system32_path/netsh.exe interface ip set 
dns";
+               $netsh_command .= " name = \"$private_interface_name\"";
+               $netsh_command .= " source = dhcp";
+               $netsh_command .= " register = both";
+               $netsh_command .= " ;";
+       }
+       else {
+               $netsh_command .= "$system32_path/netsh.exe interface ip set 
dns";
+               $netsh_command .= " name = \"$public_interface_name\"";
+               $netsh_command .= " source = dhcp";
+               $netsh_command .= " register = both";
+               $netsh_command .= " ;";
+               
+               $netsh_command .= "$system32_path/netsh.exe interface ip set 
dns";
+               $netsh_command .= " name = \"$private_interface_name\"";
+               $netsh_command .= " source = dhcp";
+               $netsh_command .= " register = both";
+               $netsh_command .= " ;";
+       }
        
        # Execute the netsh.exe command
-       my ($netsh_exit_status, $netsh_output) = $self->execute($netsh_command);
+       my ($netsh_exit_status, $netsh_output) = 
run_ssh_command($computer_node_name, $management_node_keys, $netsh_command);
        if (defined($netsh_exit_status)  && $netsh_exit_status == 0) {
-               notify($ERRORS{'OK'}, 0, "disabled dynamic DNS registration on 
public and private adapters");
+               notify($ERRORS{'OK'}, 0, "enabled dynamic DNS registration on 
$interface adapters");
        }
        elsif (defined($netsh_exit_status)) {
-               notify($ERRORS{'WARNING'}, 0, "failed to disable dynamic DNS 
registration on public and private adapters, exit status: $netsh_exit_status, 
output:\n@{$netsh_output}");
+               notify($ERRORS{'WARNING'}, 0, "failed to enable dynamic DNS 
registration on $interface adapters, exit status: $netsh_exit_status, 
output:\n@{$netsh_output}");
                return;
        }
        else {
-               notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to 
disable dynamic DNS registration on public and private adapters");
+               notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to 
enable dynamic DNS registration on $interface adapters");
                return;
        }
 
        return 1;
-} ## end sub disable_dynamic_dns
+} ## end sub enable_dynamic_dns
 
 #/////////////////////////////////////////////////////////////////////////////
 
-=head2 disable_netbios
+=head2 disable_dynamic_dns
+
+ Parameters  :
+ Returns     :
+ Description :
+
+=cut
+
+sub disable_dynamic_dns {
+       my $self = shift;
+       if (ref($self) !~ /windows/i) {
+               notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
+               return;
+       }
+
+       my $system32_path = $self->get_system32_path() || return;
+       
+       my $registry_string .= <<"EOF";
+Windows Registry Editor Version 5.00
+
+; This file disables dynamic DNS updates
+
+[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters]
+"DisableDynamicUpdate"=dword:00000001
+
+[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters]
+"DisableReverseAddressRegistrations"=dword:00000001
+EOF
+
+       # Import the string into the registry
+       if ($self->import_registry_string($registry_string)) {
+               notify($ERRORS{'OK'}, 0, "disabled dynamic dns");
+       }
+       else {
+               notify($ERRORS{'WARNING'}, 0, "failed to disable dynamic dns");
+               return;
+       }
+
+       # Get the network configuration
+       my $network_configuration = $self->get_network_configuration();
+       if (!$network_configuration) {
+               notify($ERRORS{'WARNING'}, 0, "unable to retrieve network 
configuration");
+               return;
+       }
+       
+       # Get the public and private interface names
+       my $public_interface_name = $self->get_public_interface_name();
+       my $private_interface_name = $self->get_private_interface_name();
+       
+       # Assemble netsh.exe commands to disable DNS registration
+       my $netsh_command;
+       $netsh_command .= "$system32_path/netsh.exe interface ip set dns";
+       $netsh_command .= " name = \"$public_interface_name\"";
+       $netsh_command .= " source = dhcp";
+       $netsh_command .= " register = none";
+       $netsh_command .= " ;";
+       
+       $netsh_command .= "$system32_path/netsh.exe interface ip set dns";
+       $netsh_command .= " name = \"$private_interface_name\"";
+       $netsh_command .= " source = dhcp";
+       $netsh_command .= " register = none";
+       $netsh_command .= " ;";
+       
+       # Execute the netsh.exe command
+       my ($netsh_exit_status, $netsh_output) = $self->execute($netsh_command);
+       if (defined($netsh_exit_status)  && $netsh_exit_status == 0) {
+               notify($ERRORS{'OK'}, 0, "disabled dynamic DNS registration on 
public and private adapters");
+       }
+       elsif (defined($netsh_exit_status)) {
+               notify($ERRORS{'WARNING'}, 0, "failed to disable dynamic DNS 
registration on public and private adapters, exit status: $netsh_exit_status, 
output:\n@{$netsh_output}");
+               return;
+       }
+       else {
+               notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to 
disable dynamic DNS registration on public and private adapters");
+               return;
+       }
+
+       return 1;
+} ## end sub disable_dynamic_dns
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 disable_netbios
 
  Parameters  :
  Returns     :
@@ -5734,13 +6088,10 @@ sub get_network_configuration {
                # Autoconfiguration ip address will be displayed as 
"Autoconfiguration IP Address. . . : 169.x.x.x"
                $setting =~ s/autoconfiguration_ip/ip/;
                
-               # Remove the trailing s from dns_servers
-               $setting =~ s/dns_servers/dns_server/;
-               
                # Check which setting was found and add to hash
                if ($setting =~ /dns_servers/) {
                        
push(@{$network_configuration->{$interface_name}{$setting}}, $value);
-                       #notify($ERRORS{'OK'}, 0, "$interface_name:$setting = 
@{$network_configuration->{$interface_name}{$setting}}");
+                       notify($ERRORS{'OK'}, 0, "$interface_name:$setting\n" . 
format_data($network_configuration->{$interface_name}{$setting}));
                }
                elsif ($setting =~ /ip_address/) {
                        $value =~ s/[^\.\d]//g;
@@ -6935,22 +7286,23 @@ sub start_service {
                notify($ERRORS{'WARNING'}, 0, "failed to execute command to to 
start service: $service_name");
                return;
        }
-       elsif (grep(/is not started/i, @{$output})) {
-               notify($ERRORS{'OK'}, 0, "service is not started: 
$service_name");
+       elsif (grep(/already been started/i, @{$output})) {
+               notify($ERRORS{'OK'}, 0, "service is already started: 
$service_name");
                return 1;
        }
        elsif (grep(/(does not exist|service name is invalid)/i, @$output)) {
                notify($ERRORS{'WARNING'}, 0, "service could not be started 
because it does not exist: $service_name, output:\n" . join("\n", @$output));
                return 0;
        }
-       elsif ($exit_status || grep(/could not be started/i, @$output)) {
+       elsif ($exit_status) {
                notify($ERRORS{'WARNING'}, 0, "failed to start service: 
$service_name, exit status: $exit_status, command:\n$command\noutput:\n" . 
join("\n", @$output));
                return 0;
        }
        else {
                notify($ERRORS{'OK'}, 0, "started service: $service_name" . 
join("\n", @$output));
+               return 1;
        }
-       return 1;
+       
 } ## end sub start_service
 
 #/////////////////////////////////////////////////////////////////////////////
@@ -7826,63 +8178,57 @@ sub get_node_configuration_directory {
 
 #/////////////////////////////////////////////////////////////////////////////
 
-=head2 set_computer_name
+=head2 get_kms_client_product_keys
 
- Parameters  : $computer_name (optional)
- Returns     : If successful: true
+ Parameters  : none
+ Returns     : hash reference
+ Description : Retrieves the $KMS_CLIENT_PRODUCT_KEYS variable.
+
+=cut
+
+sub get_kms_client_product_keys {
+       return $KMS_CLIENT_PRODUCT_KEYS;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 get_kms_client_product_key
+
+ Parameters  : $product_name (optional)
+ Returns     : If successful: string
                If failed: false
- Description : Sets the registry keys to set the computer name. This subroutine
-               does not attempt to reboot the computer.
-               The computer name argument is optional. If not supplied, the
-               computer's short name stored in the database will be used,
-               followed by a hyphen and the image ID that is loaded.
+ Description : Returns a KMS client product key based on the version of Windows
+               either specified as an argument or installed on the computer. A
+               KMS client product key is a publically shared product key which
+               must be installed before activating using a KMS server.
 
 =cut
 
-sub set_computer_name {
+sub get_kms_client_product_key {
        my $self = shift;
        if (ref($self) !~ /windows/i) {
                notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
                return;
        }
        
-       # Get the computer name
-       my $new_computer_name = shift;
-       if (!$new_computer_name) {
-               $new_computer_name = $self->data->get_computer_short_name();
-               if (!$new_computer_name) {
-                       notify($ERRORS{'WARNING'}, 0, "computer name argument 
was not supplied and could not be retrieved from the reservation data");
-                       return;
-               }
-               
-               # Append the image ID to the computer name
-               my $image_id = $self->data->get_image_id();
-               $new_computer_name .= "-$image_id" if $image_id;
+       # Get the product name
+       my $product_name = shift || $self->get_product_name();
+       if (!$product_name) {
+               notify($ERRORS{'WARNING'}, 0, "product name was not passed as 
an argument and could not be retrieved from computer");
+               return;
        }
        
-       my $computer_node_name   = $self->data->get_computer_node_name();
-       
-       my $registry_string .= <<"EOF";
-Windows Registry Editor Version 5.00
-
-[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\ComputerName\\ComputerName]
-"ComputerName"="$new_computer_name"
-
-[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters]
-"Hostname"="$new_computer_name"
-"NV Hostname"="$new_computer_name"
-EOF
+       # Remove (TM) or (R) from the product name
+       $product_name =~ s/ \([tmr]*\)//ig;
        
-       # Import the string into the registry
-       if ($self->import_registry_string($registry_string)) {
-               notify($ERRORS{'DEBUG'}, 0, "set registry keys to change the 
computer name of $computer_node_name to $new_computer_name");
-       }
-       else {
-               notify($ERRORS{'WARNING'}, 0, "failed to set registry keys to 
change the computer name of $computer_node_name to $new_computer_name");
+       # Get the matching product key from the hash for the product name
+       my $product_key = $KMS_CLIENT_PRODUCT_KEYS->{$product_name};
+       if (!$product_key) {
+               notify($ERRORS{'WARNING'}, 0, "unsupported product name: 
$product_name, KMS client product key is not known");
                return;
        }
-       
-       return 1;
+       notify($ERRORS{'DEBUG'}, 0, "returning KMS client setup key for 
$product_name: $product_key");
+       return $product_key;
 }
 
 #/////////////////////////////////////////////////////////////////////////////
@@ -8187,13 +8533,11 @@ sub set_static_public_address {
        my $computer_public_ip_address = 
$self->data->get_computer_public_ip_address() || '<undefined>';
        my $subnet_mask = $self->data->get_management_node_public_subnet_mask() 
|| '<undefined>';
        my $default_gateway = 
$self->data->get_management_node_public_default_gateway() || '<undefined>';
-       my @dns_servers = $self->data->get_management_node_public_dns_servers();
 
    if ($server_request_fixed_ip) {
       $computer_public_ip_address = $server_request_fixed_ip;
       $subnet_mask = $self->data->get_server_request_netmask();
       $default_gateway = $self->data->get_server_request_router();
-      @dns_servers = $self->data->get_server_request_dns_servers();
    }
        
        # Assemble a string containing the static IP configuration
@@ -8202,7 +8546,6 @@ public interface name: $interface_name
 public IP address: $computer_public_ip_address
 public subnet mask: $subnet_mask
 public default gateway: $default_gateway
-public DNS server(s): @dns_servers
 EOF
        
        # Make sure required info was retrieved
@@ -8212,6 +8555,8 @@ EOF
        }
        
        my $current_public_ip_address = $self->get_public_ip_address();
+       my $current_public_subnet_mask = $self->get_public_subnet_mask();
+       
        if ($current_public_ip_address eq $computer_public_ip_address) {
                notify($ERRORS{'DEBUG'}, 0, "public IP address of 
$computer_name is already set to $current_public_ip_address, attempting to set 
it again in case any parameters changed");
        }
@@ -8225,9 +8570,6 @@ EOF
        
        notify($ERRORS{'OK'}, 0, "attempting to set static public IP address on 
$computer_name:\n$configuration_info_string");
        
-       my $primary_dns_server_address = shift @dns_servers;
-       notify($ERRORS{'DEBUG'}, 0, "primary DNS server address: 
$primary_dns_server_address\nalternate DNS server address(s):\n" . (join("\n", 
@dns_servers) || '<none>'));
-       
        # Delete any default routes
        $self->delete_default_routes();
        
@@ -8236,15 +8578,6 @@ EOF
        
        # Set the static public IP address
        my $command = "$system32_path/netsh.exe interface ip set address 
name=\"$interface_name\" source=static addr=$computer_public_ip_address 
mask=$subnet_mask gateway=$default_gateway gwmetric=0";
-       
-       # Set the static DNS server address
-       $command .= " && $system32_path/netsh.exe interface ip set dns 
name=\"$interface_name\" source=static addr=$primary_dns_server_address 
register=none";
-       
-       # Add commands to set the alternate DNS server addresses
-       for my $alternate_dns_server_address (@dns_servers) {
-               $command .= " && $system32_path/netsh.exe interface ip add dns 
name=\"$interface_name\" addr=$alternate_dns_server_address";
-       }
-       
        my ($exit_status, $output) = $self->execute($command, 1, 60, 3);
        if (!defined($output)) {
                notify($ERRORS{'WARNING'}, 0, "failed to execute command to set 
static public IP address");
@@ -8255,16 +8588,147 @@ EOF
                return;
        }
        else {
-               notify($ERRORS{'OK'}, 0, "set static public IP address");
+               notify($ERRORS{'OK'}, 0, "set static public IP address: 
$computer_public_ip_address/$subnet_mask, default gateway: $default_gateway");
        }
+
+       $self->set_public_default_route() || return;
        
-       # Add persistent static public default route
-       if (!$self->set_public_default_route()) {
-               notify($ERRORS{'WARNING'}, 0, "failed to add persistent static 
public default route");
+       $self->set_static_dns_servers() || return;
+       
+       return 1;
+}
+
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 set_static_dns_servers
+
+ Parameters  : none
+ Returns     : boolean
+ Description : Configures the computer to use static DNS server addresses 
rather
+               than addresses obtained from DHCP. Static addresses will only be
+               used if either of the following conditions is true:
+               1. Server request is configured with specific DNS servers
+               2. The management node is configured to assign static public IP
+                  addresses and the public DNS server list is not empty
+               3. The image is configured to use Active Directory 
authentication
+                  and the configured domain's DNS server list is not empty
+               
+               If multiple conditions are true, only the DNS servers configured
+               for the first condition met are used.
+
+=cut
+
+sub set_static_dns_servers {
+       my $self = shift;       
+       if (ref($self) !~ /windows/i) {
+               notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
+               return;
+       }
+       
+       my $computer_name       = $self->data->get_computer_short_name();
+       my $image_name  = $self->data->get_image_name();
+       my $system32_path = $self->get_system32_path() || return;
+       
+       my $mn_public_ip_configuration = 
$self->data->get_management_node_public_ip_configuration();
+       my @mn_dns_servers = 
$self->data->get_management_node_public_dns_servers();
+       
+       my $domain_dns_name = $self->data->get_image_domain_dns_name();
+       my @domain_dns_servers = $self->data->get_image_domain_dns_servers();
+       
+       my @server_request_dns_servers = 
$self->data->get_server_request_dns_servers();
+       
+       my @dns_servers;
+       if (@server_request_dns_servers) {
+               @dns_servers = @server_request_dns_servers;
+               notify($ERRORS{'DEBUG'}, 0, "server request specific DNS 
servers will be statically set on $computer_name: " . join(", ", @dns_servers));
+       }
+       elsif ($domain_dns_name && @domain_dns_servers) {
+               @dns_servers = @domain_dns_servers;
+               notify($ERRORS{'DEBUG'}, 0, "$image_name image is configured 
for Active Directory, domain DNS servers will be statically set on 
$computer_name: " . join(", ", @dns_servers));
+       }
+       elsif ($mn_public_ip_configuration =~ /static/i && @mn_dns_servers) {
+               @dns_servers = @mn_dns_servers;
+               notify($ERRORS{'DEBUG'}, 0, "management node IP configuration 
set to $mn_public_ip_configuration, management node DNS servers will be 
statically set on $computer_name: " . join(", ", @dns_servers));
+       }
+       else {
+               notify($ERRORS{'WARNING'}, 0, "$computer_name not configured to 
use static DNS servers:\n" .
+                       "management node IP configuration               : 
$mn_public_ip_configuration\n" .
+                       "management node DNS servers configured         : " . 
(@mn_dns_servers ? 'yes' : 'no') . "\n" .
+                       "image configured for Active Directory          : " . 
($domain_dns_name ? 'yes' : 'no') . "\n" .
+                       "Active Directory domain DNS servers configured : " . 
(@domain_dns_servers ? 'yes' : 'no')
+               );
+               return;
+       }
+       
+       my $private_interface_name = $self->get_private_interface_name();
+       if (!$private_interface_name) {
+               notify($ERRORS{'WARNING'}, 0, "unable to set static DNS servers 
on $computer_name, private interface name could not be retrieved");
+               return;
+       }
+       
+       my $public_interface_name = $self->get_public_interface_name();
+       if (!$public_interface_name) {
+               notify($ERRORS{'WARNING'}, 0, "unable to set static DNS servers 
on $computer_name, public interface name could not be retrieved");
                return;
        }
        
-       notify($ERRORS{'OK'}, 0, "configured static address for public 
interface '$interface_name'");
+       for my $interface_name ($private_interface_name, 
$public_interface_name) {
+               # Get the first address from the array - the netsh.exe syntax 
is different for the first/primary DNS server and others
+       
+               # netsh interface ipv4 set dnsservers [name=]<string> 
[source=]dhcp|static [[address=]<IP address>|none] 
[[register=]none|primary|both] [[validate=]yes|no]
+               # name - The name or index of the interface.
+               # source - One of the following values:
+               #    dhcp: Sets DHCP as the source for configuring DNS servers 
for the specific interface.
+               #    static: Sets the source for configuring DNS servers to 
local static configuration.
+               # address - One of the following values:
+               #    <IP address>: An IP address for a DNS server.
+               #    none: Clears the list of DNS servers.
+               # register - One of the following values:
+               #    none: Disables Dynamic DNS registration.
+               #    primary: Register under the primary DNS suffix only.
+               #    both: Register under both the primary DNS suffix, as well 
as under the connection-specific suffix.
+               # validate - Specifies whether validation of the DNS server 
setting will be performed. The value is yes by default.
+               my $primary_dns_server = $dns_servers[0];
+               my $command = "$system32_path/netsh.exe interface ipv4 set 
dnsservers name=\"$interface_name\" source=static address=$primary_dns_server 
validate=no";
+               
+               # netsh interface ipv4 add dnsservers [name=]<string> 
[address=]<IPv4 address> [[index=]<integer>] [[validate=]yes|no]
+               # name         - The name or index of the interface where DNS 
servers are added.
+               # address      - The IP address for the DNS server you are 
adding.
+               # index        - Specifies the index (preference) for the 
specified DNS server address.
+               # validate     - Specifies whether validation of the DNS server 
setting will be performed. The value is yes by default.
+               for (my $i=1; $i<scalar(@dns_servers); $i++) {
+                       my $secondary_dns_server = $dns_servers[$i];
+                       $command .= " ; $system32_path/netsh.exe interface ipv4 
add dnsservers name=\"$interface_name\" address=$secondary_dns_server 
validate=no";
+               }
+               
+               my ($exit_status, $output) = $self->execute($command);
+               if (!defined($output)) {
+                       notify($ERRORS{'WARNING'}, 0, "failed to execute 
command to configure static DNS servers for $interface_name interface on 
$computer_name: $command");
+                       return;
+               }
+               elsif ($exit_status ne '0') {
+                       notify($ERRORS{'WARNING'}, 0, "failed to configure 
static DNS servers for $interface_name interface on $computer_name, exit 
status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
+                       return 0;
+               }
+               else {
+                       notify($ERRORS{'OK'}, 0, "configured static DNS servers 
for $interface_name interface on $computer_name: " . join(", ", @dns_servers));
+               }
+       }
+       
+       # Flush the DNS cache - not sure if this is necessary but AD computers 
sometimes have trouble finding things for some reason
+       my $command = "$system32_path/ipconfig.exe /flushdns";
+       my ($exit_status, $output) = $self->execute($command);
+       if (!defined($output)) {
+               notify($ERRORS{'WARNING'}, 0, "failed to execute command to 
flush DNS resolver cache on $computer_name: $command");
+       }
+       elsif ($exit_status ne '0') {
+               notify($ERRORS{'WARNING'}, 0, "failed to flush DNS resolver 
cache on $computer_name, exit status: $exit_status, 
command:\n$command\noutput:\n" . join("\n", @$output));
+       }
+       else {
+               notify($ERRORS{'OK'}, 0, "flushed DNS resolver cache on 
$computer_name");
+       }
+       
        return 1;
 }
 
@@ -8526,11 +8990,11 @@ sub is_64_bit {
        
        # Check if architecture has previously been determined
        if (defined($self->{OS_ARCHITECTURE}) && $self->{OS_ARCHITECTURE} eq 
'64') {
-               notify($ERRORS{'DEBUG'}, 0, '64-bit Windows OS previously 
detected');
+               #notify($ERRORS{'DEBUG'}, 0, '64-bit Windows OS previously 
detected');
                return 1;
        }
        elsif (defined($self->{OS_ARCHITECTURE}) && $self->{OS_ARCHITECTURE} eq 
'32') {
-               notify($ERRORS{'DEBUG'}, 0, '32-bit Windows OS previously 
detected');
+               #notify($ERRORS{'DEBUG'}, 0, '32-bit Windows OS previously 
detected');
                return 0;
        }
        
@@ -10984,6 +11448,7 @@ sub run_script {
        my $timeout_seconds = shift || 300;
        
        my $computer_node_name = $self->data->get_computer_node_name();
+       my $system32_path = $self->get_system32_path() || return;
        
        # Check if script exists
        if (!$self->file_exists($script_path)) {
@@ -11013,7 +11478,13 @@ sub run_script {
        my $timestamp = makedatestring();
        
        # Assemble the command
-       my $command = "cmd.exe /c \"$script_path_escaped & exit %ERRORLEVEL%\"";
+       my $command;
+       if ($script_extension =~ /vbs/i) {
+               $command = "cmd.exe /c \"$system32_path/cscript.exe 
$script_path_escaped & exit %ERRORLEVEL%\"";
+       }
+       else {
+               $command = "cmd.exe /c \"$script_path_escaped & exit 
%ERRORLEVEL%\"";
+       }
        
        # Execute the command
        notify($ERRORS{'DEBUG'}, 0, "executing script on 
$computer_node_name:\nscript path: $script_path\nlog file path: 
$log_file_path\nscript timeout: $timeout_seconds seconds");
@@ -11029,13 +11500,13 @@ sub run_script {
        $logfile_contents = '=' x $header_line_length . 
"\r\n$logfile_contents\r\n" . '=' x $header_line_length . "\r\n";
        $logfile_contents .= join("\r\n", @$output) . "\r\n";
        $self->create_text_file($log_file_path, $logfile_contents, 1);
-       
+
        if ($exit_status == 0) {
-               notify($ERRORS{'OK'}, 0, "successfully executed script on 
$computer_node_name: '$script_path', output saved to '$log_file_path', 
command:\n$command, output:\n" . join("\n". @$output));
+               notify($ERRORS{'OK'}, 0, "successfully executed script on 
$computer_node_name: '$script_path'\nlog file: log_file_path\ncommand: 
$command, output:\n" . join("\n", @$output));
                return 1;
        }
        else {
-               notify($ERRORS{'WARNING'}, 0, "script '$script_path' returned a 
non-zero exit status: $exit_status\nlogfile path: '$log_file_path'\ncommand: 
'$command'\noutput:\n" . join("\n", @$output));
+               notify($ERRORS{'WARNING'}, 0, "script '$script_path' returned a 
non-zero exit status: $exit_status\nlog file: $log_file_path\ncommand: 
'$command'\noutput:\n" . join("\n", @$output));
                return 0;
        }
 }
@@ -11184,7 +11655,7 @@ sub install_exe_update {
        elsif ($exit_status eq '194') {
                # Exit status 194 - installed but reboot required
                notify($ERRORS{'DEBUG'}, 0, "installed update on 
$computer_node_name, exit status $exit_status indicates a reboot is required");
-               $self->data->set_imagemeta_postoption('reboot');
+               push @{$self->{reboot_required}}, "installed update: 
$file_path, exit status indicates a reboot is required";
                return 1;
        }
        elsif ($exit_status eq '0') {
@@ -11198,7 +11669,7 @@ sub install_exe_update {
        my @log_file_lines = $self->get_file_contents($log_file_path);
        for my $line (@log_file_lines) {
                if ($line =~ /RebootNecessary = 1|reboot is required/i) {
-                       $self->data->set_imagemeta_postoption('reboot');
+                       push @{$self->{reboot_required}}, "installed update: 
$file_path, log file indicates a reboot is required: $line";
                }
        }
        
@@ -11292,7 +11763,7 @@ sub install_msu_update {
                        if ($error_code eq '2359302') {
                                # Already installed but reboot is required
                                notify($ERRORS{'DEBUG'}, 0, "update $update_id 
is already installed but a reboot is required:\n" . format_data(\%event_data));
-                               $self->data->set_imagemeta_postoption('reboot');
+                               push @{$self->{reboot_required}}, "installed 
update: $file_path, event log indicates a reboot is required";
                        }
                        else {
                                notify($ERRORS{'WARNING'}, 0, "error occurred 
installing update $update_id:\n" . format_data(\%event_data));
@@ -11302,7 +11773,7 @@ sub install_msu_update {
                        if ($debug_message =~ /IsRebootRequired: 1/i) {
                                # RebootIfRequested.01446: Reboot is not 
scheduled. IsRunWizardStarted: 0, IsRebootRequired: 0, RestartMode: 1
                                notify($ERRORS{'DEBUG'}, 0, "installed update 
$update_id, reboot is required:\n$debug_message");
-                               $self->data->set_imagemeta_postoption('reboot');
+                               push @{$self->{reboot_required}}, "installed 
update: $file_path, event message indicates a reboot is required: 
$debug_message";
                        }
                        elsif ($debug_message =~ /Update is already 
installed/i) {
                                # InstallWorker.01051: Update is already 
installed
@@ -12118,6 +12589,56 @@ sub disable_set_network_location_prompt
 
 #/////////////////////////////////////////////////////////////////////////////
 
+=head2 get_current_computer_hostname
+
+ Parameters  : none
+ Returns     : string
+ Description : Retrieves the current hostname the computer is using. If a
+               computer was renamed but not rebooted, this will return the
+               previous name.
+
+=cut
+
+sub get_current_computer_hostname {
+       my $self = shift;
+       unless (ref($self) && $self->isa('VCL::Module')) {
+               notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
+               return;
+       }
+       
+       my $computer_name = $self->data->get_computer_node_name();
+       
+       my $command = 'cmd.exe /c "C:/Windows/Sysnative/Wbem/wmic.exe 
COMPUTERSYSTEM GET Name /Value"';
+       my ($exit_status, $output) = $self->execute($command);
+       if (!defined($output)) {
+               notify($ERRORS{'WARNING'}, 0, "failed to execute command on 
$computer_name: $command");
+               return;
+       }
+       elsif ($exit_status ne '0') {
+               notify($ERRORS{'WARNING'}, 0, "failed to retrieve current 
computer hostname from $computer_name, exit status: $exit_status, 
command:\n$command\noutput:\n" . join("\n", @$output));
+               return 0;
+       }
+       
+       # Output should be:
+       # Name=vm-100
+       my ($line) = grep(/Name=(.+)/i, @$output);
+       if (!$line) {
+               notify($ERRORS{'WARNING'}, 0, "failed to retrieve current 
computer name from $computer_name, output does not contain a 'Name=' line:\n" . 
join("\n", @$output));
+               return;
+       }
+       
+       my ($current_computer_name) = $line =~ /Name=(.+)$/ig;
+       if (!$current_computer_name) {
+               notify($ERRORS{'WARNING'}, 0, "failed to retrieve current 
computer name from $computer_name, failed to parse line: '$line'");
+               return;
+       }
+
+       notify($ERRORS{'OK'}, 0, "retrieved current computer hostname from 
$computer_name: '$current_computer_name'");
+       return $current_computer_name;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
 =head2 set_computer_hostname
 
  Parameters  : $new_computer_name (optional)
@@ -12178,9 +12699,9 @@ sub set_computer_hostname {
                notify($ERRORS{'DEBUG'}, 0, "failed to execute command to set 
computer name of $database_computer_hostname to $new_computer_name");
                return;
        }
-       elsif (grep(/ReturnValue = 0;/i, @$output)) {
+       elsif (grep(/(ReturnValue = 0|Method execution successful)/i, 
@$output)) {
                notify($ERRORS{'OK'}, 0, "set computer name of 
$database_computer_hostname to $new_computer_name, exit status: $exit_status, 
command:\n$command\noutput:\n" . join("\n", @$output));
-               $self->data->set_imagemeta_postoption('reboot');
+               push @{$self->{reboot_required}}, "computer hostname was 
changed";
        }
        else {
                notify($ERRORS{'WARNING'}, 0, "failed to set computer name of 
$database_computer_hostname to $new_computer_name, exit status: $exit_status, 
command:\n$command\noutput:\n" . join("\n", @$output));
@@ -12189,11 +12710,10 @@ sub set_computer_hostname {
        
        # Set the DNS suffix registry key
        if ($dns_suffix) {
-               return 
$self->reg_add('HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\Tcpip\Parameters',
 'NV Domain', 'REG_SZ', $dns_suffix);
-       }
-       else {
-               return 1;
+               
$self->reg_add('HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\Tcpip\Parameters',
 'NV Domain', 'REG_SZ', $dns_suffix);
        }
+       
+       return 1;
 }
 
 #/////////////////////////////////////////////////////////////////////////////
@@ -12408,6 +12928,1388 @@ sub get_nfs_mounts_windows {
        }
 }
 
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 get_windows_features
+
+ Parameters  : none
+ Returns     : hash reference
+ Description : Retrieves a list of all features available on the Windows OS. If
+               called in scalar context, a hash reference is returned:
+               {
+                 "Chess" => {
+                   "State" => "Disabled"
+                 },
+                 "ClientForNFS-Infrastructure" => {
+                   "State" => "Enabled"
+                 },
+                 ...
+               }
+               
+               If called in array context, an array of feature names is
+               returned:
+               (
+                  Chess,
+                  ClientForNFS-Infrastructure,
+                  ...
+               )
+
+=cut
+
+sub get_windows_features {
+       my $self = shift;
+       if (ref($self) !~ /Windows/i) {
+               notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
+               return;
+       }
+       
+       my $computer_name = $self->data->get_computer_node_name();
+       my $system32_path = $self->get_system32_path() || return;
+       
+       notify($ERRORS{'DEBUG'}, 0, "retrieving Windows features on 
$computer_name");
+       my $command = "$system32_path/cmd.exe /c \"dism /online /get-features 
/format:table\"";
+       my ($exit_status, $output) = $self->execute($command);
+       if (!defined($output)) {
+               notify($ERRORS{'WARNING'}, 0, "failed to execute command on 
$computer_name: $command");
+               return;
+       }
+       elsif ($exit_status ne '0') {
+               notify($ERRORS{'WARNING'}, 0, "failed to retrieve feature info 
from $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" 
. join("\n", @$output));
+               return;
+       }
+       
+       my $feature_info = {};
+       for my $line (@$output) {
+               # Line format:
+               # FreeCell                                    | Disabled
+               my ($feature_name, $state) = $line =~ 
/^(\S+)\s+.*(Enabled|Disabled)\s*$/i;
+               if (defined($feature_name) && defined($state)) {
+                       $feature_info->{$feature_name}{State} = $state;
+               }
+       }
+       
+       if (!keys(%$feature_info)) {
+               notify($ERRORS{'WARNING'}, 0, "failed to retrieve feature info 
from $computer_name, failed to parse any feature names and states from 
output:\n" . join("\n", @$output));
+               return;
+       }
+       
+       if (wantarray) {
+               my @feature_names = sort { lc($a) cmp lc($b) } keys 
%$feature_info;
+               notify($ERRORS{'DEBUG'}, 0, "retrieved feature info from 
$computer_name, returning array:\n" . join("\n", @feature_names));
+               return $feature_info;
+       }
+       else {
+               notify($ERRORS{'DEBUG'}, 0, "retrieved feature info from 
$computer_name, returning hash reference:\n" . format_data($feature_info));
+               return $feature_info;
+       }
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 get_windows_feature_info
+
+ Parameters  : $feature_name
+ Returns     : hash reference
+ Description : Retrieves info for a single Windows feature and constructs a
+               hash:
+               {
+                  "Description" => "Install the .NET Environment for 
supporting managed code activation",
+                  "Display Name" => ".NET Environment",
+                  "Feature Name" => "WAS-NetFxEnvironment",
+                  "Restart Required" => "Possible",
+                  "State" => "Enabled"
+               }
+
+=cut
+
+sub get_windows_feature_info {
+       my $self = shift;
+       if (ref($self) !~ /Windows/i) {
+               notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
+               return;
+       }
+       
+       my $feature_name = shift;
+       if (!$feature_name) {
+               notify($ERRORS{'WARNING'}, 0, "feature name argument was not 
specified");
+               return;
+       }
+       
+       my $computer_name = $self->data->get_computer_node_name();
+       my $system32_path = $self->get_system32_path() || return;
+       
+       notify($ERRORS{'DEBUG'}, 0, "retrieving info for Windows feature on 
$computer_name: $feature_name");
+       my $command = "$system32_path/cmd.exe /c \"DISM.exe /Online 
/Get-FeatureInfo /FeatureName=$feature_name\"";
+       my ($exit_status, $output) = $self->execute($command);
+       if (!defined($output)) {
+               notify($ERRORS{'WARNING'}, 0, "failed to execute command on 
$computer_name: $command");
+               return;
+       }
+       elsif (grep(/Feature name.*is unknown/, @$output)) {
+               # Feature name foo is unknown
+               notify($ERRORS{'OK'}, 0, "Windows feature is unknown on 
$computer_name: $feature_name, returning empty hash reference");
+               return {};
+       }
+       elsif ($exit_status ne '0') {
+               notify($ERRORS{'WARNING'}, 0, "failed to retrieve feature info 
from $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" 
. join("\n", @$output));
+               return;
+       }
+
+       my $feature_information_line_found = 0;
+       my $feature_info = {};
+       for my $line (@$output) {
+               if ($line !~ /\w/) {
+                       next;
+               }
+               elsif (!$feature_information_line_found) {
+                       # Ignore all lines until a 'Feature Information:' line 
is found
+                       if ($line =~ /Feature Information:/) {
+                               $feature_information_line_found = 1;
+                       }
+                       next;
+               }
+               
+               # Line format:
+               # Display Name : Windows Media Player
+               my ($property, $value) = $line =~ /^(\w.*\S)\s+:\s+(\S.*)$/i;
+               if (defined($property) && defined($value)) {
+                       $feature_info->{$property} = $value;
+               }
+               else {
+                       notify($ERRORS{'DEBUG'}, 0, "line does not contain 
property:value: '$line'");
+               }
+       }
+       
+       if (keys(%$feature_info)) {
+               notify($ERRORS{'DEBUG'}, 0, "retrieved Windows feature info 
from $computer_name: $feature_name\n" . format_data($feature_info));
+               return $feature_info;
+       }
+       else {
+               notify($ERRORS{'WARNING'}, 0, "failed to retrieve Windows 
feature info from $computer_name: $feature_name, failed to parse any properties 
from output:\n" . join("\n", @$output));
+               return;
+       }
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 is_windows_feature_enabled
+
+ Parameters  : $feature_name
+ Returns     : boolean
+ Description : Determines whether or not a Windows feature such as the NFS
+               client is enabled.
+
+=cut
+
+sub is_windows_feature_enabled {
+       my $self = shift;
+       if (ref($self) !~ /Windows/i) {
+               notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
+               return;
+       }
+       
+       my $feature_name = shift;
+       if (!$feature_name) {
+               notify($ERRORS{'WARNING'}, 0, "feature name argument was not 
specified");
+               return;
+       }
+       
+       my $computer_name = $self->data->get_computer_node_name();
+       
+       my $feature_info = $self->get_windows_feature_info($feature_name) || 
return;
+       if (!keys(%$feature_info)) {
+               notify($ERRORS{'DEBUG'}, 0, "Windows feature is NOT enabled on 
$computer_name because the feature is unknown: $feature_name");
+               return 0;
+       }
+       elsif (!defined($feature_info->{State})) {
+               notify($ERRORS{'WARNING'}, 0, "failed to determine if Windows 
feature is enabled on $computer_name: $feature_name, feature info does not 
contain a 'State' key:\n" . format_data($feature_info));
+               return;
+       }
+       
+       my $state = $feature_info->{State};
+       if ($state =~ /Enabled/i) {
+               notify($ERRORS{'DEBUG'}, 0, "Windows feature is enabled on 
$computer_name: $feature_name");
+               return 1;
+       }
+       else {
+               notify($ERRORS{'DEBUG'}, 0, "Windows feature is NOT enabled on 
$computer_name: $feature_name");
+               return 0;
+       }
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 enable_windows_feature
+
+ Parameters  : $feature_name
+ Returns     : boolean
+ Description : Enables a Windows feature. This will also recursively enable any
+               parent features which must be enabled before the feature
+               specified by the argument can be enabled.
+
+=cut
+
+sub enable_windows_feature {
+       my $self = shift;
+       if (ref($self) !~ /Windows/i) {
+               notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
+               return;
+       }
+       
+       my ($feature_name, $no_recurse) = @_;
+       if (!$feature_name) {
+               notify($ERRORS{'WARNING'}, 0, "feature name argument was not 
specified");
+               return;
+       }
+       
+       my $computer_name = $self->data->get_computer_node_name();
+       my $system32_path = $self->get_system32_path() || return;
+       
+       my $log_path = "C:/Windows/Logs/DISM/$feature_name.log";
+
+       my $command = "$system32_path/cmd.exe /c \"DISM.exe /Online 
/Enable-Feature /FeatureName:$feature_name /NoRestart /LogPath=$log_path\"";
+       notify($ERRORS{'DEBUG'}, 0, "enabling Windows feature on 
$computer_name: $feature_name, command:\n$command");
+       my ($exit_status, $output) = $self->execute({
+               command => $command,
+               timeout_seconds => 120,
+               display_output => 0,
+       });
+       if (!defined($output)) {
+               notify($ERRORS{'WARNING'}, 0, "failed to execute command on 
$computer_name: $command");
+               return;
+       }
+       elsif (grep(/completed successfully/, @$output)) {
+               notify($ERRORS{'OK'}, 0, "enabled Windows feature on 
$computer_name: $feature_name");
+               return 1;
+       }
+       
+       my $parent_feature_found = 0;
+       if (!$no_recurse) {
+               # Check if parent features need to be enabled first, line like 
this will exist:
+               #    Ensure that the following parent feature(s) are enabled 
first
+               #    IIS-Security, IIS-WebServer, IIS-WebServerRole
+               my $parent_feature_line_found = 0;
+               LINE: for my $line (@$output) {
+                       if ($line !~ /\w/) {
+                               next LINE;
+                       }
+                       elsif (!$parent_feature_line_found) {
+                               if ($line =~ /Ensure that the following parent 
feature.*enabled first/) {
+                                       $parent_feature_line_found = 1;
+                               }
+                               next LINE;
+                       }
+                       
+                       # Stop checking if this line is found:
+                       #    The DISM log file can be found at 
C:\Windows\Logs\DISM\dism.log
+                       if ($line =~ /DISM log file/) {
+                               last LINE;
+                       }
+                       
+                       my @parent_feature_names = split(/,\s+/, $line);
+                       if (@parent_feature_names) {
+                               $parent_feature_found = 1;
+                               notify($ERRORS{'DEBUG'}, 0, "parent Windows 
feature(s) need to be enabled before $feature_name can be enabled: " . 
join("\n", @parent_feature_names));
+                               for my $parent_feature_name 
(@parent_feature_names) {
+                                       if 
(!$self->enable_windows_feature($parent_feature_name)) {
+                                               notify($ERRORS{'WARNING'}, 0, 
"failed to enable Windows feature on $computer_name: $feature_name, failed to 
enable parent feature: $parent_feature_name");
+                                               return;
+                                       }
+                               }
+                       }
+                       else {
+                               notify($ERRORS{'DEBUG'}, 0, "line does not 
appear to contain the name of a parent feature: '$line'");
+                               next LINE;
+                       }
+               }
+       }
+       
+       # Check if any parent features were found which need to be enabled first
+       # If not, failed to enable feature for some other reason
+       if ($parent_feature_found) {
+               # Make one more attempt to enable the feature, do not attempt 
to install parent features again ($no_recurse = 1)
+               return $self->enable_windows_feature($feature_name, 1);
+       }
+       else {
+               notify($ERRORS{'WARNING'}, 0, "failed to enable Windows feature 
on $computer_name: $feature_name, exit status: $exit_status, command: 
'$command', output:\n" . join("\n", @$output));
+               return;
+       }
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 run_powershell_command
+
+ Parameters  : $powershell_script_contents, $display_output (optional), 
$encode_command (optional)
+ Returns     : array ($exit_status, $output)
+ Description : Runs Powershell code as a command.
+
+=cut
+
+sub run_powershell_command {
+       my $self = shift;       
+       if (ref($self) !~ /windows/i) {
+               notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
+               return;
+       }
+       
+       my ($powershell_command_argument, $display_output, $encode_command) = 
@_;
+       if (!$powershell_command_argument) {
+               notify($ERRORS{'WARNING'}, 0, "powershell script contents 
argument was not supplied");
+               return;
+       }
+       
+       my $system32_path = $self->get_system32_path() || return;
+       my $computer_name       = $self->data->get_computer_short_name();
+       
+       # -Version Starts the specified version of Windows PowerShell 
+       # -NoLogo Hides the copyright banner at startup
+       # -NoExit Does not exit after running startup commands 
+       # -Sta Start the shell using a single-threaded apartment 
+       # -NoProfile Does not use the user profile
+       # -NonInteractive Does not present an interactive prompt to the user.
+       # -InputFormat Valid values are "Text" (text strings) or "XML"
+       # -OutputFormat Valid values are "Text" (text strings) or "XML"
+       # -EncodedCommand Accepts a base-64-encoded string version of a command
+       # -File Execute a script file.
+       # -ExecutionPolicy Sets the default execution policy for the session
+       # -Command      Executes the specified commands
+       
+       #my $command = "$system32_path/WindowsPowerShell/v1.0/powershell.exe 
-NoLogo -NoProfile -NonInteractive";
+       
+       my $command;
+       $command .= 'cmd.exe /c "';
+       $command .= "$system32_path/WindowsPowerShell/v1.0/powershell.exe 
-NoLogo -NoProfile -NonInteractive";
+       
+       
+       if ($encode_command) {
+               # Use the -EncodedCommand argument to avoid the need to escape 
various special characters
+               # The 2nd argument to encode_base64 needs to be an empty string 
or else it will break the encoded string up into 76 character lines
+               my $powershell_command_encoded = 
encode_base64(encode("UTF-16LE", $powershell_command_argument), "");
+               
+               #$command .= " -InputFormat Text";
+               $command .= " -OutputFormat Text";
+               $command .= " -EncodedCommand $powershell_command_encoded";
+       }
+       else {
+               # Replace newlines with semicolon
+               $powershell_command_argument =~ s/[\n\r]+/ ; /g;
+               
+               # Clean up semicolons
+               $powershell_command_argument =~ s/\s+;[\s;]*/ ; /g;
+               
+               # Remove semicolons from before and after curly brackets
+               $powershell_command_argument =~ s/[\s;]*([{}])[\s;]*/ $1 /g;
+               
+               #$powershell_command_argument .= ' ; [Environment]::Exit(!\$?)';
+               $command .= " -Command \\\"$powershell_command_argument\\\"";
+       }
+       $command .= ' < NUL"';
+       
+       notify($ERRORS{'DEBUG'}, 0, "attempting to run PowerShell command on 
$computer_name:\n$command") if $display_output;
+       my ($exit_status, $output) = $self->execute({
+               command => $command,
+               display_output => 0,
+               timeout_seconds => 30,
+               #no_persistent_connection => 1,
+               max_attempts => 1,
+       });
+       
+       if (!defined($output)) {
+               notify($ERRORS{'WARNING'}, 0, "failed to execute command to run 
PowerShell command on $computer_name");
+               return;
+       }
+       else {
+               notify($ERRORS{'OK'}, 0, "ran PowerShell command on 
$computer_name, exit status: $exit_status, command: '$command', output:\n" . 
join("\n", @$output)) if $display_output;
+               return ($exit_status, $output);
+       }
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 run_powershell_as_script
+
+ Parameters  : $powershell_script_contents, $display_output (optional), 
$retain_script_file (optional)
+ Returns     : array ($exit_status, $output)
+ Description : Accepts a string containing the contents of a Powershell script,
+               creates the script on the computer under C:\cygwin\VCL\Scripts,
+               and executes the script. The script is named after the calling
+               subroutine, so ad_join.ps1 would be generated when invoked from
+               ad_join().
+               
+               By default, the script file is deleted after it is executed for
+               safety. This can be overridden if the $retain_script_file
+               argument is true.
+=cut
+
+sub run_powershell_as_script {
+       my $self = shift;       
+       if (ref($self) !~ /windows/i) {
+               notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
+               return;
+       }
+       
+       my ($script_contents_argument, $display_output, $retain_script_file) = 
@_;
+       if (!$script_contents_argument) {
+               notify($ERRORS{'WARNING'}, 0, "powershell script contents 
argument was not supplied");
+               return;
+       }
+       
+       my $system32_path = $self->get_system32_path() || return;
+       my $computer_name       = $self->data->get_computer_short_name();
+       
+       # Figure out the script location
+       my $node_configuration_directory = 
$self->get_node_configuration_directory();
+       my $calling_subroutine = get_calling_subroutine();
+       $calling_subroutine =~ s/.*:://g;
+       my $powershell_script_path = 
"$node_configuration_directory/Scripts/$calling_subroutine.ps1";
+       
+       # Remove trailing newlines and blank lines
+       $script_contents_argument =~ s/[\r\n]+$//g;
+       
+       # Create copy of script contents, use this for execution, copied in 
case transformations need to be made in the future
+       my $script_contents = $script_contents_argument;
+       
+       $self->create_text_file($powershell_script_path, $script_contents);
+       
+       # Running Powershell scripts/commands via Cygwin has issues because 
Powershell.exe does screwy things with the terminal
+       # If OS.pm::execute_new is used (persistent SSH connection), script 
hangs if
+       # run normally and does not exit with [Environment]::Exit(x)
+       # Using run_ssh_command fails to determine the correct exit status but 
does not hang regardless of how script exits.
+       # Wrapping the script in cmd.exe /c "... < NUL" seems to prevent 
execute_new from hanging and the exit status is correct
+       
+       my $command;
+       $command .= 'cmd.exe /c "';
+       $command .= "$system32_path/WindowsPowerShell/v1.0/powershell.exe 
-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File 
$powershell_script_path";
+       $command .= ' < NUL"';
+       
+       notify($ERRORS{'DEBUG'}, 0, "attempting to execute PowerShell script: 
$powershell_script_path, contents:\n$script_contents") if $display_output;
+       my ($exit_status, $output) = $self->execute({
+               command => $command,
+               display_output => 1,
+               timeout_seconds => 60,
+               #no_persistent_connection => 1,
+               max_attempts => 1,
+       });
+       
+       # Delete the script file unless retain flag was specified
+       if ($retain_script_file) {
+               notify($ERRORS{'DEBUG'}, 0, "script NOT deleted because 
\$retain_script_file argument was specified");
+               
+               # TODO: add subs to correctly set Windows file permissions
+               # Open up permissions so Powershell file can easily be debugged 
on the Windows computer by users other than root
+               $self->execute("chmod -v 777 `cygpath 
\"$powershell_script_path\"`", 1);
+       }
+       else {
+               $self->delete_file($powershell_script_path);
+       }
+       
+       if (!defined($output)) {
+               notify($ERRORS{'WARNING'}, 0, "failed to execute command to run 
PowerShell commands as script on $computer_name");
+               return;
+       }
+       else {
+               notify($ERRORS{'OK'}, 0, "ran PowerShell commands as script on 
$computer_name, exit status: $exit_status, command: '$command', script 
contents:\n$script_contents_argument\noutput:\n" . join("\n", @$output)) if 
$display_output;
+               return ($exit_status, $output);
+       }
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 powershell_command_exists
+
+ Parameters  : $powershell_command
+ Returns     : boolean
+ Description : Checks if a PowerShell command or cmdlets exists on the 
computer.
+
+=cut
+
+sub powershell_command_exists {
+       my $self = shift;       
+       if (ref($self) !~ /windows/i) {
+               notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
+               return;
+       }
+       
+       my $powershell_command_argument = shift;
+       if (!$powershell_command_argument) {
+               notify($ERRORS{'WARNING'}, 0, "powershell command argument was 
not supplied");
+               return;
+       }
+       
+       my $computer_name       = $self->data->get_computer_short_name();
+       
+       my $powershell_command = "Get-Command $powershell_command_argument | 
Format-List Name";
+       my ($exit_status, $output) = 
$self->run_powershell_command($powershell_command);
+       if (!defined($output)) {
+               notify($ERRORS{'WARNING'}, 0, "failed to execute command to 
determine if '$powershell_command' PowerShell command exists on 
$computer_name");
+               return;
+       }
+       elsif (grep(/(NotFound)/, @$output)) {
+               notify($ERRORS{'DEBUG'}, 0, "PowerShell command does NOT exist 
on $computer_name: $powershell_command_argument, output:\n" . join("\n", 
@$output));
+               return 0;
+       }
+       else {
+               notify($ERRORS{'DEBUG'}, 0, "PowerShell command exists on 
$computer_name: $powershell_command_argument, output:\n" . join("\n", 
@$output));
+               return 1;
+       }
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 get_ad_computer_ou_dn
+
+ Parameters  : none
+ Returns     : boolean
+ Description : Converts an OU path as displayed on the Object tab when viewing
+               an OU's properties in the Active Directory Users and Computers
+               tool from:
+               my.ad.domain/Org/Unit/ComputerOU
+               To:
+               OU=ComputerOU,OU=Unit,OU=Org,DC=domain,DC=ad,DC=my
+
+=cut
+
+sub get_ad_computer_ou_dn {
+       my $self = shift;       
+       if (ref($self) !~ /windows/i) {
+               notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
+               return;
+       }
+       
+       my $domain_dns_name = $self->data->get_image_domain_dns_name();
+       
+       # Accepts a $computer_ou argument but should only be used for testing
+       my $computer_ou = shift || $self->data->get_image_domain_base_ou();
+       my $computer_ou_original = $computer_ou;
+       
+       if (!defined($domain_dns_name)) {
+               notify($ERRORS{'WARNING'}, 0, "AD domain DNS name is not 
configured");
+               return;
+       }
+       elsif (!defined($computer_ou)) {
+               notify($ERRORS{'DEBUG'}, 0, "AD domain computer OU is not 
configured");
+               return;
+       }
+       
+       # Possible cases:
+       #    OU=ComputerOU,OU=VCL,DC=my,DC=ad,DC=domain
+       #    OU=ComputerOU,OU=VCL
+       #    ComputerOU,VCL
+       #    ComputerOU
+       #    my.ad.domain/VCL/ComputerOU
+       #    VCL/ComputerOU
+       
+       # OU can either contain commas or slashes but not both - determines 
which order to put OU parts back together in
+       if ($computer_ou =~ /,/ && $computer_ou =~ /\//) {
+               notify($ERRORS{'WARNING'}, 0, "invalid AD OU, it can't contain 
both a comma and slash: $computer_ou");
+               return;
+       }
+       
+       # Remove domain DN section if it was specified for the image
+       $computer_ou =~ s/,DC=.*//g;
+       
+       # Strip out OU= parts, will be added later
+       $computer_ou =~ s/\s*OU=//g;
+       
+       # Assemble the domain part of the DN based on the domain DNS name
+       my @domain_parts = split(/\./, $domain_dns_name);
+       my $domain_section = "DC=" . join(",DC=", @domain_parts);
+       
+       # Check which order the OU parts should be reassembled in
+       my @ou_parts;
+       if ($computer_ou =~ /\//) {
+               # my.ad.domain/VCL/ComputerOU
+               # VCL/ComputerOU
+               @ou_parts = reverse split(/\/+/, $computer_ou);
+               
+               # Check if last part contains a period, if so, strip it
+               if ($ou_parts[-1] =~ /\./) {
+                       pop @ou_parts;
+               }
+       }
+       else {
+               @ou_parts = split(/,+/, $computer_ou);
+       }
+       my $ou_section = "OU=" . join(",OU=", @ou_parts);
+       
+       my $dn = "$ou_section,$domain_section";
+       
+       notify($ERRORS{'DEBUG'}, 0, "converted computer OU to DN: 
$computer_ou_original --> $dn");
+       return $dn;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 ad_join_prepare
+
+ Parameters  : none
+ Returns     : boolean
+ Description : Performs tasks necessary prior to joining a computer to an 
Active
+               Directory domain:
+               * Ensures the 'TCP NetBIOS helper' service is started
+               * Sets static DNS servers if configured for the domain
+               * Deletes existing matching computer objects
+
+=cut
+
+sub ad_join_prepare {
+       my $self = shift;       
+       if (ref($self) !~ /windows/i) {
+               notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
+               return;
+       }
+       
+       # Enable and start the TCP NetBIOS helper service
+       $self->set_service_startup_mode('lmhosts', 'auto');
+       $self->start_service('lmhosts');
+       
+       # Set specific DNS servers for private and public interfaces if DNS 
servers are configured
+       $self->set_static_dns_servers();
+       
+       # Delete existing computer with same computer name
+       # Computer may have been joined to a different OU
+       # Don't bother moving existing objects
+       $self->ad_delete_computer();
+       
+       return 1;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 ad_join
+
+ Parameters  : none
+ Returns     : boolean
+ Description : Joins the computer to the Active Directory domain configured for
+               the image.
+
+=cut
+
+sub ad_join {
+       my $self = shift;       
+       if (ref($self) !~ /windows/i) {
+               notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
+               return;
+       }
+       
+       # Calculate how long the tasks take
+       my $start_time = time;
+       my $rename_computer_reboot_duration = 0;
+       my $ad_join_reboot_duration = 0;
+       
+       my $computer_name       = $self->data->get_computer_short_name();
+       my $image_name  = $self->data->get_image_name();
+       
+       my $domain_dns_name = $self->data->get_image_domain_dns_name();
+       my $domain_username = $self->data->get_image_domain_username();
+       my $domain_password = $self->data->get_image_domain_password();
+       
+       my $computer_ou_dn = $self->get_ad_computer_ou_dn();
+       
+       if (!defined($domain_dns_name)) {
+               notify($ERRORS{'WARNING'}, 0, "unable to add $computer_name to 
AD, image $image_name is not assigned to a domain");
+               return;
+       }
+       elsif (!defined($domain_username)) {
+               notify($ERRORS{'WARNING'}, 0, "unable to add $computer_name to 
AD, user name is not configured for $domain_dns_name domain");
+               return;
+       }
+       elsif (!defined($domain_password)) {
+               notify($ERRORS{'WARNING'}, 0, "unable to add $computer_name to 
AD, password is not configured for $domain_dns_name domain");
+               return;
+       }
+       
+       # Make sure the computer is not already a member of a domain
+       # TODO: add logic to check if computer belongs to the correct domain in 
the correct OU
+       # If not, remove and rejoin
+       my $current_domain_name = $self->ad_get_current_domain();
+       if ($current_domain_name) {
+               if ($current_domain_name ne $domain_dns_name) {
+                       notify($ERRORS{'WARNING'}, 0, "unable to add 
$computer_name to $domain_dns_name domain, it is already a member of a 
different domain: $current_domain_name");
+                       return;
+               }
+               
+               # Search for the computer object in the domain
+               my $current_computer_dn = $self->ad_search_computer();
+               if (!$current_computer_dn) {
+                       notify($ERRORS{'WARNING'}, 0, "unable to add 
$computer_name to $domain_dns_name domain, it appears to already a member of 
the domain but the current DN could not be determined");
+                       return;
+               }
+               
+               # Extract the OU DN from the computer DN
+               my ($current_computer_ou_dn) = $current_computer_dn =~ 
/^[^,]+,(OU=.+)$/;
+               if (!$current_computer_ou_dn) {
+                       notify($ERRORS{'WARNING'}, 0, "unable to add 
$computer_name to $domain_dns_name domain, failed to parse OU DN from current 
computer DN: $current_computer_dn");
+                       return;
+               }
+               
+               if ($current_computer_ou_dn =~ /^$computer_ou_dn$/i) {
+                       notify($ERRORS{'OK'}, 0, "$computer_name is already 
joined to $domain_dns_name domain and in the correct OU: 
$current_computer_ou_dn");
+                       return 1;
+               }
+               else {
+                       notify($ERRORS{'WARNING'}, 0, "$computer_name is 
already joined to $domain_dns_name domain but in the a different OU:\n" .
+                               "correct OU: $computer_ou_dn\n" .
+                               "current OU: $current_computer_ou_dn"
+                       );
+                       $self->ad_unjoin() || return;
+               }
+       }
+       
+       # Figure out/fix the computer OU and assemble optional section to add 
to PowerShell command
+       my $domain_computer_command_section = '';
+       if ($computer_ou_dn) {
+               $domain_computer_command_section = "-OUPath 
\"$computer_ou_dn\"";
+       }
+       
+       notify($ERRORS{'DEBUG'}, 0, "attempting to join $computer_name to AD\n" 
.
+               "domain DNS name: $domain_dns_name\n" .
+               "domain user: $domain_username\n" .
+               "domain password: $domain_password\n" .
+               "domain computer OU DN: " . ($computer_ou_dn ? $computer_ou_dn 
: '<not configured>')
+       );
+
+       # Perform preparation tasks
+       $self->ad_join_prepare() || return;
+       
+       # Assemble the PowerShell script
+       my $ad_powershell_script = <<EOF;
+\$ps_credential = New-Object 
System.Management.Automation.PsCredential("$domain_dns_name\\$domain_username", 
(ConvertTo-SecureString "$domain_password" -AsPlainText -Force))
+Add-Computer -DomainName "$domain_dns_name" -Credential \$ps_credential 
$domain_computer_command_section -Verbose -ErrorAction Stop
+EOF
+
+       # Check if the computer needs to be renamed
+       my $current_computer_hostname = $self->get_current_computer_hostname() 
|| '<unknown>';
+       if (lc($current_computer_hostname) ne lc($computer_name)) {
+               notify($ERRORS{'DEBUG'}, 0, "$computer_name needs to be 
renamed, current hostname: '$current_computer_hostname'");
+               
+               # Check if computer supports PowerShell Rename-Computer cmdlet
+               # If it does, computer can be renamed and joined to AD in 1 
step with 1 reboot
+               # Otherwise, computer name needs to be changed, rebooted, then 
added to AD
+               my $powershell_supports_rename = 
$self->powershell_command_exists('Rename-Computer');
+               if ($powershell_supports_rename) {
+                       $ad_powershell_script .= "Rename-Computer -NewName 
$computer_name -DomainCredential \$ps_credential -Force -Verbose";
+               }
+               else {
+                       notify($ERRORS{'DEBUG'}, 0, "PowerShell version on 
$computer_name does NOT support Rename-Computer, renaming computer");
+                       if (!$self->set_computer_hostname()) {
+                               notify($ERRORS{'WARNING'}, 0, "failed to join 
$computer_name to Active Directory domain, PowerShell version does NOT support 
Rename-Computer, failed to rename using traditional method");
+                               return;
+                       }
+                       
+                       my $rename_computer_reboot_start = time;
+                       if (!$self->reboot(300, 3, 1)) {
+                               notify($ERRORS{'WARNING'}, 0, "failed to join 
$computer_name to Active Directory domain, failed to reboot computer after it 
was renamed");
+                               return;
+                       }
+                       $rename_computer_reboot_duration = (time - 
$rename_computer_reboot_start);
+               }
+       }
+
+       # Success:
+       # WARNING: The changes will take effect after you restart the computer
+       # VCLV98-248.
+       
+       # Possible errors:
+       
+       # File C:\Users\Administrator\Desktop\ad_join.ps1 cannot be loaded 
because
+       # the execution of scripts is disabled on this system. Please see 
"get-help
+       # about_signing" for more details.
+       
+       # OU doesn't exist:
+       # Add-Computer : This command cannot be executed on target
+       # computer('VCLV98-248') due to following error: The system cannot find 
the
+       # file specified.
+       
+       # Could happen if DNS isn't properly configured:
+       # This command cannot be executed on target computer('WIN7') due to 
following
+       # error: The specified domain either does not exist or could not be
+       # contacted.
+       
+       # Computer already added to AD in another OU:
+       # Add-Computer : This command cannot be executed on target
+       # computer('VCLV98-247') due to following error: The account already 
exists.
+       
+       my ($exit_status, $output) = 
$self->run_powershell_as_script($ad_powershell_script, 0, 1);
+       if (!defined($output)) {
+               notify($ERRORS{'WARNING'}, 0, "failed to execute PowerShell 
script to join $computer_name to Active Directory domain");
+               return;
+       }
+       
+       # Combine the output lines into a single string or else unpredictable 
text wrapping may occur
+       my $output_string = join(' ', @$output);
+       $output_string =~ s/\s+/ /g;
+       
+       if ($output_string =~ /(error:|does not exist|cannot be loaded)/i) {
+               notify($ERRORS{'WARNING'}, 0, "failed to join $computer_name to 
Active Directory domain, output:\n$output_string");
+               return 0;
+       }
+       else {
+               notify($ERRORS{'OK'}, 0, "executed PowerShell script to join 
$computer_name to Active Directory domain, output:\n" . join("\n", @$output));
+       }
+
+       # Reboot, computer should be joined to AD with the correct hostname
+       # If computer had to be rebooted to be renamed, certain tasks in 
reboot() don't need to be performed again
+       # Set reboot()'s last $pre_configure flag accordingly
+       my $ad_join_reboot_pre_configure = ($rename_computer_reboot_duration ? 
0 : 1);
+       
+       my $ad_join_reboot_start = time;
+       if (!$self->reboot(300, 3, 1, $ad_join_reboot_pre_configure)) {
+               notify($ERRORS{'WARNING'}, 0, "failed to join $computer_name to 
Active Directory domain, failed to reboot computer after it joined the domain");
+               return;
+       }
+       $ad_join_reboot_duration = (time - $ad_join_reboot_start);
+       
+       my $total_duration = (time - $start_time);
+       my $other_tasks_duration = ($total_duration - 
$rename_computer_reboot_duration - $ad_join_reboot_duration);
+       
+       notify($ERRORS{'DEBUG'}, 0, "successfully joined $computer_name to 
Active Directory domain: $domain_dns_name, time statistics:\n" .
+               "computer rename reboot : $rename_computer_reboot_duration 
seconds\n" .
+               "AD join reboot         : $ad_join_reboot_duration seconds\n" .
+               "other tasks            : $other_tasks_duration seconds\n" .
+               "total                  : $total_duration seconds"
+       );
+       return 1;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 ad_unjoin
+
+ Parameters  : none
+ Returns     : boolean
+ Description : Unjoins the computer from the Active Directory domain by
+               attempting to add the computer to a workgroup named 'VCL'. If
+               successful, the computer object is deleted from the domain.
+
+=cut
+
+sub ad_unjoin {
+       my $self = shift;       
+       if (ref($self) !~ /windows/i) {
+               notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
+               return;
+       }
+       
+       my $computer_name       = $self->data->get_computer_short_name();
+       my $image_name  = $self->data->get_image_name();
+       
+       my $domain_dns_name = $self->data->get_image_domain_dns_name();
+       my $domain_username = $self->data->get_image_domain_username();
+       my $domain_password = $self->data->get_image_domain_password();
+       
+       if (!defined($domain_dns_name)) {
+               notify($ERRORS{'WARNING'}, 0, "unable to remove $computer_name 
from AD, image $image_name is not assigned to a domain");
+               return;
+       }
+       elsif (!defined($domain_username)) {
+               notify($ERRORS{'WARNING'}, 0, "unable to remove $computer_name 
from AD, user name is not configured for $domain_dns_name domain");
+               return;
+       }
+       elsif (!defined($domain_password)) {
+               notify($ERRORS{'WARNING'}, 0, "unable to remove $computer_name 
from AD, password is not configured for $domain_dns_name domain");
+               return;
+       }
+       
+       if (!$self->ad_get_current_domain()) {
+               notify($ERRORS{'DEBUG'}, 0, "$computer_name does not need to be 
removed from AD because it is not currently joined to a domain");
+               return 1;
+       }
+       
+       notify($ERRORS{'DEBUG'}, 0, "attempting to unjoin $computer_name from 
AD");
+       
+       # Assemble the PowerShell script
+       my $ad_powershell_script = <<EOF;
+\$Host.UI.RawUI.BufferSize = New-Object Management.Automation.Host.Size(5000, 
500)
+\$ps_credential = New-Object 
System.Management.Automation.PsCredential("$domain_dns_name\\$domain_username", 
(ConvertTo-SecureString "$domain_password" -AsPlainText -Force))
+try {
+   Add-Computer -WorkgroupName VCL -Credential \$ps_credential -ErrorAction 
Stop
+}
+catch {
+   Write-Host "ERROR: failed to add computer to workgroup, error: 
\$(\$_.Exception.Message)"
+   exit 1
+}
+EOF
+
+       my ($exit_status, $output) = 
$self->run_powershell_as_script($ad_powershell_script, 1, 1);
+       if (!defined($output)) {
+               notify($ERRORS{'WARNING'}, 0, "failed to execute PowerShell 
script to remove $computer_name from Active Directory domain");
+               return;
+       }
+       elsif (grep(/ERROR/, @$output)) {
+               # Computer object was already or deleted or can't be found for 
some reason:
+               #   This command cannot be executed on target computer('') due 
to following error: No mapping between account names and security IDs was done.
+               if (grep(/No mapping between account names/, @$output)) {
+                       notify($ERRORS{'WARNING'}, 0, "failed to remove 
$computer_name from Active Directory domain, the computer object may have been 
deleted from the domain, output:\n" . join("\n", @$output));
+               }
+               else {
+                       notify($ERRORS{'WARNING'}, 0, "failed to remove 
$computer_name from Active Directory domain, output:\n" . join("\n", @$output));
+               }
+               return 0;
+       }
+       
+       notify($ERRORS{'OK'}, 0, "removed $computer_name from Active Directory 
domain, output:\n" . join("\n", @$output));
+       
+       if (!$self->reboot(300, 3, 1)) {
+               notify($ERRORS{'WARNING'}, 0, "failed to remove $computer_name 
from Active Directory domain, failed to reboot computer after unjoining 
domain");
+               return;
+       }
+       
+       $self->ad_delete_computer($computer_name);
+       return 1;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 ad_get_current_domain
+
+ Parameters  : none
+ Returns     : boolean
+ Description : Checks if the computer is joined to any Active Directory domain.
+
+=cut
+
+sub ad_get_current_domain {
+       my $self = shift;       
+       if (ref($self) !~ /windows/i) {
+               notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a 
function, it must be called as a class method");
+               return;
+       }
+       
+       my $computer_name       = $self->data->get_computer_short_name();
+       my $system32_path = $self->get_system32_path() || return;
+       
+       my $command = "echo | $system32_path/Wbem/wmic.exe COMPUTERSYSTEM GET 
PartOfDomain,Domain /FORMAT:LIST";
+       my ($exit_status, $output) = $self->execute($command);
+       if (!defined($output)) {
+               notify($ERRORS{'WARNING'}, 0, "failed to execute command to 
determine if $computer_name is joined to a domain: $command");
+               return;
+       }
+       
+       my ($part_of_domain_line) = grep(/^PartOfDomain/i, @$output);
+       if (!$part_of_domain_line) {
+               notify($ERRORS{'WARNING'}, 0, "unable to determine if 
$computer_name is joined to a domain, output does not contain a 'PartOfDomain' 
line:\n" . join("\n", @$output));
+               return;
+       }
+       elsif ($part_of_domain_line =~ /FALSE/i) {
+               notify($ERRORS{'DEBUG'}, 0, "$computer_name is NOT joined to a 
domain, output:\n" . join("\n", @$output));
+               return 0;
+       }
+       
+       my ($domain_line) = grep(/^Domain/i, @$output);
+       my ($domain_name) = $domain_line =~ /Domain=(.+)/;
+       if ($domain_name) {
+               notify($ERRORS{'DEBUG'}, 0, "$computer_name is joined to a 
domain, returning '$domain_name'\n" . join("\n", @$output));
+               return $domain_name;
+       }
+       else {
+               notify($ERRORS{'WARNING'}, 0, "$computer_name is joined to a 
domain but 'Domain=' line could not be parsed from the output, returning 1:\n" 
. join("\n", @$output));
+               return 1;
+       }
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 ad_search
+
+ Parameters  : $ldap_filter_argument, $attempt_limit (optional)

[... 399 lines stripped ...]

Reply via email to