Author: arkurth
Date: Wed Aug 12 15:13:30 2015
New Revision: 1695554
URL: http://svn.apache.org/r1695554
Log:
VCL-871
Updated OS.pm::create_text_file to ensure that text is added on a new line at
the end of the file if called with the $append flag. This subroutine should
correctly add newlines when necessary. Callers of append_text_file (which calls
create_text_file) should not have to ensure a trailing newline exists in the
string passed.
Removed trailing newline from the AllowUsers line appended to
external_sshd_config in Linux.pm::grant_connect_method_access. It should no
longer be needed.
Updated OS.pm::set_vcld_post_load_status to use create_text_file instead of
echo.
VCL-896
Reworked and cleaned up Linux.pm::update_public_hostname. Added call to
hostnamectl. Removed Linux.pm::update_hostname_file which was using sed and
echo instead of create_text_file.
VCL-897
Changed argument accepted by Linux.pm::grant_root_access to a simple string.
Changed Linux.pm::create_user to only call grant_root_access if necessary.
Other
Added Linux.pm::install_package. This isn't currently being called but may be
used in the future.
Modified:
vcl/trunk/managementnode/lib/VCL/Module/OS.pm
vcl/trunk/managementnode/lib/VCL/Module/OS/Linux.pm
Modified: vcl/trunk/managementnode/lib/VCL/Module/OS.pm
URL:
http://svn.apache.org/viewvc/vcl/trunk/managementnode/lib/VCL/Module/OS.pm?rev=1695554&r1=1695553&r2=1695554&view=diff
==============================================================================
--- vcl/trunk/managementnode/lib/VCL/Module/OS.pm (original)
+++ vcl/trunk/managementnode/lib/VCL/Module/OS.pm Wed Aug 12 15:13:30 2015
@@ -1363,37 +1363,22 @@ sub set_vcld_post_load_status {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a
function, it must be called as a class method");
return;
}
-
- my $image_os_type = $self->data->get_image_os_type();
- my $computer_node_name = $self->data->get_computer_node_name();
-
- my $time = localtime;
my $file_path = 'currentimage.txt';
- $self->remove_lines_from_file($file_path, 'vcld_post_load');
+ my $time = localtime;
my $post_load_line = "vcld_post_load=success ($time)";
- # Assemble the command
- my $command;
- $command .= " echo >> $file_path";
- $command .= " && echo \"$post_load_line\" >> $file_path";
-
- my ($exit_status, $output) = $self->execute($command, 1);
- if (defined($exit_status) && $exit_status == 0) {
- notify($ERRORS{'DEBUG'}, 0, "added line to $file_path on
$computer_node_name: '$post_load_line'");
- }
- elsif ($exit_status) {
- notify($ERRORS{'WARNING'}, 0, "failed to add line to $file_path
on $computer_node_name: '$post_load_line', exit status: $exit_status,
output:\n" . join("\n", @$output));
- return;
- }
- else {
- notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to add
line to $file_path on $computer_node_name");
- return;
+ my $file_contents;
+ my @existing_lines = $self->get_file_contents($file_path);
+ for my $existing_line (@existing_lines) {
+ if ($existing_line !~ /\w/ || $existing_line =~
/^vcld_post_load/) {
+ next;
+ }
+ $file_contents .= "$existing_line\n";
}
-
- $self->set_text_file_line_endings($file_path);
- return 1;
+ $file_contents .= $post_load_line;
+ return $self->create_text_file($file_path, $file_contents);
}
#/////////////////////////////////////////////////////////////////////////////
@@ -2142,10 +2127,17 @@ sub get_public_default_gateway {
Parameters : $file_path, $file_contents, $append
Returns : boolean
- Description : Creates a text file on the computer. The $file_contents
- string argument is converted to ASCII hex values. These values
- are echo'd on the computer which avoids problems with special
- characters and escaping.
+ Description : Creates a text file on the computer or appends to an existing
+ file.
+
+ A trailing newline character is added to the end of the
+ file if one is not present in the $file_contents argument.
+
+ It is assumed that when appending to an existing file, the value
+ of $file_contents is intended to be added on a new line at the
+ end of the file. The contents of the existing file are first
+ retrieved. If the existing file does not contain a trailing
+ newline, one is added before appending to the file.
=cut
@@ -2167,24 +2159,57 @@ sub create_text_file {
}
my $computer_node_name = $self->data->get_computer_node_name();
- my $image_os_type = $self->data->get_image_os_type();
- # Attempt to create the parent directory if it does not exist
- if ($file_path =~ /[\\\/]/ && $self->can('create_directory')) {
- my $parent_directory_path = parent_directory_path($file_path);
- $self->create_directory($parent_directory_path) if
$parent_directory_path;
+ my $image_os_type = $self->data->get_image_os_type();
+ my $newline;
+ if ($image_os_type =~ /win/i) {
+ $newline = "\r\n";
+ }
+ else {
+ $newline = "\n";
}
- my $mode;
+ # Used only to format notify messages
my $mode_string;
if ($append) {
- $mode = '>>';
$mode_string = 'append';
+
+ # Retrieve the contents of the existing file if necessary
+ if ($self->file_exists($file_path)) {
+ my $existing_file_contents =
$self->get_file_contents($file_path);
+ if (!defined($existing_file_contents)) {
+ # Do not proceed if any problem occurred
retrieving the existing file contents
+ # Otherwise, it would be overwritten and data
would be lost
+ notify($ERRORS{'WARNING'}, 0, "failed to
$mode_string text file on $computer_node_name, append argument was specified
but contents of the existing file could not be retrieved: $file_path");
+ return;
+ }
+ elsif ($existing_file_contents &&
$existing_file_contents !~ /\n$/) {
+ # Add a newline to the end of the existing
contents if one isn't present
+ $existing_file_contents .= $newline;
+ $file_contents_string = $existing_file_contents
. $file_contents_string;
+ }
+
+ }
}
else {
- $mode = '>';
$mode_string = 'create';
}
+
+ # Make sure the file contents ends with a newline
+ # This is helpful to prevent problems with files such as sshd_config
and sudoers where a line might be echo'd to the end of it
+ # Without the newline, the last line could wind up being a merged line
if a simple echo is used to add a line
+ if (length($file_contents_string) && $file_contents_string !~ /\n$/) {
+ $file_contents_string .= $newline;
+ }
+
+ # Make line endings consistent
+ $file_contents_string =~ s/\r*\n/$newline/g;
+
+ # Attempt to create the parent directory if it does not exist
+ if ($file_path =~ /[\\\/]/ && $self->can('create_directory')) {
+ my $parent_directory_path = parent_directory_path($file_path);
+ $self->create_directory($parent_directory_path) if
$parent_directory_path;
+ }
# The echo method will fail of the file contents are too large
# An "Argument list too long" error will be displayed
@@ -2206,8 +2231,13 @@ sub create_text_file {
# Join the hex values together into a string
my $hex_string = join('', @hex_values);
- # Attempt to create/append the file using the hex string
- my $command = "echo -e \"$hex_string\" $mode \"$file_path\"";
+ # Enclose the file path in quotes if it contains a space
+ if ($file_path =~ /[\s]/) {
+ $file_path = "\"$file_path\"";
+ }
+
+ # Attempt to create the file using the hex string
+ my $command = "echo -n -e \"$hex_string\" > $file_path";
my ($exit_status, $output) = $self->execute($command, 0, 15, 1);
if (!defined($output)) {
notify($ERRORS{'DEBUG'}, 0, "failed to execute command
to $mode_string file on $computer_node_name, attempting to create file on
management node and copy it to $computer_node_name");
@@ -2228,20 +2258,7 @@ sub create_text_file {
notify($ERRORS{'DEBUG'}, 0, "skipping attempt to $mode_string
on $computer_node_name using echo, file content string length:
$file_contents_length");
}
- # File will be copied from the management node to the computer
- if ($append) {
- if ($self->file_exists($file_path)) {
- my $existing_file_contents =
$self->get_file_contents($file_path);
- if (defined($existing_file_contents)) {
- $file_contents_string = $existing_file_contents
. $file_contents_string;
- }
- else {
- notify($ERRORS{'WARNING'}, 0, "failed to
$mode_string text file on $computer_node_name, append argument was specified
but contents of the existing file could not be retrieved: $file_path");
- return;
- }
- }
- }
-
+ # File was not created using the quicker echo method
# Create a temporary file on the management node, copy it to the
computer, then delete temporary file from management node
my $mn_temp_file_path = tmpnam();
if (!$self->mn_os->create_text_file($mn_temp_file_path,
$file_contents_string, $append)) {
@@ -2253,7 +2270,6 @@ sub create_text_file {
return;
}
$self->mn_os->delete_file($mn_temp_file_path);
-
notify($ERRORS{'DEBUG'}, 0, "created text file on $computer_node_name
by copying a file created on management node");
return 1;
}
@@ -4122,13 +4138,9 @@ sub update_cluster {
$cluster_info_string .= "$cluster_computer_public_ip_address\n";
}
- # Remove trailing newline
- $cluster_info_string =~ s/\n$//;
-
# Create the cluster_info file on the computer
if ($self->create_text_file($cluster_info_file_path,
$cluster_info_string)) {
notify($ERRORS{'DEBUG'}, 0, "created $cluster_info_file_path on
$computer_short_name:\n$cluster_info_string");
- $self->set_text_file_line_endings($cluster_info_file_path);
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to create
$cluster_info_file_path on $computer_short_name");
@@ -4136,7 +4148,7 @@ sub update_cluster {
}
# Open the firewall allowing other cluster reservations computers access
- if ($self->can('enable_firewall_port')) {
+ if (@public_ip_addresses && $self->can('enable_firewall_port')) {
my $firewall_scope = join(",", @public_ip_addresses);
notify($ERRORS{'DEBUG'}, 0, "attempting to open the firewall on
$computer_short_name to allow access from other cluster reservation computers:
$firewall_scope");
Modified: vcl/trunk/managementnode/lib/VCL/Module/OS/Linux.pm
URL:
http://svn.apache.org/viewvc/vcl/trunk/managementnode/lib/VCL/Module/OS/Linux.pm?rev=1695554&r1=1695553&r2=1695554&view=diff
==============================================================================
--- vcl/trunk/managementnode/lib/VCL/Module/OS/Linux.pm (original)
+++ vcl/trunk/managementnode/lib/VCL/Module/OS/Linux.pm Wed Aug 12 15:13:30 2015
@@ -636,12 +636,8 @@ sub post_reservation {
Parameters : none
Returns : boolean
Description : Retrieves the public IP address being used on the Linux
computer.
- Runs ipcalc locally on the management node to determine the
- registered hostname for that IP address. If unable to determine
- the hostname by running ipcalc on the management node, an
attempt
- is made to run ipcalc on the Linux computer. Once the hostname
is
- determined, the hostname command is run to set the hostname on
- the Linux computer.
+ Determines the hostname the IP address resolves to. Sets the
+ hostname on the Linux computer.
=cut
@@ -654,93 +650,92 @@ sub update_public_hostname {
my $computer_node_name = $self->data->get_computer_node_name();
- # Get the IP address of the public adapter
- my $public_ip_address = $self->get_public_ip_address();
- if (!$public_ip_address) {
- notify($ERRORS{'WARNING'}, 0, "hostname cannot be set, unable
to determine public IP address");
- return;
+ my $public_hostname = shift;
+ if (!$public_hostname) {
+ # Get the IP address of the public adapter
+ my $public_ip_address = $self->get_public_ip_address();
+ if (!$public_ip_address) {
+ notify($ERRORS{'WARNING'}, 0, "hostname cannot be set,
unable to determine public IP address");
+ return;
+ }
+ notify($ERRORS{'DEBUG'}, 0, "retrieved public IP address of
$computer_node_name: $public_ip_address");
+
+ # Get the hostname for the public IP address
+ $public_hostname = ip_address_to_hostname($public_ip_address)
|| $computer_node_name;
}
- notify($ERRORS{'DEBUG'}, 0, "retrieved public IP address of
$computer_node_name: $public_ip_address");
- # Get the hostname for the public IP address
- my $public_hostname = ip_address_to_hostname($public_ip_address) ||
$computer_node_name;
+ my $error_occurred = 0;
- # Set the node's hostname to public hostname
- if ($self->can("update_hostname_file")) {
- if (!$self->update_hostname_file($public_hostname)) {
- notify($ERRORS{'WARNING'}, 0, "failed to update
hostname file");
+ # Check if hostname file exists and update if necessary
+ my $hostname_file_path = '/etc/hostname';
+ if ($self->file_exists($hostname_file_path)) {
+ if ($self->create_text_file($hostname_file_path,
$public_hostname)) {
+ notify($ERRORS{'DEBUG'}, 0, "updated
$hostname_file_path on $computer_node_name with hostname '$public_hostname'");
+ }
+ else {
+ notify($ERRORS{'WARNING'}, 0, "failed to update
$hostname_file_path on $computer_node_name with '$public_hostname'");
+ $error_occurred = 1;
}
- }
-
- my $hostname_command = "hostname $public_hostname";
- my ($hostname_exit_status, $hostname_output) =
$self->execute($hostname_command);
- if (!defined($hostname_output)) {
- notify($ERRORS{'WARNING'}, 0, "failed to SSH command to set
hostname on $computer_node_name to $public_hostname, command:
'$hostname_command'");
- return;
- }
- elsif ($hostname_exit_status == 0) {
- notify($ERRORS{'OK'}, 0, "set public hostname on
$computer_node_name to $public_hostname");
- return 1;
}
else {
- notify($ERRORS{'WARNING'}, 0, "failed to set public hostname on
$computer_node_name to $public_hostname, exit status: $hostname_exit_status,
output:\n" . join("\n", @$hostname_output));
- return 0;
+ notify($ERRORS{'DEBUG'}, 0, "$hostname_file_path not updated on
$computer_node_name because the file does not exist");
}
- return 1;
-}
-
-#/////////////////////////////////////////////////////////////////////////////
-
-=head2 update_hostname_file
-
- Parameters : hostname
- Returns : boolean
- Description : updates the static hostname file on node, so hostname persists
across reboots
- seperated from update_public_hostname as the file location and
format can differ
- accross Linux distributions
-
-=cut
-
-sub update_hostname_file {
- my $self = shift;
- if (ref($self) !~ /linux/i) {
- notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a
function, it must be called as a class method");
- return 0;
- }
-
- my $public_hostname = shift;
- if (!$public_hostname) {
- notify($ERRORS{'WARNING'}, 0, "public_hostname was not passed
correctly");
- return 0;
- }
-
- my $computer_node_name = $self->data->get_computer_node_name();
- my $network_file_path = '/etc/sysconfig/network';
- my $hostname_file_path = '/etc/hostname';
- # For Linux OS that are using /etc/hostname
- if($self->file_exists($hostname_file_path)) {
- my $update_hostname_file_cmd = "echo $public_hostname >
$hostname_file_path";
- if($self->execute($update_hostname_file_cmd)) {
- notify($ERRORS{'OK'}, 0, "updated $hostname_file_path
on $computer_node_name to $public_hostname");
+ # Check if network file exists and update if necessary
+ my $network_file_path = '/etc/sysconfig/network';
+ if ($self->file_exists($network_file_path)) {
+ my $sed_command = "sed -i -e \"/^HOSTNAME=/d\"
$network_file_path; echo \"HOSTNAME=$public_hostname\" >> $network_file_path";
+ my ($sed_exit_status, $sed_output) =
$self->execute($sed_command);
+ if (!defined($sed_output)) {
+ notify($ERRORS{'WARNING'}, 0, "failed to execute
command to update hostname in $network_file_path on $computer_node_name");
+ return;
+ }
+ elsif ($sed_exit_status != 0) {
+ notify($ERRORS{'WARNING'}, 0, "failed to update
hostname in $network_file_path on $computer_node_name, exit status:
$sed_exit_status, output:\n" . join("\n", @$sed_output));
+ $error_occurred = 1;
+ }
+ else {
+ notify($ERRORS{'OK'}, 0, "updated hostname in
$network_file_path on $computer_node_name to '$public_hostname'");
}
}
-
- my $command = "sed -i -e \"/^HOSTNAME=/d\" $network_file_path; echo
\"HOSTNAME=$public_hostname\" >> $network_file_path";
- my ($exit_status, $output) = $self->execute($command);
- if (!defined($output)) {
- notify($ERRORS{'WARNING'}, 0, "failed to SSH command to set
hostname on $computer_node_name to $public_hostname, command: '$command'");
+ else {
+ notify($ERRORS{'DEBUG'}, 0, "$network_file_path not updated on
$computer_node_name because the file does not exist");
}
- elsif ($exit_status == 0) {
- notify($ERRORS{'OK'}, 0, "set public hostname on
$computer_node_name to $public_hostname");
+
+ # Check if hostnamectl exists, this is provided by systemd on
CentOS/RHEL 7+
+ if ($self->command_exists('hostnamectl')) {
+ my $hostnamectl_command = "hostnamectl set-hostname
$public_hostname";
+ my ($hostnamectl_exit_status, $hostnamectl_output) =
$self->execute($hostnamectl_command);
+ if (!defined($hostnamectl_output)) {
+ notify($ERRORS{'WARNING'}, 0, "failed to execute
command to set hostname using hostnamectl command on $computer_node_name to
$public_hostname");
+ return;
+ }
+ elsif ($hostnamectl_exit_status != 0) {
+ notify($ERRORS{'WARNING'}, 0, "failed to set hostname
using hostnamectl command on $computer_node_name to $public_hostname, exit
status: $hostnamectl_exit_status, command: '$hostnamectl_command', output:\n" .
join("\n", @$hostnamectl_output));
+ $error_occurred = 1;
+ }
+ else {
+ notify($ERRORS{'OK'}, 0, "set hostname using
hostnamectl command on $computer_node_name to $public_hostname");
+ }
}
else {
- notify($ERRORS{'WARNING'}, 0, "failed to set public hostname on
$computer_node_name to $public_hostname, exit status: $exit_status, output:\n"
. join("\n", @ $output));
+ my $hostname_command = "hostname $public_hostname";
+ my ($hostname_exit_status, $hostname_output) =
$self->execute($hostname_command);
+ if (!defined($hostname_output)) {
+ notify($ERRORS{'WARNING'}, 0, "failed to execute
command to set hostname using hostname command on $computer_node_name to
$public_hostname");
+ return;
+ }
+ elsif ($hostname_exit_status != 0) {
+ notify($ERRORS{'WARNING'}, 0, "failed to set hostname
using hostname command on $computer_node_name to $public_hostname, exit status:
$hostname_exit_status, command: '$hostname_command', output:\n" . join("\n",
@$hostname_output));
+ $error_occurred = 1;
+ }
+ else {
+ notify($ERRORS{'OK'}, 0, "set hostname using hostname
command on $computer_node_name to $public_hostname");
+ }
}
-
- return 1;
-
+
+ return !$error_occurred;
}
#/////////////////////////////////////////////////////////////////////////////
@@ -2770,17 +2765,13 @@ sub create_user {
notify($ERRORS{'WARNING'}, 0, "failed to process
grant_connect_method_access for $username");
}
}
-
+
# Add user to sudoers if necessary
- if ($self->can("grant_root_access")) {
- if (!$self->grant_root_access({
- username => $username,
- root_access => $root_access,
- })) {
- notify($ERRORS{'WARNING'}, 0, "failed to process
grant_root_access for $username");
- }
+ if ($root_access && !$self->grant_root_access($username)) {
+ notify($ERRORS{'WARNING'}, 0, "failed to process
grant_root_access for $username");
+ return;
}
-
+
return 1;
} ## end sub create_user
@@ -2790,8 +2781,8 @@ sub create_user {
=head2 grant_root_access
Parameters : $username
- Returns : 1 , 0
- Description : Updates sudoers file for
+ Returns : boolean
+ Description : Adds the user to the sudoers file.
=cut
@@ -2802,45 +2793,22 @@ sub grant_root_access {
return;
}
- my $user_parameters = shift;
- if (!$user_parameters) {
- notify($ERRORS{'WARNING'}, 0, "unable to grant acess, user
parameters argument was not provided");
- return;
- }
- elsif (!ref($user_parameters) || ref($user_parameters) ne 'HASH') {
- notify($ERRORS{'WARNING'}, 0, "unable to grant access, argument
provided is not a hash reference");
- return;
- }
-
- my $username = $user_parameters->{username};
+ my $username = shift;
if (!defined($username)) {
- notify($ERRORS{'WARNING'}, 0, "argument hash does not contain a
'username' key:\n" . format_data($user_parameters));
+ notify($ERRORS{'WARNING'}, 0, "username argument was not
supplied");
return;
}
- my $root_access = $user_parameters->{root_access};
- if (!defined($root_access)) {
- notify($ERRORS{'WARNING'}, 0, "argument hash does not contain a
'root_access' key:\n" . format_data($user_parameters));
- return;
- }
-
- if ($root_access) {
- my $sudoers_file_path = '/etc/sudoers';
- my $sudoers_line = "$username ALL= NOPASSWD: ALL";
- if ($self->append_text_file($sudoers_file_path, $sudoers_line))
{
- notify($ERRORS{'DEBUG'}, 0, "appended line to
$sudoers_file_path: '$sudoers_line'");
- return 1;
- }
- else {
- notify($ERRORS{'WARNING'}, 0, "failed to append line to
$sudoers_file_path: '$sudoers_line'");
- return 1;
- }
+ my $sudoers_file_path = '/etc/sudoers';
+ my $sudoers_line = "$username ALL= NOPASSWD: ALL";
+ if ($self->append_text_file($sudoers_file_path, $sudoers_line)) {
+ notify($ERRORS{'DEBUG'}, 0, "appended line to
$sudoers_file_path: '$sudoers_line'");
+ return 1;
}
else {
- notify($ERRORS{'OK'}, 0, "root access for user $username was
not allowed root_access = $root_access ");
- return 1;
+ notify($ERRORS{'WARNING'}, 0, "failed to append line to
$sudoers_file_path: '$sudoers_line'");
+ return 0;
}
-
}
#/////////////////////////////////////////////////////////////////////////////
@@ -6095,7 +6063,7 @@ sub get_ssh_public_key_string {
}
if ($public_key_string) {
if ($comment) {
- $public_key_string =~ s/(ssh-[^\s]+\s[^\s=]+).*$/$1==
$comment/g;
+ $public_key_string =~ s/(ssh-[^\s]+\s[^\s=]+).*$/$1
$comment/g;
}
return $public_key_string;
}
@@ -6183,6 +6151,63 @@ sub _get_ssh_public_key_string_helper {
}
}
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 install_package
+
+ Parameters : $package_name, $timeout_seconds (optional)
+ Returns : boolean
+ Description : Installs a Linux package using yum.
+
+=cut
+
+sub install_package {
+ my $self = shift;
+ if (ref($self) !~ /linux/i) {
+ notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a
function, it must be called as a class method");
+ return;
+ }
+
+ my ($package_name, $timeout_seconds) = @_;
+ if (!$package_name) {
+ notify($ERRORS{'WARNING'}, 0, "package name argument was not
supplied");
+ return;
+ }
+
+ my $computer_name = $self->data->get_computer_node_name();
+
+ if (!$self->command_exists('yum')) {
+ notify($ERRORS{'WARNING'}, 0, "failed to install $package_name
on $computer_name, yum command is not available");
+ return;
+ }
+
+ $timeout_seconds = 120 unless $timeout_seconds;
+
+ my $command = "yum install -q -y $package_name";
+ notify($ERRORS{'DEBUG'}, 0, "attempting to install $package_name using
yum on $computer_name, timeout seconds: $timeout_seconds");
+ my ($exit_status, $output) = $self->execute({
+ 'command' => $command,
+ 'display_output' => 0,
+ 'timeout_seconds' => $timeout_seconds,
+ });
+ if (!defined($output)) {
+ notify($ERRORS{'WARNING'}, 0, "failed to execute command to
install $package_name using yum on $computer_name");
+ return;
+ }
+ elsif ($exit_status ne 0) {
+ notify($ERRORS{'WARNING'}, 0, "failed to install $package_name
using yum on $computer_name, exit status: $exit_status, command: '$command',
output:\n" . join("\n", @$output));
+ return 0;
+ }
+ elsif (grep(/$package_name.+already installed/, @$output)) {
+ notify($ERRORS{'DEBUG'}, 0, "$package_name is already installed
on $computer_name, command: '$command', output:\n" . join("\n", @$output));
+ return 1;
+ }
+ else {
+ notify($ERRORS{'OK'}, 0, "installed $package_name using yum on
$computer_name, command: '$command', output:\n" . join("\n", @$output));
+ return 1;
+ }
+}
+
##/////////////////////////////////////////////////////////////////////////////
1;
__END__