blob: f65c241372c440cd76d3e5911bbe35336c11029c [file] [log] [blame]
#!/usr/bin/perl -w
###############################################################################
# $Id$
###############################################################################
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###############################################################################
=head1 NAME
VCL::Module::OS::Windows - Windows OS support module
=head1 SYNOPSIS
Needs to be written
=head1 DESCRIPTION
This module provides...
=cut
###############################################################################
package VCL::Module::OS::Windows;
# Specify the lib path using FindBin
use FindBin;
use lib "$FindBin::Bin/../../..";
# Configure inheritance
use base qw(VCL::Module::OS);
# Specify the version of this module
our $VERSION = '2.5.1';
# Specify the version of Perl to use
use 5.008000;
use strict;
use warnings;
use diagnostics;
use Encode;
use English '-no_match_vars';
use File::Basename;
use MIME::Base64;
use Net::Netmask;
use Text::CSV_XS;
use VCL::utils;
###############################################################################
=head1 CLASS VARIABLES
=cut
=head2 $SOURCE_CONFIGURATION_DIRECTORY
Data type : String
Description : Location on the management node of the files specific to this OS
module which are needed to configure the loaded OS on a computer.
This is normally the directory under 'tools' named after this OS
module.
Example:
/usr/local/vcl/tools/Windows
=cut
our $SOURCE_CONFIGURATION_DIRECTORY = "$TOOLS/Windows";
=head2 $NODE_CONFIGURATION_DIRECTORY
Data type : String
Description : Location on computer on which an image has been loaded where
configuration files reside. The files residing on the managment
node in the directory specified by $NODE_CONFIGURATION_DIRECTORY
are copied to this directory.
Example:
C:\Cygwin\home\root\VCL
=cut
our $NODE_CONFIGURATION_DIRECTORY = 'C:/Cygwin/home/root/VCL';
=head2 %TIME_ZONE_INFO
Data type : Hash
Description : Windows time zone code information. The hash keys are the
numerical Windows time zone codes used for things such as
Sysprep.
=cut
our %TIME_ZONE_INFO = (
'Afghanistan Standard Time' => {'abbreviation' => 'KAB', 'offset' => '+04:30', 'code' => '175'},
'Alaskan Standard Time' => {'abbreviation' => 'ALA', 'offset' => '-09:00', 'code' => '3'},
'Arab Standard Time' => {'abbreviation' => 'BKR', 'offset' => '+03:00', 'code' => '150'},
'Arabian Standard Time' => {'abbreviation' => 'ABT', 'offset' => '+04:00', 'code' => '165'},
'Arabic Standard Time' => {'abbreviation' => 'BKR', 'offset' => '+03:00', 'code' => '158'},
'Atlantic Standard Time' => {'abbreviation' => 'AST', 'offset' => '-04:00', 'code' => '50'},
'AUS Central Standard Time' => {'abbreviation' => 'ADA', 'offset' => '+09:30', 'code' => '245'},
'AUS Eastern Standard Time' => {'abbreviation' => 'CMS', 'offset' => '+10:00', 'code' => '255'},
'Azerbaijan Standard Time' => {'abbreviation' => undef, 'offset' => '+04:00', 'code' => undef},
'Azores Standard Time' => {'abbreviation' => 'AZO', 'offset' => '-01:00', 'code' => '80'},
'Canada Central Standard Time' => {'abbreviation' => 'CST', 'offset' => '-06:00', 'code' => '25'},
'Cape Verde Standard Time' => {'abbreviation' => 'AZO', 'offset' => '-01:00', 'code' => '83'},
'Caucasus Standard Time' => {'abbreviation' => 'ABT', 'offset' => '+04:00', 'code' => '170'},
'Cen. Australia Standard Time' => {'abbreviation' => 'ADA', 'offset' => '+09:30', 'code' => '250'},
'Central America Standard Time' => {'abbreviation' => 'CST', 'offset' => '-06:00', 'code' => '33'},
'Central Asia Standard Time' => {'abbreviation' => 'ADC', 'offset' => '+06:00', 'code' => '195'},
'Central Brazilian Standard Time' => {'abbreviation' => undef, 'offset' => '-04:00', 'code' => undef},
'Central Europe Standard Time' => {'abbreviation' => 'AMS', 'offset' => '+01:00', 'code' => '95'},
'Central European Standard Time' => {'abbreviation' => 'AMS', 'offset' => '+01:00', 'code' => '100'},
'Central Pacific Standard Time' => {'abbreviation' => 'MSN', 'offset' => '+11:00', 'code' => '280'},
'Central Standard Time' => {'abbreviation' => 'CST', 'offset' => '-06:00', 'code' => '20'},
'Central Standard Time (Mexico)' => {'abbreviation' => 'CST', 'offset' => '-06:00', 'code' => '30'},
'China Standard Time' => {'abbreviation' => 'SST', 'offset' => '+08:00', 'code' => '210'},
'Dateline Standard Time' => {'abbreviation' => 'IDLE', 'offset' => '-12:00', 'code' => '0'},
'E. Africa Standard Time' => {'abbreviation' => 'BKR', 'offset' => '+03:00', 'code' => '155'},
'E. Australia Standard Time' => {'abbreviation' => 'BGP', 'offset' => '+10:00', 'code' => '260'},
'E. Europe Standard Time' => {'abbreviation' => 'BCP', 'offset' => '+02:00', 'code' => '115'},
'E. South America Standard Time' => {'abbreviation' => 'BBA', 'offset' => '-03:00', 'code' => '65'},
'Eastern Standard Time' => {'abbreviation' => 'EST', 'offset' => '-05:00', 'code' => '35'},
'Egypt Standard Time' => {'abbreviation' => 'BCP', 'offset' => '+02:00', 'code' => '120'},
'Ekaterinburg Standard Time' => {'abbreviation' => 'EIK', 'offset' => '+05:00', 'code' => '180'},
'Fiji Standard Time' => {'abbreviation' => 'FKM', 'offset' => '+12:00', 'code' => '285'},
'FLE Standard Time' => {'abbreviation' => 'HRI', 'offset' => '+02:00', 'code' => '125'},
'Georgian Standard Time' => {'abbreviation' => undef, 'offset' => '+04:00', 'code' => undef},
'GMT Standard Time' => {'abbreviation' => 'GMT', 'offset' => '+00:00', 'code' => '85'},
'Greenland Standard Time' => {'abbreviation' => 'BBA', 'offset' => '-03:00', 'code' => '73'},
'Greenwich Standard Time' => {'abbreviation' => 'GMT', 'offset' => '+00:00', 'code' => '90'},
'GTB Standard Time' => {'abbreviation' => 'AIM', 'offset' => '+02:00', 'code' => '130'},
'Hawaiian Standard Time' => {'abbreviation' => 'HAW', 'offset' => '-10:00', 'code' => '2'},
'India Standard Time' => {'abbreviation' => 'BCD', 'offset' => '+05:30', 'code' => '190'},
'Iran Standard Time' => {'abbreviation' => 'THE', 'offset' => '+03:30', 'code' => '160'},
'Israel Standard Time' => {'abbreviation' => 'BCP', 'offset' => '+02:00', 'code' => '135'},
'Korea Standard Time' => {'abbreviation' => 'SYA', 'offset' => '+09:00', 'code' => '230'},
'Mid-Atlantic Standard Time' => {'abbreviation' => 'MAT', 'offset' => '-02:00', 'code' => '75'},
'Mountain Standard Time' => {'abbreviation' => 'MST', 'offset' => '-07:00', 'code' => '10'},
'Mountain Standard Time (Mexico)' => {'abbreviation' => 'MST', 'offset' => '-07:00', 'code' => '13'},
'Myanmar Standard Time' => {'abbreviation' => 'MMT', 'offset' => '+06:30', 'code' => '203'},
'N. Central Asia Standard Time' => {'abbreviation' => 'ADC', 'offset' => '+06:00', 'code' => '201'},
'Namibia Standard Time' => {'abbreviation' => undef, 'offset' => '+02:00', 'code' => undef},
'Nepal Standard Time' => {'abbreviation' => 'NPT', 'offset' => '+05:45', 'code' => '193'},
'New Zealand Standard Time' => {'abbreviation' => 'AWE', 'offset' => '+12:00', 'code' => '290'},
'Newfoundland Standard Time' => {'abbreviation' => 'NWF', 'offset' => '-03:30', 'code' => '60'},
'North Asia East Standard Time' => {'abbreviation' => 'SST', 'offset' => '+08:00', 'code' => '227'},
'North Asia Standard Time' => {'abbreviation' => 'BHJ', 'offset' => '+07:00', 'code' => '207'},
'Pacific SA Standard Time' => {'abbreviation' => 'AST', 'offset' => '-04:00', 'code' => '56'},
'Pacific Standard Time' => {'abbreviation' => 'PST', 'offset' => '-08:00', 'code' => '4'},
'Romance Standard Time' => {'abbreviation' => 'AMS', 'offset' => '+01:00', 'code' => '105'},
'Russian Standard Time' => {'abbreviation' => 'MSV', 'offset' => '+03:00', 'code' => '145'},
'SA Eastern Standard Time' => {'abbreviation' => 'BBA', 'offset' => '-03:00', 'code' => '70'},
'SA Pacific Standard Time' => {'abbreviation' => 'EST', 'offset' => '-05:00', 'code' => '45'},
'SA Western Standard Time' => {'abbreviation' => 'AST', 'offset' => '-04:00', 'code' => '55'},
'Samoa Standard Time' => {'abbreviation' => 'MIS', 'offset' => '-11:00', 'code' => '1'},
'SE Asia Standard Time' => {'abbreviation' => 'BHJ', 'offset' => '+07:00', 'code' => '205'},
'Singapore Standard Time' => {'abbreviation' => 'SST', 'offset' => '+08:00', 'code' => '215'},
'South Africa Standard Time' => {'abbreviation' => 'BCP', 'offset' => '+02:00', 'code' => '140'},
'Sri Lanka Standard Time' => {'abbreviation' => 'ADC', 'offset' => '+06:00', 'code' => '200'},
'Taipei Standard Time' => {'abbreviation' => 'SST', 'offset' => '+08:00', 'code' => '220'},
'Tasmania Standard Time' => {'abbreviation' => 'HVL', 'offset' => '+10:00', 'code' => '265'},
'Tokyo Standard Time' => {'abbreviation' => 'OST', 'offset' => '+09:00', 'code' => '235'},
'Tonga Standard Time' => {'abbreviation' => 'TOT', 'offset' => '+13:00', 'code' => '300'},
'US Eastern Standard Time' => {'abbreviation' => 'EST', 'offset' => '-05:00', 'code' => '40'},
'US Mountain Standard Time' => {'abbreviation' => 'MST', 'offset' => '-07:00', 'code' => '15'},
'Vladivostok Standard Time' => {'abbreviation' => 'HVL', 'offset' => '+10:00', 'code' => '270'},
'W. Australia Standard Time' => {'abbreviation' => 'SST', 'offset' => '+08:00', 'code' => '225'},
'W. Central Africa Standard Time' => {'abbreviation' => 'AMS', 'offset' => '+01:00', 'code' => '113'},
'W. Europe Standard Time' => {'abbreviation' => 'AMS', 'offset' => '+01:00', 'code' => '110'},
'West Asia Standard Time' => {'abbreviation' => 'EIK', 'offset' => '+05:00', 'code' => '185'},
'West Pacific Standard Time' => {'abbreviation' => 'BGP', 'offset' => '+10:00', 'code' => '275'},
'Yakutsk Standard Time' => {'abbreviation' => 'SYA', 'offset' => '+09:00', 'code' => '240'},
);
=head2 $KMS_CLIENT_PRODUCT_KEYS
Data type : Hash reference
Description : Keys used to activate Windows using a KMS server. The key needs
to be configured before attempting to activate via KMS. Each
flavor of Windows uses a different KMS client key. These keys are
public information.
=cut
# These keys are publicly available:
# https://technet.microsoft.com/en-us/library/jj612867(v=ws.11).aspx
our $KMS_CLIENT_PRODUCT_KEYS = {
'Windows 10 Education' => 'NW6C2-QMPVW-D7KKK-3GKT6-VCFB2',
'Windows 10 Education N' => '2WH4N-8QGBV-H22JP-CT43Q-MDWWJ',
'Windows 10 Enterprise' => 'NPPR9-FWDCX-D2C8J-H872K-2YT43',
'Windows 10 Enterprise 2015 LTSB' => 'WNMTR-4C88C-JK8YV-HQ7T2-76DF9',
'Windows 10 Enterprise 2015 LTSB N' => '2F77B-TNFGY-69QQF-B8YKP-D69TJ',
'Windows 10 Enterprise 2016 LTSB' => 'DCPHK-NFMTC-H88MJ-PFHPY-QJ4BJ',
'Windows 10 Enterprise 2016 LTSB N' => 'QFFDN-GRT3P-VKWWX-X7T3R-8B639',
'Windows 10 Enterprise N' => 'DPH2V-TTNVB-4X9Q3-TJR4H-KHJW4',
'Windows 10 Professional' => 'W269N-WFGWX-YVC9B-4J6C9-T83GX',
'Windows 10 Professional N' => 'MH37W-N47XK-V7XM9-C7227-GCQG9',
'Windows 7 Enterprise' => '33PXH-7Y6KF-2VJC9-XBBR8-HVTHH',
'Windows 7 Enterprise E' => 'C29WB-22CC8-VJ326-GHFJW-H9DH4',
'Windows 7 Enterprise N' => 'YDRBP-3D83W-TY26F-D46B2-XCKRJ',
'Windows 7 Professional' => 'FJ82H-XT6CR-J8D7P-XQJJ2-GPDD4',
'Windows 7 Professional E' => 'W82YF-2Q76Y-63HXB-FGJG9-GF7QX',
'Windows 7 Professional N' => 'MRPKT-YTG23-K7D7T-X2JMM-QY7MG',
'Windows 8 Enterprise' => '32JNW-9KQ84-P47T8-D8GGY-CWCK7',
'Windows 8 Enterprise N' => 'JMNMF-RHW7P-DMY6X-RF3DR-X2BQT',
'Windows 8 Professional' => 'NG4HW-VH26C-733KW-K6F98-J8CK4',
'Windows 8 Professional N' => 'XCVCF-2NXM9-723PB-MHCB7-2RYQQ',
'Windows 8.1 Enterprise' => 'MHF9N-XY6XB-WVXMC-BTDCT-MKKG7',
'Windows 8.1 Enterprise N' => 'TT4HM-HN7YT-62K67-RGRQJ-JFFXW',
'Windows 8.1 Professional' => 'GCRJD-8NW9H-F2CDX-CCM8D-9D6T9',
'Windows 8.1 Professional N' => 'HMCNV-VVBFX-7HMBH-CTY9B-B4FXY',
'Windows Server 2008 Datacenter' => '7M67G-PC374-GR742-YH8V4-TCBY3',
'Windows Server 2008 Datacenter without Hyper-V' => '22XQ2-VRXRG-P8D42-K34TD-G3QQC',
'Windows Server 2008 Enterprise' => 'YQGMW-MPWTJ-34KDK-48M3W-X4Q6V',
'Windows Server 2008 Enterprise without Hyper-V' => '39BXF-X8Q23-P2WWT-38T2F-G3FPG',
'Windows Server 2008 for Itanium-Based Systems' => '4DWFP-JF3DJ-B7DTH-78FJB-PDRHK',
'Windows Server 2008 HPC' => 'RCTX3-KWVHP-BR6TB-RB6DM-6X7HP',
'Windows Server 2008 R2 Datacenter' => '74YFP-3QFB3-KQT8W-PMXWJ-7M648',
'Windows Server 2008 R2 Enterprise' => '489J6-VHDMP-X63PK-3K798-CPX3Y',
'Windows Server 2008 R2 for Itanium-based Systems' => 'GT63C-RJFQ3-4GMB6-BRFB9-CB83V',
'Windows Server 2008 R2 HPC edition' => 'TT8MH-CG224-D3D7Q-498W2-9QCTX',
'Windows Server 2008 R2 Standard' => 'YC6KT-GKW9T-YTKYR-T4X34-R7VHC',
'Windows Server 2008 R2 Web' => '6TPJF-RBVHG-WBW2R-86QPH-6RTM4',
'Windows Server 2008 Standard' => 'TM24T-X9RMF-VWXK6-X8JC9-BFGM2',
'Windows Server 2008 Standard without Hyper-V' => 'W7VD6-7JFBR-RX26B-YKQ3Y-6FFFJ',
'Windows Server 2012' => 'BN3D2-R7TKB-3YPBD-8DRP2-27GG4',
'Windows Server 2012 Country Specific' => '4K36P-JN4VD-GDC6V-KDT89-DYFKP',
'Windows Server 2012 Datacenter' => '48HP8-DN98B-MYWDG-T2DCC-8W83P',
'Windows Server 2012 MultiPoint Premium' => 'XNH6W-2V9GX-RGJ4K-Y8X6F-QGJ2G',
'Windows Server 2012 MultiPoint Standard' => 'HM7DN-YVMH3-46JC3-XYTG7-CYQJJ',
'Windows Server 2012 N' => '8N2M2-HWPGY-7PGT9-HGDD8-GVGGY',
'Windows Server 2012 R2 Datacenter' => 'W3GGN-FT8W3-Y4M27-J84CP-Q3VJ9',
'Windows Server 2012 R2 Essentials' => 'KNC87-3J2TX-XB4WP-VCPJV-M4FWM',
'Windows Server 2012 R2 Server Standard' => 'D2N9P-3P6X9-2R39C-7RTCD-MDVJX',
'Windows Server 2012 Server Standard' => 'XC9B7-NBPP2-83J2H-RHMBY-92BT4',
'Windows Server 2012 Single Language' => '2WN2H-YGCQR-KFX6K-CD6TF-84YXQ',
'Windows Server 2016 Datacenter' => 'CB7KF-BWN84-R7R2Y-793K2-8XDDG',
'Windows Server 2016 Essentials' => 'JCKRF-N37P4-C2D82-9YXRT-4M63B',
'Windows Server 2016 Standard' => 'WC2BQ-8NRM3-FDDYY-2BFGV-KHKQY',
'Windows Vista Business' => 'YFKBB-PQJJV-G996G-VWGXY-2V3X8',
'Windows Vista Business N' => 'HMBQG-8H2RH-C77VX-27R82-VMQBT',
'Windows Vista Enterprise' => 'VKK3X-68KWM-X2YGT-QR4M6-4BWMV',
'Windows Vista Enterprise N' => 'VTC42-BM838-43QHV-84HX6-XJXKV',
'Windows Web Server 2008' => 'WYR28-R7TFJ-3X2YQ-YCY4H-M249D',
};
###############################################################################
=head1 INTERFACE OBJECT METHODS
=cut
#//////////////////////////////////////////////////////////////////////////////
=head2 initialize
Parameters :
Returns :
Description :
=cut
sub initialize {
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;
}
notify($ERRORS{'DEBUG'}, 0, "beginning Windows module initialization");
my $request_state = $self->data->get_request_state_name();
# If the request state is reserved, retrieve the firewall configuration now to reduce a delay after the user clicks Connect
if ($request_state =~ /reserved/) {
notify($ERRORS{'DEBUG'}, 0, "request state is $request_state, caching firewall configuration to reduce delays later on");
$self->get_firewall_configuration('TCP');
}
notify($ERRORS{'DEBUG'}, 0, "Windows module initialization complete");
if ($self->can("SUPER::initialize")) {
return $self->SUPER::initialize();
}
else {
return 1;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 pre_capture
Parameters : Hash containing 'end_state' key
Returns : If successful: true
If failed: false
Description : Performs the steps necessary to prepare a Windows OS before an
image is captured.
This subroutine is called by a provisioning module's capture()
subroutine.
The steps performed are:
=over 3
=cut
sub pre_capture {
my $self = shift;
my $args = 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;
}
# Check if end_state argument was passed
if (defined $args->{end_state}) {
$self->{end_state} = $args->{end_state};
}
else {
$self->{end_state} = 'off';
}
my $computer_node_name = $self->data->get_computer_node_name();
my $image_os_install_type = $self->data->get_image_os_install_type();
# Call OS::pre_capture to perform the pre-capture tasks common to all OS's
if (!$self->SUPER::pre_capture($args)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute parent class pre_capture() subroutine");
return 0;
}
notify($ERRORS{'OK'}, 0, "beginning Windows image capture preparation tasks on $computer_node_name");
=item *
Disable RDP access from any IP address
=cut
if (!$self->firewall_disable_rdp()) {
notify($ERRORS{'WARNING'}, 0, "unable to disable RDP from all addresses");
return 0;
}
=item *
Log off all currently logged in users
=cut
if (!$self->logoff_users()) {
notify($ERRORS{'WARNING'}, 0, "unable to log off all currently logged in users on $computer_node_name");
return 0;
}
=item *
Apply Windows security templates
=cut
# This find any .inf security template files configured for the OS and run secedit.exe to apply them
if (!$self->apply_security_templates()) {
notify($ERRORS{'WARNING'}, 0, "unable to apply security templates");
return 0;
}
=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;
}
notify($ERRORS{'DEBUG'}, 0, "computer successfully unjoined from domain, rebooting for change to take effect");
# reboot if unjoin successful
if (!$self->reboot()) {
notify($ERRORS{'WARNING'}, 0, "failed to reboot after unjoining from domain");
}
}
=item *
Set Administrator account password to known value
=cut
if (!$self->set_password('Administrator', $WINDOWS_ROOT_PASSWORD)) {
notify($ERRORS{'WARNING'}, 0, "unable to set Administrator password");
return 0;
}
=item *
Set root account password to known value
=cut
if (!$self->set_password('root', $WINDOWS_ROOT_PASSWORD)) {
notify($ERRORS{'WARNING'}, 0, "unable to set root password");
return 0;
}
=item *
Delete the user assigned to this reservation
=cut
my $deleted_user_accounts = $self->delete_user_accounts();
if (!$deleted_user_accounts) {
notify($ERRORS{'DEBUG'}, 0, "unable to delete user accounts, will try again after reboot");
}
=item *
Set root as the owner of /home/root
=cut
if (!$self->set_file_owner('/home/root', 'root')) {
notify($ERRORS{'WARNING'}, 0, "unable to set root as the owner of /home/root");
return 0;
}
=item *
Enable DHCP on the private and public interfaces
=cut
if (!$self->enable_dhcp('public')) {
notify($ERRORS{'WARNING'}, 0, "failed to enable DHCP on the public interface");
return;
}
=item *
Copy the capture configuration files to the computer (scripts, utilities, drivers...)
=cut
if (!$self->copy_capture_configuration_files()) {
notify($ERRORS{'WARNING'}, 0, "unable to copy general Windows capture configuration files to $computer_node_name");
return 0;
}
=item *
Disable autoadminlogon before disabling the pagefile and rebooting
=cut
if (!$self->disable_autoadminlogon()) {
notify($ERRORS{'WARNING'}, 0, "unable to disable autoadminlogon");
return 0;
}
=item *
Disable Shutdown Event Tracker
=cut
if (!$self->disable_shutdown_event_tracker()) {
notify($ERRORS{'WARNING'}, 0, "unable to disable shutdown event tracker");
}
=item *
Disable System Restore
=cut
if (!$self->disable_system_restore()) {
notify($ERRORS{'WARNING'}, 0, "unable to disable system restore");
}
=item *
Disable hibernation
=cut
if (!$self->disable_hibernation()) {
notify($ERRORS{'WARNING'}, 0, "unable to disable hibernation");
}
=item *
Disable sleep
=cut
if (!$self->disable_sleep()) {
notify($ERRORS{'WARNING'}, 0, "unable to disable sleep");
}
=item *
Disable Windows Customer Experience Improvement program
=cut
if (!$self->disable_ceip()) {
notify($ERRORS{'WARNING'}, 0, "unable to disable Windows Customer Experience Improvement program");
}
=item *
Disable Internet Explorer configuration page
=cut
if (!$self->disable_ie_configuration_page()) {
notify($ERRORS{'WARNING'}, 0, "unable to disable IE configuration");
}
=item *
Disable Automatic Updates
=cut
if (!$self->disable_automatic_updates()) {
notify($ERRORS{'WARNING'}, 0, "unable to disable automatic updates");
}
=item *
Disable Security Center notifications
=cut
if (!$self->disable_security_center_notifications()) {
notify($ERRORS{'WARNING'}, 0, "unable to disable Security Center notifications");
}
=item *
Disable login screensaver if computer is a VM
=cut
if ($self->data->get_computer_vmhost_id(0)) {
if (!$self->disable_login_screensaver()) {
notify($ERRORS{'WARNING'}, 0, "unable to disable login screensaver");
}
}
=item *
Enable audio redirection for RDP sessions
=cut
if (!$self->enable_rdp_audio()) {
notify($ERRORS{'WARNING'}, 0, "unable to enable RDP audio redirection");
}
=item *
Enable client-compatible color depth for RDP sessions
=cut
if (!$self->enable_client_compatible_rdp_color_depth()) {
notify($ERRORS{'WARNING'}, 0, "unable to enable client-compatible color depth for RDP sessions");
}
=item *
Clean up the hard drive
=cut
if (!$self->clean_hard_drive()) {
notify($ERRORS{'WARNING'}, 0, "unable to clean unnecessary files the hard drive");
}
=item *
Disable the pagefile, reboot, and delete pagefile.sys
********* node reboots *********
=cut
# This will set the registry key to disable the pagefile, reboot, then delete pagefile.sys
# Calls the reboot() subroutine, which makes sure ssh service is set to auto and firewall is open for ssh
if (!$self->disable_pagefile()) {
notify($ERRORS{'WARNING'}, 0, "unable to disable pagefile");
return 0;
}
=item *
Delete the user assigned to this reservation if attempt before reboot failed
=cut
if (!$deleted_user_accounts && !$self->delete_user_accounts()) {
notify($ERRORS{'WARNING'}, 0, "unable to delete user accounts after reboot");
return 0;
}
=item *
Enable SSH access from any IP address
=cut
if (!$self->firewall_enable_ssh()) {
notify($ERRORS{'WARNING'}, 0, "unable to enable SSH from any IP address");
return 0;
}
=item *
Enable ping from any IP address
=cut
if (!$self->firewall_enable_ping()) {
notify($ERRORS{'WARNING'}, 0, "unable to enable ping from any IP address");
return 0;
}
=item *
Reenable the pagefile
=cut
if (!$self->enable_pagefile()) {
notify($ERRORS{'WARNING'}, 0, "unable to reenable pagefile");
return 0;
}
=item *
Delete the 'VCL Update Cygwin' scheduled task if it exists. It could conflict with other post_load scripts.
=cut
$self->delete_scheduled_task('VCL Update Cygwin');
=item *
Set the Cygwin SSHD service startup mode to manual
=cut
if (!$self->set_service_startup_mode('sshd', 'manual') && !$self->set_service_startup_mode('cygsshd', 'manual')) {
notify($ERRORS{'WARNING'}, 0, "unable to set sshd service startup mode to manual");
return 0;
}
=item *
Unmount any NFS shares
=cut
if (!$self->unmount_nfs_shares()) {
notify($ERRORS{'WARNING'}, 0, "unable to unmount NFS shares");
return;
}
else {
notify($ERRORS{'DEBUG'}, 0, "Call to unmount_nfs_shares returned successfully");
}
=back
=cut
notify($ERRORS{'OK'}, 0, "returning 1");
return 1;
} ## end sub pre_capture
#//////////////////////////////////////////////////////////////////////////////
=head2 post_load
Parameters : None.
Returns : If successful: true
If failed: false
Description : Performs the steps necessary to configure a Windows OS after an
image has been loaded.
This subroutine is called by a provisioning module's load()
subroutine.
The steps performed are:
=over 3
=cut
sub post_load {
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_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");
=item *
Wait for computer to respond to SSH
=cut
if (!$self->wait_for_response(15, 900, 8)) {
notify($ERRORS{'WARNING'}, 0, "$computer_node_name never responded to SSH");
return 0;
}
=item *
Attempt to trigger and fix Cygwin's nodosfilewarning
=cut
$self->fix_cygwin_nodosfilewarning();
=item *
Wait for root to log off
=cut
if (!$self->wait_for_logoff('root', 2)) {
notify($ERRORS{'WARNING'}, 0, "root account never logged off");
if (!$self->logoff_users()) {
notify($ERRORS{'WARNING'}, 0, "failed to log off all currently logged in users");
}
}
=item *
Set root as the owner of /home/root
=cut
if (!$self->set_file_owner('/home/root', 'root')) {
notify($ERRORS{'WARNING'}, 0, "unable to set root as the owner of /home/root");
}
=item *
Set the Cygwin SSHD service startup mode to automatic
The Cygwin SSHD service startup mode should be set to automatic after an image
has been loaded and is ready to be reserved. Access will be lost if the service
is not set to automatic and the computer is rebooted.
=cut
if (!$self->set_service_startup_mode('sshd', 'auto') && !$self->set_service_startup_mode('cygsshd', 'auto')) {
notify($ERRORS{'WARNING'}, 0, "unable to set sshd service startup mode to auto");
return 0;
}
=item *
Update the SSH known_hosts file on the management node
=cut
if (!$self->update_ssh_known_hosts()) {
notify($ERRORS{'WARNING'}, 0, "unable to update the SSH known_hosts file on the management node");
}
=item *
Enable RDP access on the private network interface
=cut
if (!$self->firewall_enable_rdp_private()) {
notify($ERRORS{'WARNING'}, 0, "unable to enable RDP on private network");
return 0;
}
=item *
Enable SSH access on the private network interface
=cut
if (!$self->firewall_enable_ssh_private()) {
notify($ERRORS{'WARNING'}, 0, "unable to enable SSH from private IP address");
return 0;
}
=item *
Enable ping on the private network interface
=cut
if (!$self->firewall_enable_ping_private()) {
notify($ERRORS{'WARNING'}, 0, "unable to enable ping from private IP address");
return 0;
}
=item *
Check the image for user account names known to be bad or easily compromised
=cut
if (!$self->check_image()) {
notify($ERRORS{'WARNING'}, 0, "unable to check the image for user accounts known to be bad");
}
=item *
Update the public IP address
=cut
if (!$self->update_public_ip_address()) {
my $public_ip_configuration = $self->data->get_management_node_public_ip_configuration();
if ($public_ip_configuration =~ /dhcp/i) {
notify($ERRORS{'WARNING'}, 0, "computer should have received a public IP address from DHCP but the address could not be determined, attempting to execute 'ipconfig /renew'");
if (!$self->ipconfig_renew()) {
notify($ERRORS{'WARNING'}, 0, "public IP address from DHCP but the address could not be determined, 'ipconfig /renew' failed");
return;
}
# Try to update the public IP address again
if (!$self->update_public_ip_address()) {
notify($ERRORS{'WARNING'}, 0, "computer should have received a public IP address from DHCP but the address could not be determined on second attempt after executing 'ipconfig /renew'");
return;
}
else {
notify($ERRORS{'DEBUG'}, 0, "computer initially failed to obtain a public IP address from DHCP, executed 'ipconfig /renew', public IP address could then be determined");
}
}
else {
notify($ERRORS{'WARNING'}, 0, "management node failed to set a static public IP address on the computer");
return;
}
}
=item *
Set persistent public default route
=cut
if (!$self->set_static_default_gateway()) {
notify($ERRORS{'WARNING'}, 0, "unable to set persistent public default route");
}
=item *
Configure and synchronize time
=cut
if (!$self->configure_time_synchronization()) {
notify($ERRORS{'WARNING'}, 0, "unable to configure and synchronize time");
}
=item *
Set the "My Computer" description to the image pretty name
=cut
if (!$self->set_my_computer_name()) {
notify($ERRORS{'WARNING'}, 0, "failed to rename My Computer");
}
=item *
Disable the "Select a location for the network" prompt
=cut
$self->disable_set_network_location_prompt();
=item *
Check if the RDP port configured on the computer matches the RDP connect method
=cut
if (!$self->check_rdp_port_configuration()) {
return 0;
}
=item *
Remove the Windows root password and other private information from the VCL configuration files
=cut
if (!$self->sanitize_files()) {
notify($ERRORS{'WARNING'}, 0, "failed to sanitize the files on the computer");
return;
}
=item *
Randomize the root account password
=cut
my $root_random_password = getpw();
$self->{root_password} = $root_random_password;
if (!$self->set_password('root', $root_random_password)) {
notify($ERRORS{'WARNING'}, 0, "failed to set random root password");
return 0;
}
=item *
Randomize the Administrator account password
=cut
my $administrator_random_password = getpw();
if (!$self->set_password('Administrator', $administrator_random_password)) {
notify($ERRORS{'WARNING'}, 0, "failed to set random Administrator password");
return 0;
}
=item *
Disable sleep
=cut
if (!$self->disable_sleep()) {
notify($ERRORS{'WARNING'}, 0, "unable to disable sleep");
}
=item *
Install Windows updates saved under tools on the management node
=cut
if (!$self->install_updates()) {
notify($ERRORS{'WARNING'}, 0, "failed to run custom post_load scripts");
}
=item *
Join Active Directory domain if configured for image
=cut
if ($imagedomain_domaindnsname) {
if (!$self->ad_check()) {
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 *
Reboot the computer if necessary
=cut
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};
}
=item *
Run custom post_load scripts residing in the image
=cut
my $script_path = '$SYSTEMROOT/vcl_post_load.cmd';
if (!$self->file_exists($script_path)) {
notify($ERRORS{'DEBUG'}, 0, "custom post_load script does NOT exist in image: $script_path");
}
else {
$self->run_script($script_path);
}
=item *
Delete the VCL Post Load' scheduled task if it exists
=cut
# Make sure the 'VCL Post Load' task is removed up if it exists to avoid conflicts
$self->delete_scheduled_task('VCL Post Load');
=item *
Call OS.pm::post_load
=cut
return $self->SUPER::post_load();
=back
=cut
}
#//////////////////////////////////////////////////////////////////////////////
=head2 post_reserve
Parameters : none
Returns : boolean
Description : Runs $SYSTEMROOT/vcl_post_reserve.cmd if it exists in the image.
Does not check if the actual script succeeded or not.
=cut
sub post_reserve {
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 0;
}
# Check if custom post_reserve script exists in the image
my $script_path = '$SYSTEMROOT/vcl_post_reserve.cmd';
if ($self->file_exists($script_path)) {
# If post_reserve script exists, assume it does user or reservation-specific actions
# If the user never connects and the reservation times out, there's no way to revert these actions in order to clean the computer for another user
# Tag the image as tainted so it is reloaded
$self->set_tainted_status('post-reserve scripts residing in the image executed');
# Run the post_reserve script
$self->run_script($script_path);
}
else {
notify($ERRORS{'DEBUG'}, 0, "custom post_reserve script does NOT exist in image: $script_path");
}
return $self->SUPER::post_reserve();
}
#//////////////////////////////////////////////////////////////////////////////
=head2 post_reservation
Parameters : none
Returns : boolean
Description : Executes $SYSTEMROOT/vcl_post_reservation.cmd if it exists in the
image. Does not check if the actual script succeeded or not.
=cut
sub post_reservation {
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 0;
}
my $computer_name = $self->data->get_computer_short_name();
# Check if custom post_reservation script exists in image
my $script_path = '$SYSTEMROOT/vcl_post_reservation.cmd';
if ($self->file_exists($script_path)) {
# Run the post_reservation script
$self->run_script($script_path);
}
else {
notify($ERRORS{'DEBUG'}, 0, "custom post_reservation script does NOT exist in image: $script_path");
}
$self->SUPER::post_reservation();
# Check if the computer is joined to any AD domain
my $computer_current_domain_name = $self->ad_get_current_domain();
if ($computer_current_domain_name) {
$self->ad_delete_computer();
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 pre_reload
Parameters : none
Returns : true
Description : Unjoins the computer from an Active Directory domain if
previously joined. This helps avoid orphaned computer objects.
=cut
sub pre_reload {
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 0;
}
my $computer_name = $self->data->get_computer_short_name();
# Check if the computer is joined to any AD domain
my $computer_current_domain_name = $self->ad_get_current_domain();
if ($computer_current_domain_name) {
# check that node is not a domain controller
my $check_dc = $self->ad_check_domain_controller();
if (!defined($check_dc) || $check_dc == 0) {
# if call to ad_check_domain_controller fails, still attempt to
# delete from domain; unusual for node to be a domain controller
notify($ERRORS{'DEBUG'}, 0, "attempting to delete computer from domain");
$self->ad_delete_computer();
}
elsif ($check_dc == 1) {
notify($ERRORS{'DEBUG'}, 0, "computer is a domain controller, not attempting to delete computer from its own domain");
}
}
return $self->SUPER::pre_reload();
}
#//////////////////////////////////////////////////////////////////////////////
=head2 sanitize
Parameters :
Returns :
Description :
=cut
sub sanitize {
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_node_name = $self->data->get_computer_node_name();
# Revoke access
if (!$self->revoke_access()) {
notify($ERRORS{'WARNING'}, 0, "failed to revoke access to $computer_node_name");
return 0;
}
# Delete the reservation users
if ($self->delete_user_accounts()) {
notify($ERRORS{'OK'}, 0, "users deleted from $computer_node_name");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to delete users from $computer_node_name");
return 0;
}
if (!$self->unmount_nfs_shares()) {
notify($ERRORS{'WARNING'}, 0, "failed to unmount nfs shares");
return 0;
}
else {
notify($ERRORS{'DEBUG'}, 0, "successfully unmounted any NFS shares");
}
notify($ERRORS{'OK'}, 0, "$computer_node_name has been sanitized");
return 1;
} ## end sub sanitize
#//////////////////////////////////////////////////////////////////////////////
=head2 grant_access
Parameters :
Returns :
Description :
=cut
sub grant_access {
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_node_name = $self->data->get_computer_node_name();
my $system32_path = $self->get_system32_path();
if ($self->process_connect_methods("", 1)) {
notify($ERRORS{'OK'}, 0, "processed connection methods on $computer_node_name");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to process connection methods on $computer_node_name");
return;
}
notify($ERRORS{'OK'}, 0, "access has been granted for reservation on $computer_node_name");
return 1;
} ## end sub grant_access
#//////////////////////////////////////////////////////////////////////////////
=head2 revoke_access
Parameters :
Returns :
Description :
=cut
sub revoke_access {
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_node_name = $self->data->get_computer_node_name();
# Disallow RDP connections
if ($self->firewall_disable_rdp()) {
notify($ERRORS{'OK'}, 0, "firewall was configured to deny RDP access on $computer_node_name");
}
else {
notify($ERRORS{'WARNING'}, 0, "firewall could not be configured to deny RDP access on $computer_node_name");
return 0;
}
notify($ERRORS{'OK'}, 0, "access has been revoked to $computer_node_name");
return 1;
} ## end sub revoke_access
###############################################################################
=head1 AUXILIARY OBJECT METHODS
=cut
#//////////////////////////////////////////////////////////////////////////////
=head2 create_directory
Parameters :
Returns :
Description :
=cut
sub create_directory {
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_node_name = $self->data->get_computer_node_name();
my $path = shift;
if (!$path) {
notify($ERRORS{'WARNING'}, 0, "directory path argument was not specified");
return;
}
# If ~ is passed as the directory path, skip directory creation attempt
# The command will create a /root/~ directory since the path is enclosed in quotes
return 1 if $path eq '~';
notify($ERRORS{'DEBUG'}, 0, "attempting to create directory: '$path'");
# Assemble the Windows shell mkdir command and execute it
my $mkdir_command = "cmd.exe /c \"mkdir \\\"$path\\\"\"";
my ($mkdir_exit_status, $mkdir_output) = $self->execute($mkdir_command);
if (!defined($mkdir_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to create directory on $computer_node_name: $path");
return;
}
elsif ($mkdir_exit_status == 0) {
notify($ERRORS{'OK'}, 0, "created directory on $computer_node_name: '$path'");
}
elsif (grep(/already exists/i, @$mkdir_output)) {
notify($ERRORS{'OK'}, 0, "directory already exists on $computer_node_name: '$path'");
return 1;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to create directory on $computer_node_name: '$path', exit status: $mkdir_exit_status, output:\n" . join("\n", @$mkdir_output));
}
# Make sure directory was created
if (!$self->file_exists($path)) {
notify($ERRORS{'WARNING'}, 0, "directory does not exist on $computer_node_name: '$path'");
return 0;
}
else {
notify($ERRORS{'DEBUG'}, 0, "verified directory exists on $computer_node_name: '$path'");
return 1;
}
} ## end sub create_directory
#//////////////////////////////////////////////////////////////////////////////
=head2 delete_file
Parameters :
Returns :
Description :
=cut
sub delete_file {
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;
# Get file path subroutine argument
my $path_argument = shift;
if (!$path_argument) {
notify($ERRORS{'WARNING'}, 0, "file path was not specified as an argument");
return;
}
# Check if file exists before attempting to delete it
if (!$self->file_exists($path_argument)) {
notify($ERRORS{'OK'}, 0, "file not deleted because it does not exist: '$path_argument'");
return 1;
}
my $path_unix = $self->format_path_unix($path_argument);
my $path_dos = $self->format_path_dos($path_argument);
notify($ERRORS{'DEBUG'}, 0, "attempting to delete file: '$path_argument'");
# Assemble a set of commands concatenated together
# Try to take ownership, set the permissions, then delete the file using both Cygwin bash and Windows commands
# This should allow files to be deleted with restrictive ownership, permissions, and attributes
my $path_unix_directory = parent_directory_path($path_unix);
my ($path_unix_pattern) = $path_unix =~ /\/?([^\/]+)$/;
my $command;
$command .= "echo ---";
$command .= " ; echo Calling chown.exe to change owner to root...";
$command .= " ; /usr/bin/chown.exe -Rv root $path_unix 2>&1";
$command .= " ; echo ---";
$command .= " ; echo Calling chmod.exe to change permissions to 777...";
$command .= " ; /usr/bin/chmod.exe -Rv 777 $path_unix 2>&1";
$command .= " ; echo ---";
$command .= " ; echo Calling \\\"rm.exe -rfv $path_unix\\\" to to delete file...";
$command .= " ; /usr/bin/rm.exe -rfv $path_unix 2>&1";
if ($path_unix_pattern =~ /\*/) {
$command .= " ; echo ---";
$command .= " ; echo Calling \\\"rm.exe -rfv $path_unix_directory/.$path_unix_pattern\\\" to to delete file...";
$command .= " ; /usr/bin/rm.exe -rfv $path_unix_directory/.$path_unix_pattern 2>&1";
}
# Add call to rmdir if the path does not contain a wildcard
# rmdir does not accept wildcards
if ($path_dos !~ /\*/) {
$command .= " ; echo ---";
$command .= " ; echo Calling \\\"cmd.exe /c rmdir $path_dos\\\" to to delete directory...";
$command .= " ; cmd.exe /c \"rmdir /s /q \\\"$path_dos\\\"\" 2>&1";
}
$command .= " ; echo ---";
$command .= " ; echo Calling \\\"cmd.exe /c del $path_dos\\\" to to delete file...";
$command .= " ; cmd.exe /c \"del /s /q /f /a \\\"$path_dos\\\" 2>&1\" 2>&1";
$command .= " ; echo ---";
$command .= " ; echo Calling \\\"cmd.exe /c dir $path_dos\\\" to to list remaining files...";
$command .= " ; cmd.exe /c \"dir /a /w \\\"$path_dos\\\"\" 2>&1";
$command .= " ; echo ---";
$command .= " ; date +%r";
# Run the command
my ($exit_status, $output) = $self->execute($command, 0);
if (!defined($exit_status)) {
notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to delete file: '$path_argument'");
return;
}
## Sleep 1 second before checking if file was deleted
#sleep 1;
# Check if file was deleted
if ($self->file_exists($path_argument)) {
notify($ERRORS{'WARNING'}, 0, "failed to delete file, it still exists: '$path_argument', command:\n$command\noutput:\n" . join("\n", @$output));
return;
}
else {
notify($ERRORS{'DEBUG'}, 0, "deleted file: '$path_argument'");
return 1;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 move_file
Parameters :
Returns :
Description :
=cut
sub move_file {
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 file path subroutine arguments
my $source_path = shift;
my $destination_path = shift;
if (!$source_path) {
notify($ERRORS{'WARNING'}, 0, "file source path was not specified as an argument");
return;
}
if (!$destination_path) {
notify($ERRORS{'WARNING'}, 0, "file destination path was not specified as an argument");
return;
}
# Replace backslashes with forward slashes
$source_path =~ s/\\+/\//gs;
$destination_path =~ s/\\+/\//gs;
notify($ERRORS{'DEBUG'}, 0, "attempting to move file: $source_path --> $destination_path");
# Assemble the Windows shell move command and execute it
my $move_command = "mv -fv \"$source_path\" \"$destination_path\"";
my ($move_exit_status, $move_output) = $self->execute($move_command, 1);
if (defined($move_exit_status) && $move_exit_status == 0) {
notify($ERRORS{'OK'}, 0, "file moved: $source_path --> $destination_path, output:\n@{$move_output}");
}
elsif ($move_exit_status) {
notify($ERRORS{'WARNING'}, 0, "failed to move file: $source_path --> $destination_path, exit status: $move_exit_status, output:\n@{$move_output}");
return;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to move file: $source_path --> $destination_path");
return;
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 delete_files_by_pattern
Parameters : $base_directory, $regex_pattern, $max_depth (optional)
Returns : boolean
Description : Deletes all files found under the base directory
=cut
sub delete_files_by_pattern {
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 ($base_directory, $regex_pattern, $max_depth, $show_deleted) = @_;
# Make sure base directory and pattern were specified
if (!($base_directory && $regex_pattern)) {
notify($ERRORS{'WARNING'}, 0, "base directory and pattern must be specified as arguments");
return;
}
my $computer_name = $self->data->get_computer_short_name();
notify($ERRORS{'DEBUG'}, 0, "attempting to delete files on $computer_name, base directory: '$base_directory', pattern: '$regex_pattern', max depth: " . ($max_depth ? $max_depth : 'unlimited'));
# Check if the path begins with an environment variable
my ($base_directory_variable, $remainder) = $base_directory =~ /(\$[^\/\\]*)(.*)/g;
if ($base_directory_variable) {
my $cygpath_command = "/bin/cygpath.exe \"$base_directory_variable\"";
my ($cygpath_exit_status, $cygpath_output) = $self->execute($cygpath_command, 0);
if (!defined($cygpath_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command to determine if $base_directory_variable environment variable is set on $computer_name: $cygpath_command");
return;
}
elsif (grep(/cygpath:/, @$cygpath_output)) {
notify($ERRORS{'DEBUG'}, 0, "files not deleted because $base_directory_variable environment variable is not set on $computer_name");
return;
}
elsif (!grep(/\w/, @$cygpath_output)) {
notify($ERRORS{'DEBUG'}, 0, "files not deleted because $base_directory_variable environment variable is empty on $computer_name");
return;
}
my ($base_directory_variable_value) = grep(/\w/, @$cygpath_output);
$remainder = '' unless defined($remainder);
my $base_directory_new = "$base_directory_variable_value/$remainder";
$base_directory_new =~ s/[\\\/]+/\//g;
notify($ERRORS{'DEBUG'}, 0, "$base_directory_variable environment variable is set on $computer_name: '$base_directory' --> '$base_directory_new'");
$base_directory = $base_directory_new;
}
# Remove trailing slashes from base directory
$base_directory =~ s/[\/\\]*$/\//;
# Assemble command
# Use find to locate all the files under the base directory matching the pattern specified
my $command = "/bin/find.exe \"$base_directory\"";
$command .= " -mindepth 1";
$command .= " -maxdepth $max_depth" if $max_depth;
$command .= " -iregex \"$regex_pattern\"";
$command .= " -exec chown -R root {} \\;";
$command .= " -exec chmod -R 777 {} \\;";
$command .= " -exec rm -rvf {} \\;";
my ($exit_status, $output) = $self->execute($command, 0);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to delete files under $base_directory matching pattern $regex_pattern, command: $command");
return;
}
elsif ($base_directory_variable && grep(/cygpath:/i, @$output)) {
notify($ERRORS{'OK'}, 0, "files not deleted because environment variable is not set: $base_directory_variable");
return;
}
elsif (grep(/find:.*no such file/i, @$output)) {
notify($ERRORS{'OK'}, 0, "files not deleted because base directory does not exist: $base_directory, command: '$command'");
return 1;
}
elsif (grep(/(^Usage:)/i, @$output)) {
notify($ERRORS{'WARNING'}, 0, "failed to delete files under $base_directory matching pattern $regex_pattern\ncommand: $command\noutput:\n" . join("\n", @$output));
return;
}
else {
my @deleted = grep(/removed /, @$output);
my @not_deleted = grep(/cannot remove/, @$output);
my $message;
$message .= "attempted to delete files:\n";
$message .= "base directory: $base_directory\n";
$message .= "regular expression pattern: $regex_pattern\n";
$message .= "files and directories deleted: " . scalar(@deleted) . "\n";
$message .= "files and directories NOT deleted: " . scalar(@not_deleted) . "\n";
$message .= "deleted:\n" . join("\n", @deleted) . "\n" if ($show_deleted && @deleted);
$message .= "NOT deleted:\n" . join("\n", @not_deleted) if (@not_deleted);
notify($ERRORS{'OK'}, 0, $message) if (@deleted || @not_deleted);
}
return 1;
} ## end sub delete_files_by_pattern
#//////////////////////////////////////////////////////////////////////////////
=head2 file_exists
Parameters :
Returns :
Description :
=cut
sub file_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 $computer_node_name = $self->data->get_computer_node_name();
# Get the path from the subroutine arguments and make sure it was passed
my $path = shift;
if (!$path) {
notify($ERRORS{'WARNING'}, 0, "unable to detmine if file exists, path was not specified as an argument");
return;
}
my $path_dos = $self->format_path_dos($path);
# Assemble the dir command and execute it
my $dir_command = "cmd.exe /c \"dir /a \\\"$path_dos\\\"\"";
my ($dir_exit_status, $dir_output) = $self->execute($dir_command, 0);
if (!defined($dir_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to determine if file exists on $computer_node_name: $path");
return;
}
# Checking if directory exists, no wildcard: (directory exists)
# $ cmd.exe /c "dir /a C:\test"
# Volume in drive C has no label.
# Volume Serial Number is 4C9E-6C37
#
# Directory of C:\test
#
#05/16/2012 01:19 PM <DIR> .
#05/16/2012 01:19 PM <DIR> ..
# 0 File(s) 0 bytes
# 2 Dir(s) 17,999,642,624 bytes free
# Checking if file or directory exists with wildcard: (file exists)
# $ cmd.exe /c "dir /a C:\te*"
# Volume in drive C has no label.
# Volume Serial Number is 4C9E-6C37
#
# Directory of C:\
#
#05/16/2012 01:19 PM <DIR> test
# 0 File(s) 0 bytes
# 1 Dir(s) 17,999,642,624 bytes free
# Checking if file exists with wildcard: (file does not exist)
# $ cmd.exe /c "dir /a C:\test\*"
# Volume in drive C has no label.
# Volume Serial Number is 4C9E-6C37
#
# Directory of C:\test
#
#05/16/2012 01:19 PM <DIR> .
#05/16/2012 01:19 PM <DIR> ..
# 0 File(s) 0 bytes
# 2 Dir(s) 17,999,642,624 bytes free
if ($dir_exit_status == 1 || grep(/(file not found|cannot find)/i, @$dir_output)) {
notify($ERRORS{'DEBUG'}, 0, "file does NOT exist on $computer_node_name: '$path'");
return 0;
}
elsif ($path =~ /\*/ && grep(/\s0 File/, @$dir_output) && grep(/\s2 Dir/, @$dir_output)) {
#notify($ERRORS{'DEBUG'}, 0, "file does NOT exist on $computer_node_name: '$path', exit status: $dir_exit_status, command: '$dir_command', output:\n" . join("\n", @$dir_output));
notify($ERRORS{'DEBUG'}, 0, "file does NOT exist on $computer_node_name: '$path'");
return 0;
}
else {
notify($ERRORS{'DEBUG'}, 0, "file exists on $computer_node_name: '$path'");
return 1;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 set_file_owner
Parameters : file path, owner
Returns : If successful: true
If failed: false
Description : Recursively sets the owner of the file path. The file path can
be a file or directory. The owner must be a valid user account. A
group can optionally be specified by appending a semicolon and
the group name to the owner.
Examples:
set_file_owner('/home/root', 'root')
set_file_owner('/home/root', 'root:Administrators')
=cut
sub set_file_owner {
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 file path argument
my $file_path = shift;
if (!$file_path) {
notify($ERRORS{'WARNING'}, 0, "file path argument was not specified");
return;
}
# Get the owner argument
my $owner = shift;
if (!$owner) {
notify($ERRORS{'WARNING'}, 0, "owner argument was not specified");
return;
}
# Run chown
my ($chown_exit_status, $chown_output) = $self->execute("/usr/bin/chown.exe -vR \"$owner\" \"$file_path\"", 0);
# Check if exit status is defined - if not, SSH command failed
if (!defined($chown_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to set $owner as the owner of $file_path");
return;
}
# Check if any known error lines exist in the chown output
my @chown_error_lines = grep(/(chown:|cannot access|no such file|failed to)/ig, @$chown_output);
if (@chown_error_lines) {
notify($ERRORS{'WARNING'}, 0, "error occurred setting $owner as the owner of $file_path, error output:\n" . join("\n", @chown_error_lines));
return;
}
# Make sure an "ownership of" line exists in the chown output
my @chown_success_lines = grep(/(ownership of)/ig, @$chown_output);
if (@chown_success_lines) {
notify($ERRORS{'OK'}, 0, "set $owner as the owner of $file_path, files and directories modified: " . scalar(@chown_success_lines));
}
else {
notify($ERRORS{'OK'}, 0, "$owner is already the owner of $file_path");
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 logoff_users
Parameters :
Returns :
Description :
=cut
sub logoff_users {
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_node_name = $self->data->get_computer_node_name();
my $system32_path = $self->get_system32_path() || return;
my ($exit_status, $output) = $self->execute("$system32_path/qwinsta.exe", 1, 60);
if ($exit_status > 0) {
notify($ERRORS{'WARNING'}, 0, "failed to run qwinsta.exe on $computer_node_name, exit status: $exit_status, output:\n" . join("\n", @$output));
return;
}
elsif (!defined($exit_status)) {
notify($ERRORS{'WARNING'}, 0, "failed to run qwinsta.exe command on $computer_node_name");
return;
}
# Find lines with the state = Active or Disc
# Disc will occur if the user disconnected the RDP session but didn't logoff
my @connection_lines = grep(/(Active)/, @$output);
return 1 if !@connection_lines;
#notify($ERRORS{'OK'}, 0, "connections on $computer_node_name:\n@connection_lines");
# SESSIONNAME USERNAME ID STATE TYPE DEVICE
# '> root 0 Disc rdpwd '
# '>rdp-tcp#24 root 0 Active rdpwd '
foreach my $connection_line (@connection_lines) {
$connection_line =~ s/(^\s+|\s+$)//g;
my ($session_id) = $connection_line =~ /(\d+)\s+(?:Active|Listen|Conn|Disc)/g;
my ($session_name) = $connection_line =~ /^\s?>?([^ ]+)/g;
# Determine if the session ID or name will be used to kill the session
# logoff.exe has trouble killing sessions with ID=0
# Use the ID if it's > 0, otherwise use the session name
my $session_identifier;
if ($session_id) {
$session_identifier = $session_id;
}
elsif ($session_name) {
$session_identifier = $session_name;
}
else {
notify($ERRORS{'WARNING'}, 0, "session ID or name could not be determined from line:\n$connection_line");
next;
}
notify($ERRORS{'DEBUG'}, 0, "attempting to kill connection $session_identifier: '$connection_line'");
#LOGOFF [sessionname | sessionid] [/SERVER:servername] [/V]
# sessionname The name of the session.
# sessionid The ID of the session.
# /SERVER:servername Specifies the Terminal server containing the user
# session to log off (default is current).
# /V Displays information about the actions performed.
# Call logoff.exe, pass it the session
my ($logoff_exit_status, $logoff_output) = $self->execute("$system32_path/logoff.exe $session_identifier /V");
if (!defined($logoff_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command to log off session: $session_identifier");
}
elsif ($logoff_exit_status == 0) {
notify($ERRORS{'OK'}, 0, "logged off session: $session_identifier, output:\n" . join("\n", @$logoff_output));
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to log off session: $session_identifier, exit status: $logoff_exit_status, output:\n" . join("\n", @$logoff_output));
}
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 user_exists
Parameters : $username (optional)
Returns : boolean
Description : Executes 'net user <username>' to determine whether or not a
local user exists on the Windows computer.
=cut
sub user_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 $computer_node_name = $self->data->get_computer_node_name();
my $system32_path = $self->get_system32_path() || return;
# Attempt to get the username from the arguments
# If no argument was supplied, use the user specified in the DataStructure
my $username = shift;
if (!$username) {
$username = $self->data->get_user_login_id();
}
#notify($ERRORS{'DEBUG'}, 0, "checking if user $username exists on $computer_node_name");
# Attempt to query the user account
my $command = "$system32_path/net.exe user \"$username\"";
my ($exit_status, $output) = $self->execute($command, 0);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command to determine if user exists on $computer_node_name: $username");
return;
}
# Expected output if user exists:
# $ net user administrator
# User name Administrator
# Full Name
# Comment Built-in account for administering the computer/domain
# ...
# The command completed successfully.
# Expected output if user does NOT exist:
# $ net user foo
# The user name could not be found.
#
# More help is available by typing NET HELPMSG 2221.
# Note: exit status may not be reliable, see VCL-1054
if ($exit_status == 2 || grep(/not.*found/i, @$output)) {
notify($ERRORS{'DEBUG'}, 0, "user '$username' does NOT exist on $computer_node_name, exit status: $exit_status, output:\n" . join("\n", @$output));
return 0;
}
elsif (my ($username_line) = grep(/User name[\s\t]+$username[\s\t]*$/i, @$output)) {
notify($ERRORS{'DEBUG'}, 0, "user '$username' exists on $computer_node_name, found matching line in '$command' output:\n$username_line");
return 1;
}
elsif ($exit_status == 0) {
notify($ERRORS{'WARNING'}, 0, "returning 1 but unable to reliable determine if user '$username' exists on $computer_node_name, exit status is $exit_status but output does not contain a 'User name <space...> $username' line:\n" . join("\n", @$output));
return 1;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to determine if user '$username' exists on $computer_node_name, exit status: $exit_status, output:\n" . join("\n", @$output));
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 create_user
Parameters : hash reference
Returns : boolean
Description : Creates a user on the computer. The argument must be a hash
reference containing the following keys:
* username
* password
* root_access
=cut
sub create_user {
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_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) {
notify($ERRORS{'WARNING'}, 0, "unable to create user, user parameters argument was not provided");
return;
}
elsif (!ref($user_parameters) || ref($user_parameters) ne 'HASH') {
notify($ERRORS{'WARNING'}, 0, "unable to create user, argument provided is not a hash reference");
return;
}
my $username = $user_parameters->{username};
if (!defined($username)) {
notify($ERRORS{'WARNING'}, 0, "failed to create user on $computer_node_name, argument hash does not contain a 'username' 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;
}
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;
}
# 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, command: '$add_user_command', 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;
}
}
}
if (!$self->add_user_to_group($username, "Remote Desktop Users")) {
return;
}
# Add user to Administrators group if necessary
if ($root_access) {
if (!$self->add_user_to_group($username, "Administrators")) {
return;
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "existing user NOT added to Administrators group on $computer_node_name: $username");
}
return 1;
} ## end sub create_user
#//////////////////////////////////////////////////////////////////////////////
=head2 add_user_to_group
Parameters : $username, $group
Returns : boolean
Description : Adds a user to a group on the computer.
=cut
sub add_user_to_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_node_name = $self->data->get_computer_node_name();
my $system32_path = $self->get_system32_path() || return;
# Attempt to get the username from the arguments
# If no argument was supplied, use the user specified in the DataStructure
my $username = shift;
my $group = shift;
if (!$username || !$group) {
notify($ERRORS{'WARNING'}, 0, "unable to add user to group, arguments were not passed correctly");
return;
}
# Attempt to add the user to the group using net.exe localgroup
my $localgroup_user_command = "$system32_path/net.exe localgroup \"$group\" $username /ADD";
my ($localgroup_user_exit_status, $localgroup_user_output) = $self->execute($localgroup_user_command);
if (defined($localgroup_user_exit_status) && $localgroup_user_exit_status == 0) {
notify($ERRORS{'OK'}, 0, "added user to '$group' group on $computer_node_name: $username");
}
elsif (defined($localgroup_user_exit_status) && $localgroup_user_exit_status == 2) {
# Exit status is 2, this could mean the user is already a member or that the group doesn't exist
# Check the output to determine what happened
if (grep(/error 1378/, @{$localgroup_user_output})) {
# System error 1378 has occurred.
# The specified account name is already a member of the group.
notify($ERRORS{'OK'}, 0, "user is already a member of '$group' group on $computer_node_name: $username");
return 1;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to add user to '$group' group on $computer_node_name: $username, exit status: $localgroup_user_exit_status, output:\n@{$localgroup_user_output}");
return 0;
}
} ## end elsif (defined($localgroup_user_exit_status) ... [ if (defined($localgroup_user_exit_status) ...
elsif (defined($localgroup_user_exit_status)) {
notify($ERRORS{'WARNING'}, 0, "failed to add user to '$group' group on $computer_node_name: $username, exit status: $localgroup_user_exit_status, output:\n@{$localgroup_user_output}");
return 0;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to execute command to add user to '$group' group on $computer_node_name: $username");
return;
}
return 1;
} ## end 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
Returns : 1 success 0 failure
Description : removes user account and profile directory from specificed node
=cut
sub delete_user {
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_node_name = $self->data->get_computer_node_name();
my $system32_path = $self->get_system32_path() || return;
# Attempt to get the username from the arguments
# If no argument was supplied, use the user specified in the DataStructure
my $username = shift;
if (!(defined($username))) {
$username = $self->data->get_user_login_id();
}
notify($ERRORS{'OK'}, 0, "attempting to delete user $username from $computer_node_name");
# Attempt to delete the user account
my $delete_user_command = "$system32_path/net.exe user $username /DELETE";
my ($delete_user_exit_status, $delete_user_output) = $self->execute($delete_user_command);
if (defined($delete_user_exit_status) && $delete_user_exit_status == 0) {
notify($ERRORS{'OK'}, 0, "deleted user $username from $computer_node_name");
}
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}");
return 0;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to run ssh command delete user $username from $computer_node_name");
return;
}
# Delete the user's home directory
if ($self->delete_file("C:/Documents and Settings/$username")) {
notify($ERRORS{'OK'}, 0, "deleted profile for user $username from $computer_node_name");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to delete profile for user $username from $computer_node_name");
return 0;
}
return 1;
} ## end sub delete_user
#//////////////////////////////////////////////////////////////////////////////
=head2 set_password
Parameters : $username, $password
Returns :
Description :
=cut
sub set_password {
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;
}
# IMPORTANT: be sure to test passwords containing the following:
# $!
# \ (single backslash)
# '
# "
# ~
# `
# Special bash/Linux variables: = $0, $1, $#, $*, $@, $-, $!, $_, $?, $$
# my $test_password = '$0, $1 $# $* $@ $- $! $_ $? $$\ !@#$%^&*()_+-={}[]":;<>?/.,`~' . "'";
my $computer_node_name = $self->data->get_computer_node_name();
my $system32_path = $self->get_system32_path() || return;
# Attempt to get the username from the arguments
my $username = shift;
my $password = shift;
my $user_password_only = shift;
# If no argument was supplied, use the user specified in the DataStructure
if (!defined($username)) {
$username = $self->data->get_user_logon_id();
}
if (!defined($password)) {
$password = $self->data->get_reservation_password();
}
# Make sure both the username and password were determined
if (!defined($username) || !defined($password)) {
notify($ERRORS{'WARNING'}, 0, "username and password could not be determined");
return 0;
}
# Attempt to set the password
my $password_escaped = _escape_password($password);
my $set_password_command = "$system32_path/net.exe user $username \"$password_escaped\"";
notify($ERRORS{'DEBUG'}, 0, "setting password of $username to '$password' on $computer_node_name, command:\n$set_password_command");
my ($set_password_exit_status, $set_password_output) = $self->execute($set_password_command);
if ($set_password_exit_status == 0) {
notify($ERRORS{'OK'}, 0, "password changed to '$password' for user '$username' on $computer_node_name, command: '$set_password_command', output:\n" . join("\n", @$set_password_output));
}
elsif (defined $set_password_exit_status) {
notify($ERRORS{'WARNING'}, 0, "failed to change password to '$password' (escaped: '$password_escaped') for user '$username' on $computer_node_name, exit status: $set_password_exit_status, command: '$set_password_command', output:\n" . join("\n", @$set_password_output));
return 0;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to execute command to change password to '$password' for user '$username' on $computer_node_name");
return 0;
}
return 1 if $user_password_only;
# Get the list of services
my @services = $self->get_services_using_login_id($username);
if ($username eq 'root' && !@services) {
@services = ('sshd', 'cygsshd');
}
my $success = 0;
for my $service (@services) {
notify($ERRORS{'DEBUG'}, 0, "$service service is configured to run as $username, updating service credentials");
if ($self->set_service_credentials($service, $username, $password)) {
$success = 1;
last;
}
}
if(!$success) {
my $servicelist = join(', ', @services);
notify($ERRORS{'WARNING'}, 0, "failed to set credentials to $username ($password) for any of $servicelist");
}
# Get the scheduled tasks - check if any are configured to run as the user
my $scheduled_task_info = $self->get_scheduled_task_info();
for my $task_name (keys %$scheduled_task_info) {
my $run_as_user = $scheduled_task_info->{$task_name}{'Run As User'};
if ($run_as_user && $run_as_user =~ /^(.+\\)?$username$/i) {
notify($ERRORS{'DEBUG'}, 0, "password needs to be updated for scheduled task '$task_name' set to run as user '$run_as_user'");
# Attempt to update the scheduled task credentials
# Don't return false if this fails - not extremely vital
if (!$self->set_scheduled_task_credentials($task_name, $username, $password)) {
notify($ERRORS{'WARNING'}, 0, "failed to set '$task_name' scheduled task credentials to $username ($password)");
}
}
}
notify($ERRORS{'OK'}, 0, "changed password for user: $username");
return 1;
} ## end 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
Returns :
Description :
=cut
sub enable_user {
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_node_name = $self->data->get_computer_node_name();
my $system32_path = $self->get_system32_path() || return;
# Attempt to get the username from the arguments
my $username = shift;
# If no argument was supplied, use the user specified in the DataStructure
if (!defined($username)) {
$username = $self->data->get_user_logon_id();
}
# Make sure the username was determined
if (!defined($username)) {
notify($ERRORS{'WARNING'}, 0, "username could not be determined");
return 0;
}
# Attempt to enable the user account (set ACTIVE=YES)
notify($ERRORS{'DEBUG'}, 0, "enabling user $username on $computer_node_name");
my ($enable_exit_status, $enable_output) = $self->execute("$system32_path/net.exe user $username /ACTIVE:YES");
if ($enable_exit_status == 0) {
notify($ERRORS{'OK'}, 0, "user $username enabled on $computer_node_name");
}
elsif ($enable_exit_status) {
notify($ERRORS{'WARNING'}, 0, "failed to enable user $username on $computer_node_name, exit status: $enable_exit_status, output:\n@{$enable_output}");
return 0;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to enable user $username on $computer_node_name");
return 0;
}
return 1;
} ## end sub enable_user
#//////////////////////////////////////////////////////////////////////////////
=head2 disable_user
Parameters : $username
Returns : boolean
Description : Disables the user account specified by the argument.
=cut
sub disable_user {
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_node_name = $self->data->get_computer_node_name();
# Attempt to get the username from the arguments
my $username = shift;
if (!defined($username)) {
notify($ERRORS{'WARNING'}, 0, "username argument was not supplied");
return;
}
# Attempt to enable the user account (set ACTIVE=NO)
notify($ERRORS{'DEBUG'}, 0, "disbling user $username on $computer_node_name");
my ($exit_status, $output) = $self->execute("net user $username /ACTIVE:NO");
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to run command to disable user $username on $computer_node_name");
return;
}
elsif (grep(/ successfully/, @$output)) {
notify($ERRORS{'OK'}, 0, "user $username disabled on $computer_node_name");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to disable user $username on $computer_node_name, exit status: $exit_status, output:\n" . join("\n", @$output));
return;
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 disable_pagefile
Parameters :
Returns :
Description :
=cut
sub disable_pagefile {
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;
# Set the registry key to blank
my $memory_management_key = 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management';
my $reg_add_command = $system32_path . '/reg.exe add "' . $memory_management_key . '" /v PagingFiles /d "" /t REG_MULTI_SZ /f';
my ($reg_add_exit_status, $reg_add_output) = $self->execute($reg_add_command, 1);
if (defined($reg_add_exit_status) && $reg_add_exit_status == 0) {
notify($ERRORS{'OK'}, 0, "set registry key to disable pagefile");
}
elsif (defined($reg_add_exit_status)) {
notify($ERRORS{'WARNING'}, 0, "failed to set registry key to disable pagefile, exit status: $reg_add_exit_status, output:\n@{$reg_add_output}");
return 0;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to set registry key to disable pagefile");
return;
}
# Attempt to reboot the computer in order to delete the pagefile
if ($self->reboot()) {
notify($ERRORS{'DEBUG'}, 0, "computer was rebooted after disabling pagefile in the registry");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to reboot computer after disabling pagefile");
return;
}
# Attempt to delete the pagefile from all drives
# A pagefile may reside on drives other than C: if additional volumes are configured in the image
my @volume_list = $self->get_volume_list();
if (!@volume_list || !(grep(/c/, @volume_list))) {
@volume_list = ('c');
}
# Loop through the drive letters and attempt to delete pagefile.sys on each drive
for my $drive_letter (@volume_list) {
if ($self->delete_file("$drive_letter:/pagefile.sys")) {
notify($ERRORS{'DEBUG'}, 0, "deleted pagefile.sys on all $drive_letter:");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to delete pagefile.sys on all $drive_letter:");
return;
}
}
return 1;
} ## end sub disable_pagefile
#//////////////////////////////////////////////////////////////////////////////
=head2 enable_pagefile
Parameters :
Returns :
Description :
=cut
sub enable_pagefile {
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 $memory_management_key = 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management';
my $reg_add_command = $system32_path . '/reg.exe add "' . $memory_management_key . '" /v PagingFiles /d "$SYSTEMDRIVE\\pagefile.sys 0 0" /t REG_MULTI_SZ /f';
my ($reg_add_exit_status, $reg_add_output) = $self->execute($reg_add_command, 1);
if (defined($reg_add_exit_status) && $reg_add_exit_status == 0) {
notify($ERRORS{'OK'}, 0, "set registry key to enable pagefile");
}
elsif (defined($reg_add_exit_status)) {
notify($ERRORS{'WARNING'}, 0, "failed to set registry key to enable pagefile, exit status: $reg_add_exit_status, output:\n@{$reg_add_output}");
return 0;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to set registry key to enable pagefile");
return;
}
return 1;
} ## end sub enable_pagefile
#//////////////////////////////////////////////////////////////////////////////
=head2 enable_ipv6
Parameters :
Returns :
Description :
=cut
sub enable_ipv6 {
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 $registry_string .= <<"EOF";
Windows Registry Editor Version 5.00
; This registry file contains the entries to disable all IPv6 components
; http://support.microsoft.com/kb/929852
[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters]
"DisabledComponents"=dword:00000000
EOF
# Import the string into the registry
if ($self->import_registry_string($registry_string)) {
notify($ERRORS{'OK'}, 0, "set registry keys to enable IPv6");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to set the registry keys to enable IPv6");
return 0;
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 disable_ipv6
Parameters :
Returns :
Description :
=cut
sub disable_ipv6 {
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 $registry_string .= <<"EOF";
Windows Registry Editor Version 5.00
; This registry file contains the entries to disable all IPv6 components
; http://support.microsoft.com/kb/929852
[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters]
"DisabledComponents"=dword:ffffffff
EOF
# Import the string into the registry
if ($self->import_registry_string($registry_string)) {
notify($ERRORS{'OK'}, 0, "set registry keys to disable IPv6");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to set the registry keys to disable IPv6");
return 0;
}
return 1;
} ## end sub disable_ipv6
#//////////////////////////////////////////////////////////////////////////////
=head2 import_registry_string
Parameters :
Returns :
Description :
=cut
sub import_registry_string {
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 $registry_string = shift;
if (!defined($registry_string) || !$registry_string) {
notify($ERRORS{'WARNING'}, 0, "registry file path was not passed correctly as an argument");
return;
}
#notify($ERRORS{'DEBUG'}, 0, "registry string:\n" . $registry_string);
# Escape special characters with a backslash:
# \
# "
#notify($ERRORS{'DEBUG'}, 0, "registry string:\n$registry_string");
#$registry_string =~ s/\\+/\\\\\\\\/gs;
$registry_string =~ s/\\/\\\\/gs;
$registry_string =~ s/"/\\"/gs;
# Replace \\" with \"
#$registry_string =~ s/\\+(")/\\\\$1/gs;
# Replace regular newlines with Windows newlines
$registry_string =~ s/\r?\n/\r\n/gs;
# Remove spaces from end of file
$registry_string =~ s/\s+$//;
# Assemble a temporary registry file path
# Name the file after the sub which called this so you can tell where the .reg file was generated from
my @caller = caller(1);
my ($calling_sub) = $caller[3] =~ /([^:]+)$/;
my $calling_line = $caller[2];
my $temp_registry_file_path = "C:/Cygwin/tmp/$calling_sub\_$calling_line.reg";
# Echo the registry string to a file on the node
my $echo_registry_command = "rm -f $temp_registry_file_path; /usr/bin/echo.exe -E \"$registry_string\" > " . $temp_registry_file_path;
my ($echo_registry_exit_status, $echo_registry_output) = $self->execute($echo_registry_command, 0);
if (defined($echo_registry_exit_status) && $echo_registry_exit_status == 0) {
notify($ERRORS{'DEBUG'}, 0, "registry string contents echoed to $temp_registry_file_path");
}
elsif ($echo_registry_exit_status) {
notify($ERRORS{'WARNING'}, 0, "failed to echo registry string contents to $temp_registry_file_path, exit status: $echo_registry_exit_status, output:\n@{$echo_registry_output}");
return;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to echo registry string contents to $temp_registry_file_path");
return;
}
# Run reg.exe IMPORT
if (!$self->reg_import($temp_registry_file_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to import registry string contents from $temp_registry_file_path");
return;
}
# Delete the temporary .reg file
if (!$self->delete_file($temp_registry_file_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to delete the temporary registry file: $temp_registry_file_path");
}
return 1;
} ## end sub import_registry_string
#//////////////////////////////////////////////////////////////////////////////
=head2 reg_query
Parameters : $registry_key, $registry_value (optional), $suppress_key_missing_error (optional)
Returns : If $registry_value argument is specified: scalar
If $registry_value argument is specified: hash reference
Description : Queries the registry on the Windows computer. The $registry_key
argument is required. The $registry_value argument is optional.
If $registry_value is specified, a scalar containing the value's
data is returned. The '(Default)' value's data is returned if the
$registry_value is either an empty string or exactly matches the
string '(Default)'.
If $registry_value is NOT specified, a hash reference containing
the keys's subkey names, values, and each value's data is
returned. The hash has 2 keys: 'subkeys', 'values'.
The 'subkeys' key contains an array reference. This array contains
the names of the key arguments subkeys.
The 'values' key contain a hash reference. The keys of this hash
are the names of the values that are set for the key argument.
Each of theses contains a 'type' and 'data' key containing the
registry value type and value data.
Example:
my $registry_data = $self->os->reg_query('HKLM/SYSTEM/CurrentControlSet/Services/NetBT/Parameters');
@{$registry_data->{subkeys}}[0] = 'Interfaces'
my @value_names = @{$registry_data->{values}};
$registry_data->{values}{$value_names[0]}{type} = 'REG_DWORD'
$registry_data->{values}{$value_names[0]}{data} = '123'
=cut
sub reg_query {
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;
# Get the arguments
my $key_argument = shift;
if (!defined($key_argument) || !$key_argument) {
notify($ERRORS{'WARNING'}, 0, "registry key was not passed correctly as an argument");
return;
}
my $value_argument = shift;
my $suppress_key_missing_error = shift;
# Replace forward slashes and double backslashes with a single backslashes
$key_argument =~ s/[\\\/]+/\\/g;
# Removing trailing slashes
$key_argument =~ s/\\+$//g;
# Replace abbreviated key names so argument matches reg.exe output
$key_argument =~ s/^HKLM/HKEY_LOCAL_MACHINE/;
$key_argument =~ s/^HKCU/HKEY_CURRENT_USER/;
$key_argument =~ s/^HKCR/HKEY_CLASSES_ROOT/;
$key_argument =~ s/^HKU/HKEY_USERS/;
$key_argument =~ s/^HKCC/HKEY_CURRENT_CONFIG/;
# Assemble the reg.exe QUERY command
my $command .= "$system32_path/reg.exe QUERY \"$key_argument\" ";
if (!defined($value_argument)) {
# Do not add any switches
$command .= "/s";
}
elsif ($value_argument eq '(Default)') {
# Add /ve switch to query the default value
$command .= "/ve";
}
else {
# Escape slashes and double-quotes in the value argument
(my $value_argument_escaped = $value_argument) =~ s/([\\\"])/\\$1/g;
# Add /v switch to query a specific value
$command .= "/v \"$value_argument_escaped\"";
}
# Ignore error lines, it will throw off parsing
$command .= " 2>/dev/null";
# Run reg.exe QUERY
my ($exit_status, $output) = $self->execute($command, 0);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to query registry key: $key_argument");
return;
}
elsif (grep(/unable to find the specified registry/, @$output)) {
my $message = "registry key or value does not exist:\nkey: '$key_argument'\n";
$message .= "value: '$value_argument'\n" if defined($value_argument);
$message .= "command: '$command'\n";
$message .= "exit status: $exit_status\n";
$message .= "output:\n" . join("\n", @{$output});
notify($ERRORS{'WARNING'}, 0, $message) unless $suppress_key_missing_error;
return;
}
elsif (!grep(/REG.EXE VERSION|HKEY/, @$output)) {
my $message = "failed to query registry:\nkey: '$key_argument'\n";
$message .= "value: '$value_argument'\n" if defined($value_argument);
$message .= "command: '$command'\n";
$message .= "exit status: $exit_status\n";
$message .= "output:\n" . join("\n", @{$output});
notify($ERRORS{'WARNING'}, 0, $message);
return;
}
# If value argument was specified, parse and return the data
if (defined($value_argument)) {
# Find the line containing the value information and parse it
my ($value, $type, $data) = map { $_ =~ /^\s*(.*)\s+(REG_\w+)\s+(.*)/ } @$output;
$value =~ s/(^\s+|\s+$)//g;
$type =~ s/(^\s+|\s+$)//g;
$data =~ s/(^\s+|\s+$)//g;
$value = '(Default)' if $value =~ /NO NAME/;
if ($type && defined($data)) {
$data = $self->reg_query_convert_data($type, $data);
notify($ERRORS{'DEBUG'}, 0, "retrieved registry data:\nkey: '$key_argument'\nvalue: '$value'\ntype: $type\ndata: '$data'");
return $data;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to retrieve registry data:\nkey: '$key_argument'\nvalue: '$value'\ncommand: '$command'\noutput:\n" . string_to_ascii(join("\n", @$output)));
return;
}
}
else {
# Value argument was not specified, construct a hash containing the contents of the key
my %registry_hash;
my $key;
for my $line (@$output) {
if ($line =~ /^HKEY/) {
$key = $line;
$registry_hash{$key} = {};
next;
}
elsif ($line =~ /^\s*(.*)\s+(REG_\w+)\s+(.*)/) {
my ($value, $type, $data) = ($1, $2, $3);
$value =~ s/(^\s+|\s+$)//g;
$type =~ s/(^\s+|\s+$)//g;
$data =~ s/(^\s+|\s+$)//g;
if ($type =~ /binary/i) {
#notify($ERRORS{'DEBUG'}, 0, "ignoring $type data, key: $key, value: $value");
next;
}
$value = '(Default)' if $value =~ /NO NAME/;
$data = $self->reg_query_convert_data($type, $data);
if (!defined($key) || !defined($value) || !defined($data) || !defined($type)) {
my $message = "some registry data is undefined:\n";
$message .= "line: '$line'\n";
$message .= "key: '" . ($key || 'undefined') . "'\n";
$message .= "value: '" . ($value || 'undefined') . "'\n";
$message .= "data: '" . ($data || 'undefined') . "'\n";
$message .= "type: '" . ($type || 'undefined') . "'";
notify($ERRORS{'WARNING'}, 0, $message);
}
else {
$registry_hash{$key}{$value}{type} = $type;
$registry_hash{$key}{$value} = $data;
}
}
elsif ($line =~ /^!/) {
# Ignore lines beginning with '!'
next;
}
elsif ($line =~ /^Error:/) {
# Ignore lines beginning with 'Error:' -- this is common and probably not a problem
# Example:
# Error: Access is denied in the key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\MRxDAV\EncryptedDirectories
next;
}
else {
# TODO: add support for registry values that span multiple lines. Example:
# Comments REG_SZ This security update is for Microsoft .NET Framework 3.5 SP1.
# If you later install a more recent service pack, this security update will be uninstalled automatically.
# For more information, visit http://support.microsoft.com/kb/2416473.
#notify($ERRORS{'WARNING'}, 0, "unexpected output in line: '" . string_to_ascii($line) . "'\ncommand: '$command'");
}
}
my $message = "retrieved registry data:\n";
$message .= "key: '$key_argument'\n";
$message .= "value: '$value_argument'\n" if defined($value_argument);
$message .= "keys found: " . scalar(keys %registry_hash);
notify($ERRORS{'DEBUG'}, 0, $message);
return \%registry_hash;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 reg_query_convert_data
Parameters : $type, $data
Returns : scalar
Description :
=cut
sub reg_query_convert_data {
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 ($type, $data) = @_;
if (!$type || !defined($data)) {
notify($ERRORS{'WARNING'}, 0, "registry data type and data value arguments were not specified");
return;
}
if ($type =~ /dword/i) {
if ($data =~ /^[a-fA-F0-9]+$/) {
$data = "0x$data";
}
# Make sure a valid hex value was returned
if ($data !~ /^0x[a-fA-F0-9]+$/) {
notify($ERRORS{'WARNING'}, 0, "invalid $type value: '$data'");
return;
}
# Convert the hex value to decimal
$data = hex($data);
}
elsif ($type eq 'REG_MULTI_SZ') {
# Split data into an array, data values are separated in the output by '\0'
my @data_values = split(/\\0/, $data);
$data = \@data_values;
}
elsif ($type =~ /hex/) {
# Split data into an array, data values are separated in the output by ',00'
my @hex_values = split(/,00,?/, $data);
my $string;
for my $hex_value (@hex_values) {
my $decimal_value = hex $hex_value;
$string .= pack("C*", $decimal_value);
}
return $string;
}
return $data;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 reg_add
Parameters : $registry_key, $registry_value, $registry_type, $registry_data
Returns : If successful: true
If failed: false
Description : Adds or sets a registry key.
=cut
sub reg_add {
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;
# Get the arguments
my ($registry_key, $registry_value, $registry_type, $registry_data) = @_;
if (!defined($registry_key)) {
notify($ERRORS{'WARNING'}, 0, "registry key argument was not supplied");
return;
}
# Replace forward slashes with backslashes in registry key
$registry_key =~ s/\//\\\\/g;
if (defined($registry_value)) {
if (!defined($registry_type)) {
notify($ERRORS{'WARNING'}, 0, "registry value argument was supplied but type argument was not");
return;
}
my $valid_types = 'REG_SZ|REG_MULTI_SZ|REG_DWORD_BIG_ENDIAN|REG_DWORD|REG_BINARY|REG_DWORD_LITTLE_ENDIAN|REG_NONE|REG_EXPAND_SZ';
if ($registry_type !~ /^($valid_types)$/) {
notify($ERRORS{'WARNING'}, 0, "invalid registry type was specified: '$registry_type', the following types are supported:\n" . join("\n", sort split(/\|/, $valid_types)));
return;
}
if (!defined($registry_data)) {
notify($ERRORS{'WARNING'}, 0, "registry value argument was supplied but data argument was not");
return;
}
}
# Assemble the reg.exe ADD command
my $add_registry_command = "$system32_path/reg.exe ADD \"$registry_key\"";
if ($registry_value) {
if ($registry_value =~ /^default$/i) {
$add_registry_command .= " /ve";
}
else {
$add_registry_command .= " /v \"$registry_value\"";
}
$add_registry_command .= " /t $registry_type /d \"$registry_data\"";
}
$add_registry_command .= " /f";
my ($add_registry_exit_status, $add_registry_output) = $self->execute($add_registry_command, 1);
if (!defined($add_registry_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command to add registry key: $registry_key");
return;
}
elsif ($add_registry_exit_status == 0) {
notify($ERRORS{'DEBUG'}, 0, "added registry key, command: $add_registry_command, output:\n" . join("\n", @$add_registry_output));
return 1;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to add registry key: $registry_key, exit status: $add_registry_exit_status, command:\n$add_registry_command\noutput:\n" . join("\n", @$add_registry_output));
return 0;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 reg_delete
Parameters : registry key, registry value
Returns :
Description :
=cut
sub reg_delete {
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;
# Get the arguments
my $registry_key = shift;
if (!defined($registry_key) || !$registry_key) {
notify($ERRORS{'WARNING'}, 0, "registry key was not passed correctly as an argument");
return;
}
my $registry_value = shift;
# Replace forward slashes with backslashes in registry key
$registry_key =~ s/\//\\\\/g;
# Run reg.exe DELETE
my $delete_registry_command;
if ($registry_value) {
$delete_registry_command = $system32_path . "/reg.exe DELETE \"$registry_key\" /v \"$registry_value\" /f";
}
else {
$delete_registry_command = $system32_path . "/reg.exe DELETE \"$registry_key\" /f";
$registry_value = '*';
}
my ($delete_registry_exit_status, $delete_registry_output) = $self->execute($delete_registry_command, 0);
if (defined($delete_registry_exit_status) && $delete_registry_exit_status == 0) {
notify($ERRORS{'DEBUG'}, 0, "deleted registry key: $registry_key, value: $registry_value, output:\n" . join("\n", @$delete_registry_output));
}
elsif ($delete_registry_output && grep(/unable to find/i, @$delete_registry_output)) {
# Error: The system was unable to find the specified registry key or value
notify($ERRORS{'DEBUG'}, 0, "registry key does NOT exist: $registry_key");
}
elsif ($delete_registry_exit_status) {
notify($ERRORS{'WARNING'}, 0, "failed to delete registry key: $registry_key, value: $registry_value, exit status: $delete_registry_exit_status, output:\n@{$delete_registry_output}");
return;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to delete registry key: $registry_key, value: $registry_value");
return;
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 reg_import
Parameters :
Returns :
Description :
=cut
sub reg_import {
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;
# Get the registry file path argument
my $registry_file_path = shift;
if (!defined($registry_file_path) || !$registry_file_path) {
notify($ERRORS{'WARNING'}, 0, "registry file path was not passed correctly as an argument");
return;
}
# Run reg.exe IMPORT
my $command .= $system32_path . "/reg.exe IMPORT $registry_file_path";
my ($exit_status, $output) = $self->execute($command, 0);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to import registry file: $registry_file_path");
return;
}
elsif (grep(/completed successfully/i, @$output)) {
notify($ERRORS{'DEBUG'}, 0, "imported registry file: $registry_file_path");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to import registry file: $registry_file_path, exit status: $exit_status, output:\n" . join("\n", @$output));
return;
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 reg_export
Parameters :
Returns :
Description :
=cut
sub reg_export {
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;
# Get the arguments
my $root_key = shift;
if (!$root_key) {
notify($ERRORS{'WARNING'}, 0, "registry root key was not passed correctly as an argument");
return;
}
# Get the registry file path argument
my $registry_file_path = shift;
if (!defined($registry_file_path) || !$registry_file_path) {
notify($ERRORS{'WARNING'}, 0, "registry file path was not passed correctly as an argument");
return;
}
$registry_file_path = $self->format_path_dos($registry_file_path);
# Replace forward slashes with backslashes in registry key
$root_key =~ s/\//\\\\/g;
# Run reg.exe EXPORT
my $command .= "cmd.exe /c \"del /Q \\\"$registry_file_path.tmp\\\" 2>NUL & $system32_path/reg.exe EXPORT $root_key \\\"$registry_file_path.tmp\\\" && type \\\"$registry_file_path.tmp\\\" > \\\"$registry_file_path\\\" && del /Q \\\"$registry_file_path.tmp\\\"\"";
my ($exit_status, $output) = $self->execute($command, 1);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to export registry key $root_key to file: $registry_file_path");
return;
}
elsif (grep(/completed successfully/i, @$output)) {
notify($ERRORS{'DEBUG'}, 0, "exported registry key $root_key to file: $registry_file_path");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to export registry key $root_key to file: $registry_file_path, exit status: $exit_status, output:\n" . join("\n", @$output));
return;
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 reg_load
Parameters : $root_key, $hive_file_path
Returns :
Description :
=cut
sub reg_load {
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;
# Get the arguments
my $root_key = shift;
if (!$root_key) {
notify($ERRORS{'WARNING'}, 0, "registry root key was not passed correctly as an argument");
return;
}
my $hive_file_path = shift;
if (!$hive_file_path) {
notify($ERRORS{'WARNING'}, 0, "registry hive file path was not passed correctly as an argument");
return;
}
$hive_file_path = $self->format_path_unix($hive_file_path);
# Escape backslashes in the root key
$root_key =~ s/\\+/\\\\/;
# Run reg.exe LOAD
my $command .= "$system32_path/reg.exe LOAD $root_key $hive_file_path";
my ($exit_status, $output) = $self->execute($command, 0);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to load registry hive file '$hive_file_path' into key $root_key");
return;
}
elsif (grep(/completed successfully/i, @$output)) {
notify($ERRORS{'DEBUG'}, 0, "loaded registry hive file '$hive_file_path' into key $root_key");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to load registry hive file '$hive_file_path' into key $root_key, exit status: $exit_status, output:\n" . join("\n", @$output));
return;
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 reg_unload
Parameters : $root_key
Returns :
Description :
=cut
sub reg_unload {
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;
# Get the arguments
my $root_key = shift;
if (!$root_key) {
notify($ERRORS{'WARNING'}, 0, "registry root key was not passed correctly as an argument");
return;
}
# Escape backslashes in the root key
$root_key =~ s/\\+/\\\\/;
# Run reg.exe UNLOAD
my $command .= "$system32_path/reg.exe UNLOAD $root_key";
my ($exit_status, $output) = $self->execute($command, 0);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to unload registry hive: $root_key");
return;
}
elsif (grep(/completed successfully/i, @$output)) {
notify($ERRORS{'DEBUG'}, 0, "unloaded registry hive key: $root_key");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to unload registry hive: $root_key, exit status: $exit_status, output:\n" . join("\n", @$output));
return;
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 add_hklm_run_registry_key
Parameters :
Returns :
Description :
=cut
sub add_hklm_run_registry_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;
}
my $system32_path = $self->get_system32_path() || return;
my $command_name = shift;
my $command = shift;
notify($ERRORS{'DEBUG'}, 0, "command name: " . $command_name);
notify($ERRORS{'DEBUG'}, 0, "command: " . $command);
# Replace forward slashes with backslashes, unless a space precedes the forward slash
$command =~ s/([^ ])\//$1\\/g;
notify($ERRORS{'DEBUG'}, 0, "forward to backslash: " . $command);
# Escape backslashes, can never have enough...
$command =~ s/\\/\\\\/g;
notify($ERRORS{'DEBUG'}, 0, "escape backslashes: " . $command);
# Escape quotes
$command =~ s/"/\\"/g;
notify($ERRORS{'DEBUG'}, 0, "escaped quotes: " . $command);
# Make sure arguments were supplied
if (!defined($command_name) && !defined($command)) {
notify($ERRORS{'WARNING'}, 0, "HKLM run registry key not added, arguments were not passed correctly");
return 0;
}
my $registry_string .= <<"EOF";
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run]
"$command_name"="$command"
EOF
notify($ERRORS{'DEBUG'}, 0, "registry string:\n" . $registry_string);
# Import the string into the registry
if ($self->import_registry_string($registry_string)) {
notify($ERRORS{'OK'}, 0, "added HKLM run registry value, name: $command_name, command: $command");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to add HKLM run registry value, name: $command_name, command: $command");
return 0;
}
# Attempt to query the registry key to make sure it was added
my $reg_query_command = $system32_path . '/reg.exe query "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"';
my ($reg_query_exit_status, $reg_query_output) = $self->execute($reg_query_command, 1);
if (defined($reg_query_exit_status) && $reg_query_exit_status == 0) {
notify($ERRORS{'DEBUG'}, 0, "queried '$command_name' registry key:\n" . join("\n", @{$reg_query_output}));
}
elsif (defined($reg_query_exit_status)) {
notify($ERRORS{'WARNING'}, 0, "failed to query '$command_name' registry key, exit status: $reg_query_exit_status, output:\n@{$reg_query_output}");
return 0;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to query '$command_name' registry key");
return;
}
return 1;
} ## end sub add_hklm_run_registry_key
#//////////////////////////////////////////////////////////////////////////////
=head2 delete_hklm_run_registry_key
Parameters :
Returns :
Description :
=cut
sub delete_hklm_run_registry_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;
}
my $system32_path = $self->get_system32_path() || return;
my $key_name = shift;
# Make sure argument was supplied
if (!defined($key_name) && !defined($key_name)) {
notify($ERRORS{'WARNING'}, 0, "HKLM run registry key not deleted, argument was not passed correctly");
return 0;
}
# Attempt to query the registry key to make sure it was added
my $reg_delete_command = $system32_path . '/reg.exe delete "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run" /v "' . $key_name . '" /F';
my ($reg_delete_exit_status, $reg_delete_output) = $self->execute($reg_delete_command, 1);
if (defined($reg_delete_exit_status) && $reg_delete_exit_status == 0) {
notify($ERRORS{'OK'}, 0, "deleted '$key_name' run registry key:\n" . join("\n", @{$reg_delete_output}));
}
elsif (defined($reg_delete_output) && grep(/unable to find/i, @{$reg_delete_output})) {
notify($ERRORS{'OK'}, 0, "'$key_name' run registry key was not deleted, it does not exist");
}
elsif (defined($reg_delete_exit_status)) {
notify($ERRORS{'WARNING'}, 0, "failed to delete '$key_name' run registry key, exit status: $reg_delete_exit_status, output:\n@{$reg_delete_output}");
return 0;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to delete '$key_name' run registry key");
return;
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 set_scheduled_task_credentials
Parameters : $task_name, $username, $password
Returns : boolean
Description : Sets the credentials under which a scheduled task runs.
=cut
sub set_scheduled_task_credentials {
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 ($task_name, $username, $password) = @_;
if (!defined($task_name) || !defined($username) || !defined($password)) {
notify($ERRORS{'WARNING'}, 0, "scheduled task name, username, and password arguments were not supplied");
return;
}
my $system32_path = $self->get_system32_path() || return;
my $password_escaped = _escape_password($password);
my $command = "$system32_path/schtasks.exe /Change /RU \"$username\" /RP \"$password_escaped\" /TN \"$task_name\"";
my ($exit_status, $output) = $self->execute($command);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command to change password for scheduled task: $task_name");
return;
}
elsif (grep (/^SUCCESS:/, @$output)) {
notify($ERRORS{'OK'}, 0, "changed password for scheduled task: $task_name");
return 1;
}
elsif (grep (/The parameter is incorrect/, @$output)) {
if ($task_name =~ /{/) {
# Ignore task such as: \User_Feed_Synchronization-{88DE35B9-C115-4DE3-AB5E-B9D2C4A2DB66}
# This one always fails and is not important
notify($ERRORS{'DEBUG'}, 0, "unable to change password for scheduled task '$task_name' due to Windows bug\ncommand: '$command'\noutput:\n" . join("\n", @$output));
}
else {
notify($ERRORS{'WARNING'}, 0, "unable to change password for scheduled task '$task_name' due to Windows bug\ncommand: '$command'\noutput:\n" . join("\n", @$output));
# Don't return false - There is a bug in Windows 7
# If a scheduled task is created using the GUI using a schedule the password cannot be set via schtasks.exe
# schtasks.exe displays: ERROR: The parameter is incorrect.
# If the same task is changed to run on an event such as logon it works
}
return 1;
}
elsif (grep (/^ERROR:/, @$output)) {
notify($ERRORS{'WARNING'}, 0, "failed to change password for scheduled task: $task_name, command:\n$command\noutput:\n" . join("\n", @$output));
return 0;
}
else {
notify($ERRORS{'WARNING'}, 0, "unexpected output returned while attempting to change password for scheduled task: $task_name, command:\n$command\noutput:\n" . join("\n", @$output));
return 0;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 delete_scheduled_task
Parameters :
Returns :
Description :
=cut
sub delete_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;
}
my $computer_node_name = $self->data->get_computer_node_name();
my $system32_path = $self->get_system32_path() || return;
my $task_name = shift;
# Run schtasks.exe to delete any existing task
my $delete_task_command = "$system32_path/schtasks.exe /Delete /F /TN \"$task_name\"";
my ($delete_task_exit_status, $delete_task_output) = $self->execute($delete_task_command);
if (defined($delete_task_exit_status) && $delete_task_exit_status == 0) {
notify($ERRORS{'OK'}, 0, "deleted existing scheduled task '$task_name' on $computer_node_name");
}
elsif (defined($delete_task_output) && grep(/(task.*does not exist|cannot find the file specified)/i, @{$delete_task_output})) {
notify($ERRORS{'DEBUG'}, 0, "scheduled task '$task_name' does not exist on $computer_node_name");
}
elsif (defined($delete_task_exit_status)) {
notify($ERRORS{'WARNING'}, 0, "failed to delete existing scheduled task '$task_name' on $computer_node_name, exit status: $delete_task_exit_status, output:\n@{$delete_task_output}");
return 0;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to execute ssh command to delete existing scheduled task '$task_name' on $computer_node_name");
return;
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 create_startup_scheduled_task
Parameters : $task_name, $task_command, $task_user, $task_password
Returns :
Description :
=cut
sub create_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;
}
my $computer_node_name = $self->data->get_computer_node_name();
my $system32_path = $self->get_system32_path() || return;
my $task_name = shift;
my $task_command = shift;
my $task_user = shift;
my $task_password = shift;
# Escape backslashes, can never have enough...
$task_command =~ s/\\/\\\\/g;
# Replace forward slashes with backslashes
$task_command =~ s/([^\s])\//$1\\\\/g;
# Escape quote characters
$task_command =~ s/"/\\"/g;
# Make sure arguments were supplied
if (!defined($task_name) || !defined($task_command) || !defined($task_user) || !defined($task_password)) {
notify($ERRORS{'WARNING'}, 0, "startup scheduled task not added, arguments were not passed correctly");
return;
}
# You cannot create a task if one with the same name already exists
# Windows 6.x schtasks.exe has a /F which forces a new task to be created if one with the same name already exists
# This option isn't supported with XP and other older versions of Windows
if (!$self->delete_scheduled_task($task_name)) {
notify($ERRORS{'WARNING'}, 0, "unable to delete existing scheduled task '$task_name' on $computer_node_name");
}
# Run schtasks.exe to add the task
# Occasionally see this error even though it schtasks.exe returns exit status 0:
# WARNING: The Scheduled task "System Startup Script" has been created, but may not run because the account information could not be set.
my $password_escaped = _escape_password($task_password);
my $command = "$system32_path/schtasks.exe /Create /RU \"$task_user\" /RP \"$password_escaped\" /RL HIGHEST /SC ONSTART /TN \"$task_name\" /TR \"$task_command\"";
my ($exit_status, $output) = $self->execute($command);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute ssh command created scheduled task '$task_name' on $computer_node_name");
return;
}
elsif (grep(/password is incorrect/, @$output)) {
# ERROR: The user name or password is incorrect.
notify($ERRORS{'WARNING'}, 0, "failed to create scheduled task '$task_name' on $computer_node_name\n" .
"username : '$task_user'\n" .
"password : '$task_password'\n" .
"escaped password : '$password_escaped'\n" .
"command:\n$command\n" .
"output:\n" . join("\n", @$output)
);
return 0;
}
elsif ($exit_status != 0) {
notify($ERRORS{'WARNING'}, 0, "failed to create scheduled task '$task_name' on $computer_node_name, exit status: $exit_status, command: '$command', output:\n@$output");
return 0;
}
else {
notify($ERRORS{'OK'}, 0, "created scheduled task '$task_name' on $computer_node_name");
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=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 $request_state = $self->data->get_request_state_name();
my $node_configuration_directory = $self->get_node_configuration_directory();
my $root_password;
if ($request_state =~ /(image|checkpoint)/) {
$root_password = $WINDOWS_ROOT_PASSWORD;
}
else {
if ($self->{root_password}) {
$root_password = $self->{root_password};
}
else {
$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;
}
# Make sure the 'VCL Post Load' task doesn't exist or they will conflict
$self->delete_scheduled_task('VCL Post Load');
# Copy the current version of update_cygwin.cmd to the computer
$self->copy_file_to("$SOURCE_CONFIGURATION_DIRECTORY/Scripts/update_cygwin.cmd", "$node_configuration_directory/Scripts/update_cygwin.cmd");
# 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_password)) {
$self->{created_update_cygwin_startup_scheduled_task} = 1;
return 1;
}
else {
return 0;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 enable_autoadminlogon
Parameters :
Returns :
Description :
=cut
sub enable_autoadminlogon {
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 $registry_string .= <<"EOF";
Windows Registry Editor Version 5.00
; This file enables autoadminlogon for the root account
[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon]
"AutoAdminLogon"="1"
"DefaultUserName"="root"
"DefaultPassword"= "$WINDOWS_ROOT_PASSWORD"
EOF
# Import the string into the registry
if ($self->import_registry_string($registry_string)) {
notify($ERRORS{'OK'}, 0, "enabled autoadminlogon");
return 1;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to enable autoadminlogon");
return 0;
}
} ## end sub enable_autoadminlogon
#//////////////////////////////////////////////////////////////////////////////
=head2 disable_autoadminlogon
Parameters :
Returns :
Description :
=cut
sub disable_autoadminlogon {
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 $registry_string .= <<EOF;
Windows Registry Editor Version 5.00
; This file disables autoadminlogon for the root account
[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon]
"AutoAdminLogon"="0"
"AutoLogonCount"="0"
"DefaultPassword"= ""
EOF
# Import the string into the registry
if ($self->import_registry_string($registry_string)) {
notify($ERRORS{'OK'}, 0, "disabled autoadminlogon");
return 1;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to disable autoadminlogon");
return 0;
}
} ## end sub disable_autoadminlogon
#//////////////////////////////////////////////////////////////////////////////
=head2 create_eventlog_entry
Parameters :
Returns :
Description :
=cut
sub create_eventlog_entry {
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_node_name = $self->data->get_computer_node_name();
my $system32_path = $self->get_system32_path() || return;
my $message = shift;
# Make sure the message was passed as an argument
if (!defined($message)) {
notify($ERRORS{'WARNING'}, 0, "failed to create eventlog entry, message was passed as an argument");
return 0;
}
# Run eventcreate.exe to create an event log entry
my $eventcreate_command = $system32_path . '/eventcreate.exe /T INFORMATION /L APPLICATION /SO VCL /ID 555 /D "' . $message . '"';
my ($eventcreate_exit_status, $eventcreate_output) = $self->execute($eventcreate_command);
if (defined($eventcreate_exit_status) && $eventcreate_exit_status == 0) {
notify($ERRORS{'OK'}, 0, "created event log entry on $computer_node_name: $message");
}
elsif (defined($eventcreate_exit_status)) {
notify($ERRORS{'WARNING'}, 0, "failed to create event log entry on $computer_node_name: $message, exit status: $eventcreate_exit_status, output:\n@{$eventcreate_output}");
return 0;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to create event log entry on $computer_node_name: $message");
return;
}
return 1;
} ## end sub create_eventlog_entry
#//////////////////////////////////////////////////////////////////////////////
=head2 reboot
Parameters : $total_wait_seconds, $attempt_delay_seconds, $attempt_limit, $pre_configure
Returns : boolean
Description :
=cut
sub reboot {
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;
}
# Check if an arguments were supplied
# Attempt to get the total number of seconds to wait from the arguments
my $total_wait_seconds = shift;
if (!defined($total_wait_seconds) || $total_wait_seconds !~ /^\d+$/) {
$total_wait_seconds = 300;
}
# Seconds to wait in between loop attempts
my $attempt_delay_seconds = shift;
if (!defined($attempt_delay_seconds) || $attempt_delay_seconds !~ /^\d+$/) {
$attempt_delay_seconds = 15;
}
# Number of power reset attempts to make if reboot fails
my $attempt_limit = shift;
if (!defined($attempt_limit) || $attempt_limit !~ /^\d+$/) {
$attempt_limit = 2;
}
my $pre_configure = shift;
$pre_configure = 1 unless defined $pre_configure;
my $computer_node_name = $self->data->get_computer_node_name();
my $system32_path = $self->get_system32_path();
my $reboot_start_time = time();
notify($ERRORS{'DEBUG'}, 0, "reboot will be attempted on $computer_node_name");
# Check if computer responds to ssh before preparing for reboot
if ($system32_path && $self->wait_for_ssh(0)) {
# Perform pre-reboot configuration tasks unless $pre_configure argument was supplied and is false
if ($pre_configure) {
# Make sure SSH access is enabled from private IP addresses
if (!$self->firewall_enable_ssh_private()) {
notify($ERRORS{'WARNING'}, 0, "reboot not attempted, failed to enable ssh from private IP addresses");
return 0;
}
# Set sshd service startup mode to manual
if (!$self->set_service_startup_mode('sshd', 'manual') && !$self->set_service_startup_mode('cygsshd', 'manual')) {
notify($ERRORS{'WARNING'}, 0, "reboot not attempted, unable to set sshd service startup mode to manual");
return 0;
}
else {
notify($ERRORS{'DEBUG'}, 0, "sshd service set to manual start");
}
# 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");
return 0;
}
# 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
delete $self->{network_configuration};
# Check if tsshutdn.exe exists on the computer
# tsshutdn.exe is the preferred utility, shutdown.exe often fails on Windows Server 2003
my $reboot_command;
my $windows_product_name = $self->get_product_name() || '';
if ($windows_product_name =~ /2003/ && $self->file_exists("$system32_path/tsshutdn.exe")) {
$reboot_command = "$system32_path/tsshutdn.exe 0 /REBOOT /DELAY:0 /V";
}
else {
$reboot_command = "$system32_path/shutdown.exe /r /t 0 /f";
}
my ($reboot_exit_status, $reboot_output) = $self->execute($reboot_command);
if (!defined($reboot_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command to reboot $computer_node_name");
return;
}
elsif ($reboot_exit_status == 0) {
notify($ERRORS{'OK'}, 0, "executed reboot command on $computer_node_name");
}
else {
# The following message may be displayed causing the reboot to fail:
# The computer is processing another action and thus cannot be shut down. Wait until the computer has finished its action, and then try again.(21)
notify($ERRORS{'WARNING'}, 0, "failed to reboot $computer_node_name, attempting power reset, output:\n" . join("\n", @$reboot_output));
# Call provisioning module's power_reset() subroutine
if ($self->provisioner->power_reset()) {
notify($ERRORS{'OK'}, 0, "initiated power reset on $computer_node_name");
}
else {
notify($ERRORS{'WARNING'}, 0, "reboot failed, failed to initiate power reset on $computer_node_name");
return;
}
}
}
else {
# Computer did not respond to ssh
notify($ERRORS{'WARNING'}, 0, "$computer_node_name did not respond to ssh, graceful reboot cannot be performed, attempting hard reset");
# Call provisioning module's power_reset() subroutine
if ($self->provisioner->power_reset()) {
notify($ERRORS{'OK'}, 0, "initiated power reset on $computer_node_name");
}
else {
notify($ERRORS{'WARNING'}, 0, "reboot failed, failed to initiate power reset on $computer_node_name");
return 0;
}
} ## end else [ if ($self->wait_for_ssh(0))
# Wait for the reboot to complete
my $result = $self->wait_for_reboot($total_wait_seconds, $attempt_delay_seconds, $attempt_limit);
my $reboot_duration = (time - $reboot_start_time);
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 {
notify($ERRORS{'WARNING'}, 0, "reboot failed on $computer_node_name, waited $reboot_duration seconds for computer to respond");
return 0;
}
} ## end sub reboot
#//////////////////////////////////////////////////////////////////////////////
=head2 shutdown
Parameters : $enable_dhcp
Returns :
Description :
=cut
sub shutdown {
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 argument that determines whether or not to disable DHCP before shutting down computer
my $enable_dhcp = shift;
my $computer_node_name = $self->data->get_computer_node_name();
my $system32_path = $self->get_system32_path() || return;
# Kill the screen saver process, it occasionally prevents reboots and shutdowns from working
$self->kill_process('logon.scr');
# Clear the event log before shutting down
$self->clear_event_log();
my $shutdown_command = "/bin/cygstart.exe \$SYSTEMROOT/system32/cmd.exe /c \"";
if ($enable_dhcp) {
notify($ERRORS{'DEBUG'}, 0, "enabling DHCP and shutting down $computer_node_name");
my $private_interface_name = $self->get_private_interface_name();
my $public_interface_name = $self->get_public_interface_name();
if (!$private_interface_name || !$public_interface_name) {
notify($ERRORS{'WARNING'}, 0, "unable to determine private and public interface names, failed to enable DHCP and shut down $computer_node_name");
return;
}
$shutdown_command .= "$system32_path/netsh.exe interface ip set address name=\\\"$private_interface_name\\\" source=dhcp & ";
$shutdown_command .= "$system32_path/netsh.exe interface ip set dnsservers name=\\\"$private_interface_name\\\" source=dhcp & ";
$shutdown_command .= "$system32_path/netsh.exe interface ip set address name=\\\"$public_interface_name\\\" source=dhcp & ";
$shutdown_command .= "$system32_path/netsh.exe interface ip set dnsservers name=\\\"$public_interface_name\\\" source=dhcp & ";
$shutdown_command .= "$system32_path/netsh.exe interface ip reset $NODE_CONFIGURATION_DIRECTORY/Logs/ipreset.log & ";
$shutdown_command .= "$system32_path/ipconfig.exe /release & ";
$shutdown_command .= "$system32_path/ipconfig.exe /flushdns & ";
$shutdown_command .= "$system32_path/arp.exe -d * & ";
$shutdown_command .= "$system32_path/route.exe DELETE 0.0.0.0 MASK 0.0.0.0 & ";
}
else {
notify($ERRORS{'DEBUG'}, 0, "shutting down $computer_node_name");
}
# Check if tsshutdn.exe exists on the computer
# tsshutdn.exe is the preferred utility for Windows 2003, shutdown.exe often fails
my $windows_product_name = $self->get_product_name() || '';
if ($windows_product_name =~ /2003/ && $self->file_exists("$system32_path/tsshutdn.exe")) {
$shutdown_command .= "$system32_path/tsshutdn.exe 0 /POWERDOWN /DELAY:0 /V";
}
else {
$shutdown_command .= "$system32_path/shutdown.exe /s /t 0 /f";
}
$shutdown_command .= "\"";
my $attempt_count = 0;
my $attempt_limit = 12;
while ($attempt_count < $attempt_limit) {
$attempt_count++;
if ($attempt_count > 1) {
notify($ERRORS{'DEBUG'}, 0, "sleeping for 10 seconds before making next shutdown attempt");
sleep 10;
}
my ($shutdown_exit_status, $shutdown_output) = $self->execute($shutdown_command);
if (!defined($shutdown_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute ssh command to shutdown $computer_node_name");
last;
}
elsif (grep(/(processing another action)/i, @$shutdown_output)) {
notify($ERRORS{'WARNING'}, 0, "attempt $attempt_count/$attempt_limit: failed to execute shutdown command on $computer_node_name, exit status: $shutdown_exit_status, output:\n@{$shutdown_output}");
next;
}
else {
notify($ERRORS{'DEBUG'}, 0, "attempt $attempt_count/$attempt_limit: executed shutdown command on $computer_node_name");
last;
}
}
# Wait maximum of 3 minutes for the computer to become unresponsive
if (!$self->wait_for_no_ping(180)) {
# Computer never stopped responding to ping
notify($ERRORS{'WARNING'}, 0, "$computer_node_name never became unresponsive to ping after shutdown command was issued");
return;
}
# Wait maximum of 5 minutes for computer to power off
my $power_off = $self->provisioner->wait_for_power_off(300);
if (!defined($power_off)) {
# wait_for_power_off result will be undefined if the provisioning module doesn't implement a power_status subroutine
notify($ERRORS{'OK'}, 0, "unable to determine power status of $computer_node_name from provisioning module, sleeping 1 minute to allow computer time to shutdown");
sleep 60;
}
elsif (!$power_off) {
notify($ERRORS{'WARNING'}, 0, "$computer_node_name never powered off");
return;
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 set_service_startup_mode
Parameters :
Returns : 1 if succeeded, 0 otherwise
Description :
=cut
sub set_service_startup_mode {
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 $service_name = shift;
my $startup_mode = shift;
# Make sure both arguments were supplied
if (!defined($service_name) && !defined($startup_mode)) {
notify($ERRORS{'WARNING'}, 0, "set service startup mode failed, service name and startup mode arguments were not passed correctly");
return 0;
}
# Make sure the startup mode is valid
if ($startup_mode !~ /boot|system|auto|demand|disabled|delayed-auto|manual/i) {
notify($ERRORS{'WARNING'}, 0, "set service startup mode failed, invalid startup mode: $startup_mode");
return 0;
}
# Set the mode to demand if manual was specified, specific to sc command
$startup_mode = "demand" if ($startup_mode eq "manual");
# Use sc.exe to change the start mode
my $service_startup_command = $system32_path . '/sc.exe config ' . "$service_name start= $startup_mode";
my ($service_startup_exit_status, $service_startup_output) = $self->execute($service_startup_command);
if (defined($service_startup_output) && grep(/service does not exist/, @$service_startup_output)) {
notify($ERRORS{'WARNING'}, 0, "$service_name service startup mode not set because service does not exist");
return;
}
elsif (defined($service_startup_exit_status) && $service_startup_exit_status == 0) {
notify($ERRORS{'OK'}, 0, "$service_name service startup mode set to $startup_mode");
}
elsif ($service_startup_exit_status) {
notify($ERRORS{'WARNING'}, 0, "failed to set $service_name service startup mode to $startup_mode, exit status: $service_startup_exit_status, output:\n@{$service_startup_output}");
return;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to set $service_name service startup mode to $startup_mode");
return;
}
return 1;
} ## end sub set_service_startup_mode
#//////////////////////////////////////////////////////////////////////////////
=head2 defragment_hard_drive
Parameters :
Returns : 1 if succeeded, 0 otherwise
Description :
=cut
sub defragment_hard_drive {
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_node_name = $self->data->get_computer_node_name();
my $system32_path = $self->get_system32_path() || return;
# Defragment the hard drive
notify($ERRORS{'OK'}, 0, "beginning to defragment the hard drive on $computer_node_name");
my ($defrag_exit_