#!/usr/bin/perl -w
######################################################################
#
# $Id: webjob-jqd-change-state,v 1.23 2012/01/07 08:01:17 mavrik Exp $
#
######################################################################
#
# Copyright 2008-2012 The WebJob Project, All Rights Reserved.
#
######################################################################
#
# Purpose: Change the state of the specified queues or jobs.
#
######################################################################

use FindBin qw($Bin $RealBin); use lib ("$Bin/../lib/perl5/site_perl", "$RealBin/../lib/perl5/site_perl", "/usr/local/webjob/lib/perl5/site_perl", "/opt/local/webjob/lib/perl5/site_perl");

use strict;
use File::Basename;
use Getopt::Std;
use WebJob::JqdRoutines 1.024;
use WebJob::KvpRoutines 1.029;
use WebJob::Properties 1.035;

BEGIN
{
  ####################################################################
  #
  # The Properties hash is essentially private. Those parts of the
  # program that wish to access or modify the data in this hash need
  # to call GetProperties() to obtain a reference.
  #
  ####################################################################

  my (%hProperties);

  ####################################################################
  #
  # Initialize regex variables.
  #
  ####################################################################

  $hProperties{'CommonRegexes'} = PropertiesGetGlobalRegexes();

  ####################################################################
  #
  # Initialize platform-specific variables.
  #
  ####################################################################

  if ($^O =~ /MSWin(32|64)/i)
  {
    $hProperties{'OsClass'} = "WINX";
  }
  else
  {
    $hProperties{'OsClass'} = "UNIX";
  }

  ####################################################################
  #
  # Define helper routines.
  #
  ####################################################################

  sub GetProperties
  {
    return \%hProperties;
  }
}

######################################################################
#
# Main Routine
#
######################################################################

  ####################################################################
  #
  # Punch in and go to work.
  #
  ####################################################################

  my ($phProperties);

  $phProperties = GetProperties();

  $$phProperties{'Program'} = basename(__FILE__);

  ####################################################################
  #
  # Get Options.
  #
  ####################################################################

  my (%hOptions);

  if (!getopts('d:e:G:i:j:l:s:t:', \%hOptions))
  {
    Usage($$phProperties{'Program'});
  }

  ####################################################################
  #
  # A job queue directory, '-d', is optional.
  #
  ####################################################################

  $$phProperties{'JobQueueDirectory'} = (exists($hOptions{'d'})) ? $hOptions{'d'} : "/var/webjob/spool/jqd";

  ####################################################################
  #
  # A comma delimited list of excludes, '-e', is optional.
  #
  ####################################################################

  $$phProperties{'ExcludeList'} = (exists($hOptions{'e'})) ? $hOptions{'e'} : "";

  ####################################################################
  #
  # A group file, '-G', is optional.
  #
  ####################################################################

  $$phProperties{'GroupsFile'} = (exists($hOptions{'G'})) ? $hOptions{'G'} : "/var/webjob/config/jqd/groups";

  ####################################################################
  #
  # A comma delimited list of includes, '-i', is required.
  #
  ####################################################################

  my (@aIncludes);

  $$phProperties{'IncludeList'} = (exists($hOptions{'i'})) ? $hOptions{'i'} : "";

  if ($$phProperties{'IncludeList'} eq "")
  {
    Usage($$phProperties{'Program'});
  }

  ####################################################################
  #
  # A job regular expressions file, '-j', is optional.
  #
  ####################################################################

  my ($sFileHandle);

  if (exists($hOptions{'j'}))
  {
    my $sFile = $hOptions{'j'};
    if ($sFile eq '-')
    {
      $sFileHandle = \*STDIN;
    }
    else
    {
      if (!open(FH, "< $sFile"))
      {
        print STDERR "$$phProperties{'Program'}: Error='Unable to open $sFile ($!).'\n";
        exit(2);
      }
      $sFileHandle = \*FH;
    }
  }

  ####################################################################
  #
  # A log file, '-l', is optional.
  #
  ####################################################################

  $$phProperties{'LogFile'} = (exists($hOptions{'l'})) ? $hOptions{'l'} : "/var/webjob/logfiles/jqd.log";

  ####################################################################
  #
  # A comma delimited list of states, '-s', is required.
  #
  ####################################################################

  $$phProperties{'OldNewStates'} = (exists($hOptions{'s'})) ? $hOptions{'s'} : undef;

  if (!defined($$phProperties{'OldNewStates'}))
  {
    Usage($$phProperties{'Program'});
  }

  ####################################################################
  #
  # A state type, '-t', is optional.
  #
  ####################################################################

  $$phProperties{'Type'} = (exists($hOptions{'t'})) ? $hOptions{'t'} : "job";

  if (!defined($$phProperties{'Type'}) || $$phProperties{'Type'} !~ /^(?:job|queue)$/i)
  {
    print STDERR "$$phProperties{'Program'}: Error='Invalid state type ($$phProperties{'Type'}).'\n";
    exit(2);
  }

  ####################################################################
  #
  # Finalize the old/new states.
  #
  ####################################################################

  my (%hOldStates);

  if ($$phProperties{'Type'} =~ /^queue$/i)
  {
    if ($$phProperties{'OldNewStates'} !~ /^([a-z,]+):(active|frozen|locked)$/i)
    {
      print STDERR "$$phProperties{'Program'}: Error='The old/new states ($$phProperties{'OldNewStates'}) do not pass muster for the specified state type ($$phProperties{'Type'}).'\n";
      exit(2);
    }
    $$phProperties{'OldStateList'} = $1;
    $$phProperties{'NewState'} = $2;
    if ($$phProperties{'OldStateList'} =~ /^any$/i)
    {
      foreach my $sOldState ("active", "frozen", "locked")
      {
        $hOldStates{$sOldState}++;
      }
    }
    else
    {
      foreach my $sOldState (split(/,/, $$phProperties{'OldStateList'}))
      {
        if ($sOldState =~ /^(active|frozen|locked)$/i)
        {
          $hOldStates{lc($1)}++;
        }
        else
        {
          print STDERR "$$phProperties{'Program'}: Error='Invalid queue state ($sOldState).'\n";
          exit(2);
        }
      }
    }
  }
  else
  {
    if ($$phProperties{'OldNewStates'} !~ /^([a-z,]+):(hold|todo|sent|open|done|pass|fail|foul)$/i)
    {
      print STDERR "$$phProperties{'Program'}: Error='The old/new states ($$phProperties{'OldNewStates'}) do not pass muster for the specified state type ($$phProperties{'Type'}).'\n";
      exit(2);
    }
    $$phProperties{'OldStateList'} = $1;
    $$phProperties{'NewState'} = $2;
    if ($$phProperties{'OldStateList'} =~ /^any$/i)
    {
      foreach my $sOldState ("hold", "todo", "sent", "open", "done", "pass", "fail", "foul")
      {
        $hOldStates{$sOldState}++;
      }
    }
    else
    {
      foreach my $sOldState (split(/,/, $$phProperties{'OldStateList'}))
      {
        if ($sOldState =~ /^(hold|todo|sent|open|done|pass|fail|foul)$/i)
        {
          $hOldStates{lc($1)}++;
        }
        else
        {
          print STDERR "$$phProperties{'Program'}: Error='Invalid job state ($sOldState).'\n";
          exit(2);
        }
      }
    }
  }

  @{$$phProperties{'OldStates'}} = sort(keys(%hOldStates));
  if (scalar(@{$$phProperties{'OldStates'}}) < 1)
  {
    print STDERR "$$phProperties{'Program'}: Error='At least one old state must be specified.'\n";
    exit(2);
  }

  ####################################################################
  #
  # Make sure the job queue directory exists.
  #
  ####################################################################

  if (!-d $$phProperties{'JobQueueDirectory'})
  {
    print STDERR "$$phProperties{'Program'}: Error='Base directory ($$phProperties{'JobQueueDirectory'}) does not exist.'\n";
    exit(2);
  }

  ####################################################################
  #
  # Create forward/reverse queue maps.
  #
  ####################################################################

  my (%hForwardQueueMap, %hQueueMapArgs, %hReverseQueueMap);

  %hQueueMapArgs =
  (
    'ForwardQueueMap'   => \%hForwardQueueMap,
    'JobQueueDirectory' => $$phProperties{'JobQueueDirectory'},
    'KeyRegex'          => "(?:" . $$phProperties{'CommonRegexes'}{'ClientId'} . "|" . $$phProperties{'CommonRegexes'}{'ClientSuppliedFilename'} . ")",
    'ReverseQueueMap'   => \%hReverseQueueMap,
    'SharedLock'        => 1,
    'ValueRegex'        => "\\d+",
  );
  if (!JqdSetupQueueMaps(\%hQueueMapArgs))
  {
    print STDERR "$$phProperties{'Program'}: Error='$hQueueMapArgs{'Error'}'\n";
    exit(2);
  }

  ####################################################################
  #
  # Create group queue map, and conditionally fill it with groups.
  #
  ####################################################################

  my (%hGroupMap);

  %hGroupMap = ();

  if (-f $$phProperties{'GroupsFile'})
  {
    my $sKeyRegex = $$phProperties{'CommonRegexes'}{'Group'};
    my $sUnitRegex = "(?:" . $$phProperties{'CommonRegexes'}{'GroupToken'} . "|" . $$phProperties{'CommonRegexes'}{'ClientId'} . ")";
    my $sValueRegex = "(?:|$sUnitRegex(?:,$sUnitRegex)*)"; # Empty groups are allowed.
    my %hLArgs =
    (
      'File'           => $$phProperties{'GroupsFile'},
      'Properties'     => \%hGroupMap,
      'Template'       => { $sKeyRegex => $sValueRegex, },
      'VerifyValues'   => 1,
    );
    if (!KvpGetKvps(\%hLArgs))
    {
      print STDERR "$$phProperties{'Program'}: Error='$hLArgs{'Error'}'\n";
      exit(2);
    }
  }

  ####################################################################
  #
  # Resolve the include and exclude lists.
  #
  ####################################################################

  my (%hExcludes, %hIncludes, %hResolverArgs);

  %hResolverArgs =
  (
    'ForwardMap'     => \%hForwardQueueMap,
    'GroupMap'       => \%hGroupMap,
    'QueueMap'       => \%hIncludes,
    'QueueList'      => $$phProperties{'IncludeList'},
    'RecursionStack' => [],
  );
  if (!JqdResolveQueueList(\%hResolverArgs))
  {
    print STDERR "$$phProperties{'Program'}: IncludeList='$$phProperties{'IncludeList'}' Error='$hResolverArgs{'Error'}'\n";
    exit(2);
  }

  %hResolverArgs =
  (
    'ForwardMap'     => \%hForwardQueueMap,
    'GroupMap'       => \%hGroupMap,
    'QueueMap'       => \%hExcludes,
    'QueueList'      => $$phProperties{'ExcludeList'},
    'RecursionStack' => [],
  );
  if (!JqdResolveQueueList(\%hResolverArgs))
  {
    print STDERR "$$phProperties{'Program'}: ExcludeList='$$phProperties{'ExcludeList'}' Error='$hResolverArgs{'Error'}'\n";
    exit(2);
  }

  ####################################################################
  #
  # Bash the excludes against the includes, and remove the overlap.
  #
  ####################################################################

  foreach my $sExclude (sort(keys(%hExcludes)))
  {
    if (exists($hIncludes{$sExclude}))
    {
      delete($hIncludes{$sExclude});
    }
  }

  ####################################################################
  #
  # Make sure all queues exist.
  #
  ####################################################################

  my (@aMissingQueues);

  foreach my $sQueue (sort(keys(%hIncludes)))
  {
    my $sQueueDirectory = $$phProperties{'JobQueueDirectory'} . "/" . $sQueue;
    if (!-d $sQueueDirectory)
    {
      push(@aMissingQueues, $sQueue);
    }
  }

  if (scalar(@aMissingQueues) > 0)
  {
    print STDERR "$$phProperties{'Program'}: Error='One or more queues (" . join(",", @aMissingQueues) . ") do not exist. No queues can be manipulated until this is fixed.'\n";
    exit(2);
  }

  ####################################################################
  #
  # Set the job changer.
  #
  ####################################################################

  my ($sChangerUsername, $sChangerUid);

  if ($$phProperties{'OsClass'} =~ /^UNIX$/)
  {
    my $sChangerTty = `tty`;
    if (defined($sChangerTty) && $sChangerTty =~ /^\/dev\//)
    {
      $sChangerTty =~ s/[\r\n]*$//;
      $sChangerUid = (stat($sChangerTty))[4];
      if (!defined($sChangerUid))
      {
        print STDERR "$$phProperties{'Program'}: Error='Unable to determine user ID (UID).'\n";
        exit(2);
      }
    }
    else
    {
      $sChangerUid = $<;
    }
  }
  else
  {
    $sChangerUid = $<;
  }

  $sChangerUsername = (getpwuid($sChangerUid))[0];
  if (!defined($sChangerUid))
  {
    print STDERR "$$phProperties{'Program'}: Error='Unable to determine username.'\n";
    exit(2);
  }

  ####################################################################
  #
  # Conditionally build up an array of job regular expressions.
  #
  ####################################################################

  my (@aJobNameRegexes);

  if (defined($sFileHandle))
  {
    while (my $sJobNameRegex = <$sFileHandle>)
    {
      next if ($sJobNameRegex =~ /^\s*$/);
      $sJobNameRegex =~ s/[\r\n]+$//;
      push(@aJobNameRegexes, $sJobNameRegex);
    }
    close($sFileHandle);
  }

  ####################################################################
  #
  # Do some work.
  #
  ####################################################################

  my (@aResults, %hLogArgs, %hQueueLockArgs, $sError);

  %hLogArgs =
  (
    'Creator'       => $sChangerUsername,
    'LogFile'       => $$phProperties{'LogFile'},
    'Pid'           => $$,
    'Program'       => $$phProperties{'Program'},
  );

if ($$phProperties{'Type'} =~ /^queue$/i)
{
  foreach my $sQueue (sort(keys(%hIncludes)))
  {
    ##################################################################
    #
    # Set the queue directory.
    #
    ##################################################################

    my ($sJqdQueueDirectory);

    $sJqdQueueDirectory = $$phProperties{'JobQueueDirectory'} . "/" . $sQueue;
    if (!-d $sJqdQueueDirectory)
    {
      next;
    }

    ##################################################################
    #
    # Only change the queue state if the current state matches one of
    # the specified old states.
    #
    ##################################################################

    my (%hLArgs, $sStatus);

    %hLArgs =
    (
      'Directory'          => $sJqdQueueDirectory,
      'NewState'           => $$phProperties{'NewState'},
      'OldStates'          => $$phProperties{'OldStates'},
      'RequireStateMatch'  => 0,
    );
    $sStatus = JqdSetQueueState(\%hLArgs);
    if (!defined($sStatus))
    {
      print STDERR "$$phProperties{'Program'}: Queue='$sQueue' Error='$hLArgs{'Error'}'\n";
      push(@aResults, "fail|$hLArgs{'CurrentState'}|$sQueue");
    }
    else
    {
      push(@aResults, "$sStatus|$hLArgs{'CurrentState'}|$sQueue");
    }
  }
}
else
{
  foreach my $sJobNameRegex (@ARGV, @aJobNameRegexes)
  {
    foreach my $sQueue (sort(keys(%hIncludes)))
    {
      ################################################################
      #
      # Set the queue directory.
      #
      ################################################################

      my ($sJqdQueueDirectory);

      $sJqdQueueDirectory = $$phProperties{'JobQueueDirectory'} . "/" . $sQueue;
      if (!-d $sJqdQueueDirectory)
      {
        next;
      }

      ################################################################
      #
      # Acquire the change lock. This is an exclusive lock.
      #
      ################################################################

      %hQueueLockArgs =
      (
        'LockFile' => $sJqdQueueDirectory . "/" . "change.lock",
        'LockMode' => "+<", # The file must exist or this will fail.
      );
      if (!JqdLockFile(\%hQueueLockArgs))
      {
        print STDERR "$$phProperties{'Program'}: Queue='$sQueue' Error='$hQueueLockArgs{'Error'}'\n";
        next;
      }

      ################################################################
      #
      # If the queue is in the locked state, we're done since no job
      # changes are allowed.
      #
      ################################################################

      if (JqdIsQueueLocked({ 'Directory' => $sJqdQueueDirectory }))
      {
        print STDERR "$$phProperties{'Program'}: Queue='$sQueue' Warning='Queue is locked. No job changes are allowed.'\n";
        next;
      }

      ################################################################
      #
      # Change jobs that match the specified expression, but only do
      # this for the specified job states.
      #
      ################################################################

      foreach my $sOldState (@{$$phProperties{'OldStates'}})
      {
        my $sStateDirectory = $sJqdQueueDirectory . "/" . $sOldState;
        if (!opendir(DIR, $sStateDirectory))
        {
          print STDERR "$$phProperties{'Program'}: Queue='$sQueue' Error='Directory ($sStateDirectory) could not be opened ($!).'\n";
          next;
        }
        my @aJobFiles = map("$sStateDirectory/$_", sort(grep(/$sJobNameRegex/, readdir(DIR))));
        closedir(DIR);
        foreach my $sJobFile (@aJobFiles)
        {
          if (-f $sJobFile)
          {
#FIXME Move this code block into a subroutine. <<EOF
            $hLogArgs{'NewQueueState'} = $$phProperties{'NewState'};
            $hLogArgs{'OldQueueState'} = $sOldState;
            $hLogArgs{'Queue'} = $sQueue;
            $hLogArgs{'QueueTag'} = basename($sJobFile);
            $hLogArgs{'Result'} = "fail";

            my (%hJobProperties, %hLArgs);

            %hLArgs =
            (
              'File'           => $sJobFile,
              'Properties'     => \%hJobProperties,
#FIXME Which keys should be required?
              'RequiredKeys'   => ['Command', 'CommandPath', 'JobGroup'],
              'RequireAllKeys' => 0,
              'Template'       => PropertiesGetGlobalTemplates()->{'jqd.job'},
              'VerifyValues'   => 1,
            );
            if (!KvpGetKvps(\%hLArgs))
            {
              $hLogArgs{'Message'} = $hLArgs{'Error'};
              JqdLogMessage(\%hLogArgs);
              print STDERR "$$phProperties{'Program'}: Queue='$sQueue' Error='$hLogArgs{'Message'}'\n";
              push(@aResults, "fail|$sJobFile");
              next;
            }

            $hLogArgs{'Command'} = $hJobProperties{'Command'},
            $hLogArgs{'CommandSize'} = $hJobProperties{'CommandSize'},
            $hLogArgs{'JobGroup'} = $hJobProperties{'JobGroup'},
            $hLogArgs{'PoundName'} = basename($hJobProperties{'CommandPath'});
#FIXME EOF

            (my $sNewFile = $sJobFile) =~ s/$sOldState\/($$phProperties{'CommonRegexes'}{'JqdQueueTag'})$/$$phProperties{'NewState'}\/$1/;
            my $sStatus = ChangeJobState($sJobFile, $sNewFile, $sOldState, $$phProperties{'NewState'}, \$sError);
            if (!defined($sStatus))
            {
              $hLogArgs{'Message'} = "Unable to change the job ($sJobFile) to the specified state ($sError).";
              print STDERR "$$phProperties{'Program'}: Queue='$sQueue' Error='$hLogArgs{'Message'}'\n";
              push(@aResults, "fail|$sJobFile");
            }
            elsif ($sStatus eq "noop")
            {
              next;
            }
            else
            {
              $hLogArgs{'Result'} = "pass";
              $hLogArgs{'Message'} = "Job state changed successfully.";
              push(@aResults, "pass|$sNewFile");
            }
            JqdLogMessage(\%hLogArgs);
          }
        }
      }
    }
    continue
    {
      ################################################################
      #
      # Release the change lock.
      #
      ################################################################

      JqdUnlockFile(\%hQueueLockArgs);
    }
  }
}

  ####################################################################
  #
  # Report the results.
  #
  ####################################################################

  foreach my $sResult (@aResults)
  {
    print $sResult, "\n";
  }

  ####################################################################
  #
  # Shutdown and go home.
  #
  ####################################################################

  1;


######################################################################
#
# ChangeJobState
#
######################################################################

sub ChangeJobState
{
  my ($sJobFile, $sNewFile, $sOldState, $sNewState, $psError) = @_;

  ####################################################################
  #
  # If the current and new states match, we're done.
  #
  ####################################################################

  if ($sOldState eq $sNewState)
  {
    return "noop"; # No state change is required.
  }

  ####################################################################
  #
  # Change state.
  #
  ####################################################################

#FIXME Create a matrix that defines how to handle each requested state transition.
  if ($sOldState =~ /^(?:open|done|pass|fail|foul)$/ && $sNewState =~ /^(?:hold|todo|sent)$/)
  {
    if (!open(JOBFH, "< $sJobFile"))
    {
      $$psError = "failed to open old job file: $!";
      return undef;
    }
    my $sTmpFile = $sJobFile . ".tmp";
    if (!open(TMPFH, "> $sTmpFile"))
    {
      $$psError = "failed to open temporary job file: $!";
      close(JOBFH);
      return undef;
    }
    while (my $sLine = <JOBFH>)
    {
      if ($sLine =~ /^(?:Command|Command(?:Alias|Line|Md5|Path|Sha1|Size)|Comment|Created|Creator|JobGroup|JqdId)\s*=/i)
      {
        print TMPFH $sLine;
      }
    }
    close(JOBFH);
    close(TMPFH);
    my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks) = lstat($sJobFile);
    my $sJobFileMode = $mode & 0x00000fff;
    my $sJobFileUid = $uid;
    my $sJobFileGid = $gid;
    if (!chmod($sJobFileMode, $sTmpFile))
    {
      $$psError = "failed to set permissions for temporary job file: $!";
      unlink($sTmpFile);
    }
    if (!chown($sJobFileUid, $sJobFileGid, $sTmpFile))
    {
      $$psError = "failed to set owner/group for temporary job file: $!";
      unlink($sTmpFile);
    }
    if (!rename($sTmpFile, $sNewFile))
    {
      $$psError = "failed to rename temporary job file: $!";
      unlink($sTmpFile);
      return undef;
    }
#FIXME Decide how to handle the case where the original job file can't be unlinked.
    unlink($sJobFile);
  }
  else
  {
    if (!rename($sJobFile, $sNewFile))
    {
      $$psError = "failed to rename old job file: $!";
      return undef;
    }
  }

  return "pass";
}


######################################################################
#
# Usage
#
######################################################################

sub Usage
{
  my ($sProgram) = @_;
  print STDERR "\n";
  print STDERR "Usage: $sProgram [-d jqd-dir] [-e queue[,queue[...]]] [-G group-file] -i queue[,queue[...]] [-j {job-regex-file|-}] [-l log-file] [-t type] -s old-state-list:new-state [job-regex [job-regex ...]]\n";
  print STDERR "\n";
  exit(1);
}


=pod

=head1 NAME

webjob-jqd-change-state - Change the state of the specified queues or jobs.

=head1 SYNOPSIS

B<webjob-jqd-change-state> B<[-d jqd-dir]> B<[-e queue[,queue[...]]]> B<[-G group-file]> B<-i queue[,queue[...]]> B<[-j {job-regex-file|-}]> B<[-l log-file]> B<[-t type]> B<-s old-state-list:new-state> B<[job-regex [job-regex ...]]>

=head1 DESCRIPTION

This utility changes the state of the specified queues or jobs.

=head1 OPTIONS

=over 4

=item B<-d jqd-dir>

Specifies the base directory where queues live.  This directory must
exist.  The default value is /var/webjob/spool/jqd.

=item B<-e queue[,queue[...]]>

Specifies one or more queues or queue groups that are to be excluded
from the job assignment.  Note that group names must be prefixed with
'%'.  For example, a group called 'test' must be specified as '%test'.

=item B<-G group-file>

Specifies an alternate group file.  The default group file is
/var/webjob/config/jqd/groups.

=item B<-i queue[,queue[...]]>

Specifies one or more queues or queue groups that are to be included
in the job assignment.  Note that group names must be prefixed with
'%'.  For example, a group called 'test' must be specified as '%test'.

=item B<-j job-regex-file>

Specifies a file containing job regular expressions -- one per line.
These regular expressions are processed after those specified on the
command line.

=item B<-l log-file>

Specifies an alternate log file.  The default value is
/var/webjob/logfiles/jqd.log.

=item B<-s old-state-list:new-state>

Change specified queues or jobs in one of the old states to the new
state.  The list of old states must adhere to the following format:

    {any|state[,state[...]]}

Currently, the following queue states are supported: 'active',
'frozen', and 'locked'.

Currently, the following job states are supported: 'hold', 'todo',
'sent', 'open', 'done', 'pass', 'fail', and 'foul'.

When specifying the old state value, you may use the keyword 'any' as
an alias for all states.  The new state value must be a single, valid
state for the specified state type.  Note that some state transitions
may not be allowed.

=item B<-t type>

Specifies the type of state, 'queue' or 'job', to change.  The value
for this option is not case sensitive.  The default value is 'job'.

=back

=head1 AUTHOR

Klayton Monroe

=head1 SEE ALSO

webjob-jqd-create-job(1), webjob-jqd-change-priority(1), webjob-jqd-delete-job(1), webjob-jqd-list-jobs(1)

=head1 LICENSE

All documentation and code are distributed under same terms and
conditions as B<WebJob>.

=cut
