# # $Id: Command.pm,v 37.8 2011/05/27 16:18:20 anbrown Exp $ # # (c) 1999-2011 Morgan Stanley & Co. Incorporated # See ..../src/LICENSE for terms of distribution. # package MQSeries::Command; use 5.008; use strict; use Carp; use MQSeries qw(:functions); use MQSeries::QueueManager; use MQSeries::Queue; use MQSeries::Command::Request; use MQSeries::Command::Response; #use MQSeries::Command::PCF; #use MQSeries::Command::MQSC; use MQSeries::Utils qw(ConvertUnit); use Params::Validate qw(validate); our $VERSION = '1.33'; sub new { my $proto = shift; my $class = ref($proto) || $proto; my %args = validate(@_, { 'QueueManager' => 0, 'Type' => 0, 'CommandVersion' => 0, 'Carp' => 0, 'DynamicQName' => 0, 'Expiry' => 0, 'Wait' => 0, 'ModelQName' => 0, 'StrictMapping' => 0, 'CommandQueue' => 0, 'CommandQueueName' => 0, 'RealQueueManager' => 0, 'ProxyQueueManager' => 0, 'ReplyToQMgr' => 0, 'ReplyToQ' => 0, }); my $self = { Reason => 0, CompCode => 0, CommandVersion => MQSeries::MQCFH_VERSION_1, Wait => 60000, # 60 second wait for replies... #Expiry => 600, # 60 second expiry on requests Expiry => 999999999, Carp => \&carp, Type => 'PCF', ModelQName => 'SYSTEM.DEFAULT.MODEL.QUEUE', DynamicQName => 'PERL.COMMAND.*', StrictMapping => 0, Stats => {}, DefaultMsgDesc => {}, }; # Blessed later - see below # # A large set of optional parameters that become data members if # present. # foreach my $param (qw(Carp DynamicQName ModelQName StrictMapping CommandQueue CommandVersion CommandQueueName ReplyToQMgr)) { $self->{$param} = $args{$param} if (defined $args{$param}); } # # You can specify the type, but we'll default to PCF; this in turn # determines our class (if need be, we dynamically load the # subclass). # if (defined $args{Type}) { unless ($args{Type} eq 'PCF' or $args{Type} eq 'MQSC') { $self->Carp("Invalid argument: 'Type' must be one of: PCF MQSC"); return; } $self->{Type} = $args{Type}; } if ($class =~ /::(PCF|MQSC)$/) { if ($self->{Type} ne $1) { $self->Carp("Invalid argument: 'Type' $self->{Type} does not match $class"); return; } } else { $class .= "::" . $self->{Type}; eval { "use $class" } || do { $self->Carp("Could not load sub-class '$class': $@"); return; }; } bless ($self, $class); # # Where do we send requests? # # - CommandQueue (may set RealQueueManager for message display) # - CommandQueueName (may set RealQueueManager for message display) # - SYSTEM.COMMAND.INPUT / SYSTEM.ADMIN.COMMAND.QUEUE # if ($args{CommandQueue}) { # # This is subject to a number of requirements: # - CommandQueue is an MQSeries::Queue object # - QueueManager, CommandQueueName is omitted # - RealQueueManager is specified # - ProxyQueueManager (= ReplyToQMgr) is optional # unless (ref $args{CommandQueue} && $args{CommandQueue}->isa("MQSeries::Queue")) { $self->Carp("CommandQueue argument must be an MQSeries::Queue object"); return; } foreach my $fld (qw(RealQueueManager)) { next if (defined $args{$fld}); $self->Carp("CommandQueue: required argument $fld is missing"); return; } foreach my $fld (qw(QueueManager CommandQueueName)) { next unless (defined $args{$fld}); $self->Carp("CommandQueue: argument $fld is not allowed"); return; } $self->{'RealQueueManager'} = $args{'RealQueueManager'}; } elsif ($args{CommandQueueName}) { # # NOTE: This may be distribution-list notation # (queue@xmitqname), in which case the RealQueueManager # name may specify the target queue manager name for # display purposes. # $self->{CommandQueueName} = $args{CommandQueueName}; if (defined $args{'RealQueueManager'}) { $self->{'RealQueueManager'} = $args{'RealQueueManager'}; } } else { # # Some reasonable defaults. If we're proxying to a MQSC queue # manager, then this is (in the author's case anyway) probably # an MVS queue manager with no direct client access. # if ($self->{Type} eq 'MQSC') { $self->{CommandQueueName} = "SYSTEM.COMMAND.INPUT"; } else { $self->{CommandQueueName} = "SYSTEM.ADMIN.COMMAND.QUEUE"; } } # # But if you specify the proxy, things are a bit more complex. # # NOTE: This value can be a empty string, to indicate the # "default" queue manager. # if ($self->{CommandQueue}) { # # NOTE: Have already verified that RealQueueManager is # present. ProxyQueueManager is optional. # $self->{QueueManager} = $self->{CommandQueue}->QueueManager(); if ($args{ProxyQueueManager} && ! $args{ReplyToQMgr}) { $self->{ReplyToQMgr} = $args{ProxyQueueManager}; } } elsif (exists $args{ProxyQueueManager}) { if (ref $args{QueueManager} || $args{QueueManager} eq "") { $self->Carp("QueueManager must be a non-empty string when ProxyQueueManager is specified"); return; } $self->{ProxyQueueManager} = $args{ProxyQueueManager}; $self->{RealQueueManager} = $args{QueueManager}; $self->{ReplyToQMgr} = $args{ProxyQueueManager}; # # This assumes that a default command queue name, such as # SYSTEM.COMMAND.INPUT, will have a remote queue on the proxy # called SYSTEM.COMMAND.INPUT.. # unless (exists $args{CommandQueueName}) { $self->{CommandQueueName} .= ".$args{QueueManager}"; } if (ref $args{ProxyQueueManager}) { if ( $args{ProxyQueueManager}->isa("MQSeries::QueueManager") ) { $self->{QueueManager} = $args{ProxyQueueManager}; } else { $self->Carp("Invalid argument: 'ProxyQueueManager' " . "must be an MQSeries::QueueManager object"); return; } } else { $self->{QueueManager} = MQSeries::QueueManager::-> new(QueueManager => $args{ProxyQueueManager}, Carp => $self->{Carp}, ) or return; } } else { # No proxy specified: connect directly if (ref $args{QueueManager}) { if ($args{QueueManager}->isa("MQSeries::QueueManager")) { $self->{QueueManager} = $args{QueueManager}; } else { $self->Carp("Invalid argument: 'QueueManager' " . "must be an MQSeries::QueueManager object"); return; } } else { # Name is deprecated $self->{QueueManager} = MQSeries::QueueManager::-> new(QueueManager => $args{QueueManager}, Carp => $self->{Carp}, ) or return; } } # # The Wait and Expiry parameters can be tweaked, too, but go # through a 'ConvertUnit' step to support '60s', '2m' strings. # foreach my $parameter (qw(Expiry Wait)) { if (defined $args{$parameter}) { $self->{$parameter} = ConvertUnit($parameter, $args{$parameter}); } } # # CommandVersion may need to be bumped if we're talking to a MVS # qmgr and we stayed with the default of MQCFH_VERSION_1. We # should be able to determine this now that we're actually # connected to the qmgr. Note that if you're using a proxy qmgr, # this may still end up wrong, so the caller should really do this # for us. # if (!defined($args{"CommandVersion"}) && $self->{"QueueManager"}->{"QMgrConfig"}->{"Platform"} eq "MVS") { $self->{"CommandVersion"} = MQSeries::MQCFH_VERSION_3; } # # Open the command queue, and assume that the MQSeries::Queue # object will whine appropriately. # $self->{CommandQueue} ||= MQSeries::Queue::-> new(QueueManager => $self->{QueueManager}, Queue => $self->{CommandQueueName}, Mode => 'output', Carp => $self->{Carp}, Reason => \$self->{"Reason"}, CompCode => \$self->{"CompCode"}, ) || do { $self->Carp("Unable to instantiate MQSeries::Queue object for $self->{CommandQueueName}"); return; }; # # Open the ReplyToQ # if ($args{ReplyToQ}) { if (ref $args{ReplyToQ}) { if ($args{ReplyToQ}->isa("MQSeries::Queue")) { $self->{ReplyToQ} = $args{ReplyToQ}; } else { $self->Carp("Invalid argument: 'ReplyToQ' " . "must be an MQSeries::Queue object"); return; } } else { $self->{ReplyToQ} = MQSeries::Queue::-> new(QueueManager => $self->{QueueManager}, Queue => $args{ReplyToQ}, Mode => 'input', Carp => $self->{Carp}, Reason => \$self->{"Reason"}, CompCode => \$self->{"CompCode"}, ) || do { $self->Carp("Unable to instantiate MQSeries::Queue object for $args{ReplyToQ}"); return; }; } } else { $self->{ReplyToQ} = MQSeries::Queue::-> new(QueueManager => $self->{QueueManager}, Queue => $self->{ModelQName}, DynamicQName => $self->{DynamicQName}, Mode => 'input', Carp => $self->{Carp}, Reason => \$self->{"Reason"}, CompCode => \$self->{"CompCode"}, ) || do { $self->Carp("Unable to instantiate MQSeries::Queue object for $self->{ModelQName}"); return; }; } return $self; } # # FIXME: This uses the cached response object. We probably want # to keep track of this info another way, or get rid of # this method call altogether. # sub DataParameters { my $self = shift; my @parameters; foreach my $response ( @{$self->{Response}} ) { next if $response->Header('CompCode') == MQSeries::MQCC_FAILED; push(@parameters,$response->Parameters()); } return @parameters; } # # FIXME: This, too, probably will have to go. # sub ErrorParameters { my $self = shift; my @parameters; foreach my $response ( @{$self->{Response}} ) { next if $response->Header('CompCode') != MQSeries::MQCC_FAILED; push(@parameters,$response->Parameters()); } return @parameters; } sub CompCode { my ($self) = @_; return $self->{"CompCode"}; } sub Reason { my ($self) = @_; return $self->{"Reason"}; } sub ReasonText { my ($self) = @_; if (defined $self->{'ReasonText'}) { # Cached from MF response return $self->{'ReasonText'}; } return MQReasonToText($self->{"Reason"}); } # # Set default message-descriptor options # sub MsgDesc { my ($this, %dft) = @_; while (my ($k, $v) = each %dft) { $this->{DefaultMsgDesc}->{$k} = $v; } return $this->{DefaultMsgDesc}; } # # Don't autoload this.... # sub DESTROY { 1 } # # This AUTOLOAD will allow any random method to be interpreted as a # command request. If the command isn't defined, then _Command # will blow up. # sub AUTOLOAD { our $AUTOLOAD; my ($self) = shift @_; my $name = $AUTOLOAD; $name =~ s/.*://; return $self->_Command($name,{@_}); } # # This method will query the object, and if it exists, check the # attributes to see if they match, and if they do, do nothing. That # is, its a conditional creation of the given object. # # Hash with named parameters: # - Verify (optional boolean): run for real (false, dft) / verify only (true) # - Quiet: (optional boolean): display activity (true) / silent (false, dft) # - Clear: (optional, boolean): clear queue if recreated # - Attrs: Ref to hash with attributes of object to be created # - Force: (optional, boolean): use force option when recreating queue # - Callback (optional): code ref used to coprae attributes # Returns: # - Boolean: false if create/verify failed, true if create/verify succeeded # If true: -1 if no differences/changes, 1 if differences/changes made # sub CreateObject { my ($self, %args) = @_; my ($Verify, $Quiet, $Clear, $Attrs, $Force, $Callback) = @args{qw(Verify Quiet Clear Attrs Force Callback)}; $Callback ||= \&_CompareAttributes; unless (ref $Callback eq 'CODE') { $self->Carp("'Callback' parameter must be a subroutine reference"); return; } # # Queue Manager name used for reporting (could need to extract # from default queue manager after connecting). # my $QMgr = ($self->{RealQueueManager} || $self->{QueueManager}->{QueueManager} ); my $Need = 1; my $Inquire = ""; my $Create = ""; my $Delete = ""; my $Change = ""; my $method = ""; my $Key = ""; my $Type = ""; my @KeyNames = qw(ChannelName NamelistName ProcessName QName StorageClassName AuthInfoName CFStructName CFStrucName ListenerName ServiceName SubName TopicName); my $KeyCount = 0; # # Verify that we have only been given exactly *one* of the primary # keys that let us determine the object type. # foreach my $KeyName ( @KeyNames ) { $KeyCount++ if exists $Attrs->{$KeyName}; } # # ProcessName is a valid attribute for Queues, so allow this: # $KeyCount-- if exists $Attrs->{QName} && exists $Attrs->{ProcessName}; if ( $KeyCount != 1 ) { $self->Carp("CreateObject: Unable to determine object type.\n" . ( $KeyCount == 0 ? "One of the following must be specified:\n" : ( "More than one of the following was specified:\n" . "(Exception: ProcessName and QName can both be present, since\n" . "the former is an attribute of the latter.\n" . "We assume ObjectType == Queue in this case)\n" ) ) . "\t" . join("\n\t",@KeyNames) . "\n"); return; } if ( $Attrs->{ChannelName} ) { $Inquire = "InquireChannel"; $Create = "CreateChannel"; $Change = "ChangeChannel"; $Key = "ChannelName"; $Type = "$Attrs->{ChannelType} Channel"; } elsif ( $Attrs->{NamelistName} ) { $Inquire = "InquireNamelist"; $Create = "CreateNamelist"; $Change = "ChangeNamelist"; $Key = "NamelistName"; $Type = "Namelist"; } elsif ( $Attrs->{QName} ) { $Inquire = "InquireQueue"; $Create = "CreateQueue"; $Change = "ChangeQueue"; $Delete = "DeleteQueue"; $Key = "QName"; if ( $Attrs->{QType} eq 'Remote' && $Attrs->{RemoteQName} eq '' ) { $Type = "QMgr Alias"; } elsif ( $Attrs->{QType} eq 'Local' && defined($Attrs->{Usage}) && $Attrs->{Usage} eq 'XMITQ' ) { $Type = "Transmission Queue"; } else { $Type = "$Attrs->{QType} Queue"; } } elsif ( $Attrs->{ProcessName} ) { $Inquire = "InquireProcess"; $Create = "CreateProcess"; $Change = "ChangeProcess"; $Key = "ProcessName"; $Type = "Process"; } elsif ( $Attrs->{StorageClassName} ) { $Inquire = "InquireStorageClass"; $Create = "CreateStorageClass"; $Change = "ChangeStorageClass"; $Key = "StorageClassName"; $Type = "StorageClass"; } elsif ( $Attrs->{AuthInfoName} ) { $Inquire = "InquireAuthInfo"; $Create = "CreateAuthInfo"; $Change = "ChangeAuthInfo"; $Key = "AuthInfoName"; $Type = "AuthInfo"; } elsif ( $Attrs->{CFStructName} ) { # Kept for backward compatibility $Inquire = "InquireCFStruct"; $Create = "CreateCFStruct"; $Change = "ChangeCFStruct"; $Key = "CFStructName"; $Type = "CFStruct"; } elsif ( $Attrs->{CFStrucName} ) { $Inquire = "InquireCFStruc"; $Create = "CreateCFStruc"; $Change = "ChangeCFStruc"; $Key = "CFStrucName"; $Type = "CFStruc"; } elsif ( $Attrs->{ServiceName} ) { $Inquire = "InquireService"; $Create = "CreateService"; $Change = "ChangeService"; $Key = "ServiceName"; $Type = "Service"; } elsif ( $Attrs->{ListenerName} ) { $Inquire = "InquireChannelListener"; $Create = "CreateChannelListener"; $Change = "ChangeChannelListener"; $Key = "ListenerName"; $Type = "Listener"; } elsif ( $Attrs->{SubName} ) { $Inquire = "InquireSubscription"; $Create = "CreateSubscription"; $Change = "ChangeSubscription"; $Key = "SubName"; $Type = "Subscription"; } elsif ( $Attrs->{TopicName} ) { $Inquire = "InquireTopic"; $Create = "CreateTopic"; $Change = "ChangeTopic"; $Key = "TopicName"; $Type = "Topic"; } # # First check to see if it exists. # my ($Object) = $self->$Inquire( $Key => $Attrs->{$Key} ); # # XXX -- shouldn't we be checking for no such object specifically? # Of course we should... # if ( $self->Reason() && $self->Reason() != MQSeries::MQRC_UNKNOWN_OBJECT_NAME && $self->Reason() != MQSeries::MQRCCF_CHANNEL_NOT_FOUND && $self->Reason() != MQSeries::MQRC_NO_SUBSCRIPTION ) { my $rc = $self->Reason(); $self->Carp("Unable to verify existence of $Type '$QMgr/$Attrs->{$Key}' (Reason=$rc)\n"); return; } my $Changes; if ( ref $Object eq 'HASH' ) { $method = $Change; # # If an object has been created with QSGDisposition # 'Group', then a normal 'Inquire' command returns QSPDisp # 'Copy' - which, if the queue manager is up, represents the # object unless it is just being changed on another queue # manager. # # We'll amend that to read QSGDisp 'Group', as that is what # we need to compare against. # if (defined $Object->{'QSGDisposition'} && $Object->{'QSGDisposition'} eq 'Copy') { $Object->{'QSGDisposition'} = 'Group'; } # # If it exists, let's assume we don't need to create it. If # any of the attributes are wrong, we'll then say we "Need" # it. # $Changes = $Callback->($Attrs, $Object, \&_CompareOneAttribute); $Need = scalar(keys %$Changes); unless ($Quiet) { foreach my $Attr (sort keys %$Changes) { print("Incorrect attribute '$Attr' for $Type '$QMgr/$Attrs->{$Key}'\n"); if (ref $Attrs->{$Attr} eq "ARRAY") { print "Should be:\n\t" . join("\n\t",map { qq{'$_'} } @{$Changes->{$Attr}}) . "\n"; } else { print "Should be: '$Changes->{$Attr}'\n"; } if (ref $Object->{$Attr} eq "ARRAY") { print "Currently is:\n\t" . join("\n\t",map { qq{'$_'} } @{$Object->{$Attr}} ) . "\n"; } else { print "Currently is: '$Object->{$Attr}'\n"; } } } } else { print "$Type '$QMgr/$Attrs->{$Key}' is missing\n" unless $Quiet; $method = $Create; $Changes = $Callback->($Attrs); } unless ($Need) { print "$Type '$QMgr/$Attrs->{$Key}' is correctly configured\n" unless $Quiet; return -1; # -1: No changes } return 1 if $Verify; # 1: Things changed # # If we have any changes, make sure we include the QName/QType, # ChannelName/ChannelType, ProcessName, StorageClass, # AuthInfoName or CFStructName. # # We must do this here, as user's callbacks will get it wrong... # if (keys %$Changes) { $Changes->{$Key} = $Attrs->{$Key}; if ($Key eq 'QName') { $Changes->{'QType'} = $Attrs->{'QType'}; } elsif ($Key eq 'ChannelName') { $Changes->{'ChannelType'} = $Attrs->{'ChannelType'}; } elsif ($Key eq 'AuthInfoName' && defined($self->{QueueManager}->{QMgrConfig}) && $self->{QueueManager}->{QMgrConfig}->{CommandLevel} >= 700) { $Changes->{'AuthInfoType'} = $Attrs->{'AuthInfoType'}; } } # # If a Queue QType has changed, or any object CFStructure or # QSGDisposition has changed, we'll have to delete the # old object first. # # This looks complicated because you don't want to alter a dynamic # queue that should be recreated as a real local queue (you want # to delete it first) or fail to delete a permdyn queue that # should be a tempdyn queue (but don't delete model queues where # this is true), etc. # # NOTE: We do *not* purge, unless the 'Clear' flag is specified. # That should be checked out manually, as it will be an odd case # anyway. In any event, this will be somewhat rare. # my $delete_first = 0; if ($Key eq 'QName' && $Object && ($Attrs->{QType} ne $Object->{QType} || (defined $Attrs->{DefinitionType} && defined $Object->{DefinitionType} && $Attrs->{QType} ne 'Model' && $Attrs->{DefinitionType} ne $Object->{DefinitionType} ) ) ) { $delete_first = 1; } elsif ($Object) { foreach my $fld (qw(QSGDisposition CFStructure)) { if (defined $Attrs->{$fld} && $Attrs->{$fld} ne $Object->{$fld}) { $delete_first = 1; } } } if ($delete_first) { print "Deleting $Type $QMgr/$Attrs->{$Key}'\n" unless $Quiet; # # CommandScope break in certain combinations # Since MQ uses the CommandScope underneath the covers, # CommandScope is no longer used # $self->$Delete ( $Key => $Attrs->{$Key}, QType => $Object->{QType}, ( $Clear && $Key eq 'QName' && $Object->{QType} eq 'Local' ? ( Purge => 1 ) : (), (defined $Object->{QSGDisposition} ? (QSGDisposition => $Object->{QSGDisposition}) : ()), ) ) || do { $self->Carp("Unable to delete $Object->{QType} Queue '$QMgr/$Attrs->{$Key}'\n" . $self->ReasonText() . "\n"); return; }; $method = $Create; $Changes = $Callback->($Attrs); $Object = undef; } unless ( $Quiet ) { print( ($method eq $Change ? "Updating" : "Creating") . " $Type '$QMgr/$Attrs->{$Key}'\n"); } # # Force can only be specified when changing queue objects, but not # when creating them. Therefore, this is passed as an argument to # CreateObject, and not as an "attribute". Note that Force and # Replace aren't really object attributes anyway; they are options # to the command server, and thus our treatement of them is really # a better design. # # Take that, IBM... ;-) # if ( $Force && $Key eq 'QName' && $Changes->{'QType'} ne 'Model' && $method eq $Change ) { $Changes->{Force} = 1; } # # If the QSGDisposition field is set in the object, # also set it in the changes. # my $disp_field = 'QSGDisposition'; if (defined $Object->{$disp_field}) { $Changes->{$disp_field} = $Object->{$disp_field}; } # # Here's a bit of an ugly hack, but it tends to be required... # you cannot change a queue StorageClass (MF attribute) if the # queue has messages or is in use. Attempting to do so results in # a partially-updated object. So if the StorageClass needs to be # changed, and the queue seems open/with messages, perform a # two-step update. # my $Changes2; if ($method ne $Create && $Key eq 'QName' && defined $Changes->{'StorageClass'} && scalar(keys %$Changes) > 2) { $Changes2->{'QType'} = $Changes->{'QType'}; $Changes2->{$disp_field} = $Changes->{$disp_field} if (defined $Changes->{$disp_field}); $Changes2->{'StorageClass'} = delete $Changes->{'StorageClass'}; } foreach my $diffs ($Changes, $Changes2) { next unless (defined $diffs); # $Changes2 is optional $self->$method($Key => $Changes->{$Key}, %$diffs, ) || do { $self->Carp("Unable to " . ( $method eq $Change ? "update" : "create" ) . " $Type '$QMgr/$Changes->{$Key}'\n" . $self->ReasonText() . "\n"); return; }; } return 1; # 1: Things changed } # # This method codes the basic request/reply logic common to both PCF # and MQSC command formats. # sub _Command { my $self = shift; my $command = shift; my $parameters = shift; my $key = ""; # # IMPORTANT: each and every case where we return *must* set a # reasonable value for the Reason and CompCode. # $self->{"Reason"} = 0; $self->{"CompCode"} = 0; # # We set the ReasonText if we get a MQSC response message with an # error. In other cases, we'll use MQReasonToText on the Reason # field. # delete $self->{"ReasonText"}; # # Allow the 'name' keys to default to '*' if not given. # my %command2name = ( InquireNamelist => 'NamelistName', InquireNamelistNames => 'NamelistName', InquireProcess => 'ProcessName', InquireProcessNames => 'ProcessName', InquireQueue => 'QName', InquireQueueNames => 'QName', InquireQueueStatus => 'QName', ResetQueueStatistics => 'QName', InquireChannel => 'ChannelName', InquireChannelNames => 'ChannelName', InquireChannelStatus => 'ChannelName', InquireStorageClass => 'StorageClassName', InquireStorageClassNames => 'StorageClassName', InquireService => 'ServiceName', InquireAuthInfo => 'AuthInfoName', InquireAuthInfoNames => 'AuthInfoName', InquireCFStruc => 'CFStrucName', InquireCFStrucNames => 'CFStrucName', InquireCFStrucStatus => 'CFStrucName', InquireCFStruct => 'CFStructName', InquireCFStructNames => 'CFStructName', InquireThread => 'ThreadName', InquireChannelListener => 'ListenerName', InquireSubscription => 'SubName', InquireSubscriptionStatus => 'SubName', InquireTopic => 'TopicName', InquireTopicNames => 'TopicName', InquireTopicStatus => 'TopicString', ); if ( $command2name{$command} ) { unless ($parameters->{$command2name{$command}} ) { $parameters->{$command2name{$command}} = '*'; # # For InquireTopicStatus, use '#', not '*' # if ($command eq 'InquireTopicStatus') { $parameters->{$command2name{$command}} = '#'; } } } # # If you are (re)creating a non-dynamic local queue, you should # not specify DefinitionType=Predefined. It was needed above in # order to see if we need to pre-delete a dynamic local queue, but # it should go no further. # if ($command eq "CreateQueue" && $parameters->{"DefinitionType"} && $parameters->{"DefinitionType"} eq "Predefined") { delete($parameters->{"DefinitionType"}); } # # Allow 'GenericConnectionId' of InquireConnection to default to '00' # if ( $command eq 'InquireConnection' ) { unless ( $parameters->{'GenericConnectionId'} || $parameters->{'ConnectionId'} ) { $parameters->{'GenericConnectionId'} = '00'; } } # # Allow the 'attr' keys to default to 'All' if not given. # # NOTE: v5 PCF does this for you, but V2 PCF and MQSC do NOT. # Remember -- we're after a single interface to all these formats. # my %command2all = ( InquireChannel => 'ChannelAttrs', InquireChannelStatus => 'ChannelInstanceAttrs', InquireClusterQueueManager => 'ClusterQMgrAttrs', InquireConnection => 'ConnectionAttrs', InquireNamelist => 'NamelistAttrs', InquireProcess => 'ProcessAttrs', InquireQueue => 'QAttrs', InquireQueueManager => 'QMgrAttrs', InquireQueueStatus => 'QStatusAttrs', InquireStorageClass => 'StorageClassAttrs', InquireAuthInfo => 'AuthInfoAttrs', InquireCFStruct => 'CFStructAttrs', InquireCFStruc => 'CFStrucAttrs', InquireService => 'ServiceAttrs', InquireSubscription => 'SubscriptionAttrs', InquireSubscriptionStatus => 'SubscriptionStatusAttrs', InquireTopic => 'TopicAttrs', InquireTopicStatus => 'TopicStatusAttrs', ); if ( $command2all{$command} ) { unless ( $parameters->{$command2all{$command}} ) { $parameters->{$command2all{$command}} = ['All']; } } # # The request message descriptor is computed from the # defaults set in DefaultMsgDesc, plus some hard-wired # constants. # # FIXME: Maybe copy ReplyToQ, ReplyToQMgr and Expiry # to the default message descriptor and use that? # my $req_msgdesc = { %{ $self->{DefaultMsgDesc} }, ReplyToQ => $self->{ReplyToQ}->ObjDesc("ObjectName"), ReplyToQMgr => $self->{ReplyToQMgr}, #ReplyToQMgr => (defined $self->{ReplyToQMgr} ? $self->{ReplyToQMgr} : ''), Expiry => $self->{Expiry}, }; my $putmsg_options = { Options => MQSeries::MQPMO_FAIL_IF_QUIESCING, }; # # If identity context / origin context is overridden, make sure # the proper put-message options have been specified. # # Obviously, this is ignored unless the command-queue has been # opened with MQOO_SET_IDENTITY_CONTEXT or MQOO_SET_ALL_CONTEXT. # if (defined $req_msgdesc->{ApplIdentityData} || defined $req_msgdesc->{UserIdentifier}) { #print STDERR "XXX: Adding set-identity context to PMO\n"; $putmsg_options->{Options} |= MQSeries::MQPMO_SET_IDENTITY_CONTEXT; } if (defined $req_msgdesc->{ApplOriginData}) { #print STDERR "XXX: Adding set-all context to PMO\n"; $putmsg_options->{Options} |= MQSeries::MQPMO_SET_ALL_CONTEXT; } $self->{Request} = MQSeries::Command::Request::-> new(MsgDesc => $req_msgdesc, Type => $self->{Type}, Command => $command, Parameters => $parameters, Carp => $self->{Carp}, StrictMapping => $self->{StrictMapping}, CommandVersion => $self->{CommandVersion}, ) || do { $self->{"CompCode"} = MQSeries::MQCC_FAILED; $self->{"Reason"} = MQSeries::MQRC_UNEXPECTED_ERROR; return; }; my $putresult = $self->{CommandQueue}-> Put(Message => $self->{Request}, PutMsgOpts => $putmsg_options, Sync => 0, ); # # Keep track of stats: no of requests, total bytes, largest # { $self->{'Stats'}->{'NoRequests'}++; my $put_size = !defined($self->{'Request'}->Buffer()) ? 0 : length($self->{'Request'}->Buffer()); $self->{'Stats'}->{'RequestBytes'} += $put_size; $self->{'Stats'}->{'MaxRequest'} = $put_size if (! defined $self->{'Stats'}->{'MaxRequest'} || $put_size > $self->{'Stats'}->{'MaxRequest'}); } unless ($putresult) { $self->{"CompCode"} = $self->{CommandQueue}->CompCode(); $self->{"Reason"} = $self->{CommandQueue}->Reason(); return; } $self->{Response} = []; my $MQSCHeader = { Command => $command }; my @response_buffers; while ( 1 ) { my $response = MQSeries::Command::Response::-> new(MsgDesc => { CorrelId => $self->{Request}->MsgDesc("MsgId"), }, Type => $self->{Type}, Header => $self->{Type} eq 'PCF' ? "" : {%$MQSCHeader}, StrictMapping => $self->{StrictMapping}, ) || do { $self->{"CompCode"} = MQSeries::MQCC_FAILED; $self->{"Reason"} = MQSeries::MQRC_UNEXPECTED_ERROR; return; }; my $getresult = $self->{ReplyToQ}-> Get(Message => $response, Wait => $self->{Wait}, ); # # Stats again: keep track of no replies, total bytes, largest # { $self->{'Stats'}->{'NoResponses'}++; my $buffer = $response->Buffer(); my $get_size = defined($buffer) ? length($buffer) : 0; $self->{'Stats'}->{'ResponseBytes'} += $get_size; $self->{'Stats'}->{'MaxResponse'} = $get_size if (! defined $self->{'Stats'}->{'MaxResponse'} || $get_size > $self->{'Stats'}->{'MaxResponse'}); } unless ($getresult) { $self->{"CompCode"} = $self->{ReplyToQ}->CompCode(); $self->{"Reason"} = $self->{ReplyToQ}->Reason(); $self->Carp("MQGET from ReplyQ failed.\n" . "Reason => " . MQReasonToText($self->{"Reason"}) . "\n"); return; } # # Keep going until there are no more messages. This is # essential for MQSC, but PCF tells us when the last response # is in. # last if $self->{ReplyToQ}->Reason() == MQSeries::MQRC_NO_MSG_AVAILABLE; #print STDERR "XXX: Response buffer [$response->{'Buffer'}]\n" # if ($self->{Type} eq 'MQSC'); # # Ugly hack: # - If this is MQSC # - We are asking for an InquireChannelStatus/DeleteChannel, # which gives us a count message, then a 'command accepted' # message, then the real data that we care about # - This is that 'command accepted' message # Then bin the message. # push @response_buffers, $response->{'Buffer'}; if ($self->{Type} eq 'MQSC' && ($command eq 'InquireChannelStatus' || $command eq 'DeleteChannel') && scalar(@{$self->{Response}}) <= 2 && "@response_buffers" =~ m!\bCSQM[A-Z][A-Z][A-Z][A-Z]\b.*\bACCEPTED\s*$!s) { # # Reset the responses so far. We must reset the 'reusable' # header as well, as the next response may have a different # count, return or reason code. # $self->{Response} = []; @response_buffers = (); $MQSCHeader = { Command => $command }; next; } # # FIXME: We probably don't want to keep the response # objects around. # push(@{$self->{Response}}, $response); # Have to "reuse" the header.... blegh. $MQSCHeader = $response->Header() if $self->{Type} eq 'MQSC'; last if $self->_LastSeen(); } # # OK, now what do we feed back? # # If we didn't see the last message, then return the empty list. # unless ( $self->_LastSeen() ) { $self->{"CompCode"} = MQSeries::MQCC_FAILED if $self->{"CompCode"} == MQSeries::MQCC_OK; $self->{"Reason"} = MQSeries::MQRC_UNEXPECTED_ERROR if $self->{"Reason"} == MQSeries::MQRC_NONE; $self->Carp("Last response message never seen\n"); return; } # # Massage the responses. This varies wildly between PCF and MQSC # $self->_ProcessResponses($command) || return; # # Handle the InquireFooNames commands -- they're very easy. # if ($command eq "InquireChannelNames") { # # Elide everything but the last response -- we may have gotten # individual channel names back for client conns here (as if # we had done InquireChannelName) and we don't want them. # @{$self->{Response}} = $self->{Response}->[-1]; } if ( $command =~ /^Inquire(\w+?Names)$/ ) { $key = $1; $key = 'QNames' if $key eq 'QueueNames'; # beware -- the command may have worked, but there may be no # names, in which case, $names will be undef. my $names = $self->{Response}->[0]->Parameters($key); if ( ref $names eq 'ARRAY' ) { return @$names; } else { return; } } # # Next handle anything which returns a list of parameters # elsif ( $command =~ /^(Inquire|ResetQueue|Escape)/ ) { my @parameters = $self->DataParameters(); if ( wantarray ) { return @parameters; } else { return $parameters[0]; } } # # Handle the "partial completion" reason codes returned by MQSC # for some commands. This just means the command is asynchronous, # and will be completed "later". Eg: StopChannel will stop the # channel, but you really need to poll the status with # InquireChannelStatus to figure out *when* it has stopped. Yeah, # that sucks... # elsif ( $command =~ /^(Start|Stop)Channel$/ ) { if ( $self->{"CompCode"} || ( $self->{"Reason"} != 0 && $self->{"Reason"} != 4 ) ) { $self->Carp(qq/Command '$command' failed (Reason = $self->{"Reason"})/); return; } else { return 1; } } # # Finally the simple worked or failed commands # else { if ($self->{"CompCode"} == 0 && $self->{"Reason"} == 4 && $self->{"Buffers"}[-1] =~ /CSQN13[78]I .* command (?:accepted|generated)/) { # # Deal with CommandScope for QSGs. If CompCode=0 and Reason=4, # and the last response is one of: # - 'CSQN137I ... command accepted', # - 'CSQN138I ... command generated', # this is not an error. # return 1; } elsif ( $self->{"CompCode"} || $self->{"Reason"} ) { # # Save the MQSC ReasonText. Leave undef (for translation # of normal reason code ->text) in other cases. # if (defined $MQSCHeader && defined $MQSCHeader->{'ReasonText'}) { $self->{'Reason'} = 8; # Often corrupted, so sanitize $self->{'ReasonText'} = "@{ $MQSCHeader->{'ReasonText'} }"; } $self->Carp(qq/Command '$command' failed (Reason = $self->{"Reason"})/); return; } else { return 1; } } } # # This routine is the default call-back invoked by the CreateObject # method. It compares the requested attributes with the attributes # found, then returns a list of attributes that need to be changed. # # Parameters: # - Ref to hash with requested attributes, as passed to CreateObject # - Ref to hash with attributes found (undef if object doesn't exist) # - Ref to built-in "compare one attribute" function # Returns: # - Ref to hash with requested changes # sub _CompareAttributes { my ($request, $found, $cmp_sub) = @_; my $retval = {}; foreach my $attr (sort keys %$request) { # # Don't bother comparing Attrs passed in which don't get # returned by the Inquire commands, eg. Replace, Force and # others that make no sense. # next if (defined $found && !exists $found->{$attr}); my $NeedAttr = (defined $found ? $cmp_sub->($attr, $request->{$attr}, $found->{$attr}) : 1); if ($NeedAttr) { $retval->{$attr} = $request->{$attr}; } } # End foreach: attribute return $retval; } # # Helper routine: compare one attribute, handling the scalar/array # cases as well as the whitespace issues. # # Parameters: # - Attribute name # - Requested value # - Object value / undef # Returns: # - 0: unchanged, 1: changed # sub _CompareOneAttribute { my ($name, $request, $found) = @_; my $diff = 0; # # The HeaderCompression and MessageCompression attributes can # return an array padded with '-1' (not available) entries. If # so, we remove such entries (but we keep at least one). # if (($name eq 'HeaderCompression' || $name eq 'MessageCompression') && ref($found) eq 'ARRAY' && grep { $_ == -1 } @$found) { $found = [ grep { $_ != -1 } @$found ]; if (@$found == 0) { # No elements set $found = -1; } elsif (@$found == 1) { $found = $found->[0]; # Turn into scalar } } # # One special case -- we don't need this attribute is they are # both empty and/or white space. Bear in mind that you have # to feed a single space to some of these damn commands. Very # annoying. # # Well, actually, more than one special case. If the # attribute is a list, then it will be represented as an ARRAY # reference. This does complicate things... # # First, check to see if we've been fed an array with only one # element. If so, flatten it. This greatly simplifies the # comparison, since the query will not return an ARRAY if # there is only one element of any given attribute. # if (ref $request eq "ARRAY" && scalar @$request == 1) { $request = $request->[0]; } if (ref $request ne "ARRAY") { if (ref $found eq "ARRAY" ) { $diff = 1; } elsif ($request !~ /^\s*$/ || $found !~ /^\s*$/ ) { if ($request =~ /^\d+\s*$/ ) { # Assume both numeric if ($request != $found) { $diff = 1; } } else { # Text if ($request ne $found) { $diff = 1; } } } else { # Both blank, ignore } } else { # Array case if (ref $found ne "ARRAY") { $diff = 1; } elsif (scalar(@$request) != scalar(@$found)) { $diff = 1; } else { # Both arrays, same size - compare elements for (my $index = 0; $index < scalar(@$request); $index++ ) { next if ($request->[$index] =~ /^\s*$/ && $found->[$index] =~ /^\s*$/); if ($request->[$index] =~ /^\d+/ ) { if ($request->[$index] != $found->[$index] ) { $diff = 1; } } else { # Text if ($request->[$index] ne $found->[$index] ) { $diff = 1; } } } # End foreach: element } # End else: Arrays same size } # End if: scalar/array return $diff; } # # FIXME: We want to get rid of this (and of all the response message # buffering, unless a 'KeepResponses' options is given). For now, # we'll alert whenever this method is being used, as a prelude to # fixing this. # sub Responses { my $self = shift; $self->Carp("MQSeries::Command - Responses() being invoked"); if ( ref $self->{"Response"} eq "ARRAY" ) { return @{$self->{"Response"}}; } else { return; } } # # Internal helper routine: Issue a warning message using the # 'Carp' reference # sub Carp { my ($self, $message) = @_; $self->{'Carp'}->($message); return $self; } # # Return the usage statistics # sub GetStatistics { my ($self) = @_; return { %{ $self->{'Stats'} } }; } # # Wipe the statistics so far # sub ResetStatistics { my ($self) = @_; $self->{'Stats'} = {}; return $self; } 1; __END__ =head1 NAME MQSeries::Command - OO interface to the Programmable Commands =head1 SYNOPSIS use MQSeries; use MQSeries::Command; # # Simplest usage # my $qmgr_obj = MQSeries::QueueManager-> new(QueueManager => 'some.queue.manager'); my $command = MQSeries::Command->new(QueueManager => $qmgr_obj); or die("Unable to instantiate command object\n"); # # To use MQ v6 features (PCF for z/OS, Filter commands, # InquireConnection) # my $command = MQSeries::Command->new(QueueManager => $qmgr_obj, CommandVersion => MQSeries::MQCFH_VERSION_3); or die("Unable to instantiate command object\n"); # # Inquire XXX Names returns object names, # all other Inquire XX commands return hash references # @qnames = $command->InquireQueueNames() or die "Unable to list queue names\n"; foreach my $qname ( @qnames ) { $attr = $command->InquireQueue ( QName => $qname, QAttrs => [qw( OpenInputCount OpenOutputCount CurrentQDepth )], ) or die "InquireQueue: " . MQReasonToText($command->Reason()) . "\n"; print "QName = $qname\n"; foreach my $key ( sort keys %$attr ) { print "\t$key => $attr->{$key}\n"; } } # # Filter commands (requires MQ v6 and CommandVersion 3) # Like other Inquire XXX commands (excpet Inquire XXX Names), # this returns a list of hash references, not obejct names. # my @full_queues = $command-> InquireQueueStatus('FilterCommand' => "CurrentQDepth > 10000"); my @ip_conns = $command-> InquireConnection('FilterCommand' => "ConnectionName like '144.44.*'"); # # High-level wrapper method: CreateObject # $command->CreateObject ( Attrs => { QName => 'FOO.BAR.REQUEST', QType => 'Local', MaxQDepth => '50000', MaxMsgLength => '20000', } ) || die "CreateObject: " . MQReasonToText($command->Reason()) . "\n"; $command->CreateObject ( Clear => 1, Attrs => { QName => 'FOO.BAR.REPLY', QType => 'Remote', RemoteQName => 'FOO.BAR.REPLY', RemoteQMgrName => 'SAT1', } ) || die "CreateObject: " . MQReasonToText($command->Reason()) . "\n"; =head1 DESCRIPTION The MQSeries::Command class implements an interface to the Programmable Command Format messages documented in the: "WebSphere MQ Programmable Command Formats and Administration Interface" section of the MQSeries documentation. In particular, this document will primarily explain how to interpret the above documentation, and thus use this particular implementation in perl. Please read and understand the following sections of the above document: Programmable Command Formats Chapter . Definitions of the Programmable Command Formats Chapter . Structures used for commands and responses This interface also supports the text-based MQSC format messages used by the queue manager of some platforms, particularly MVS. Using the same interface, either PCF or MQSC command server can be queried, with the results translated into the same format for responses as well. Note that there are limits to how transparent this is (see MQSC NOTES), but the code tries quite hard to hide as many of the differences as possible. =head2 COMMAND ARGUMENTS Before we discuss the specific arguments and return values of each of the methods supported by the MQSeries::Command module, we must explain how the keys and values used by the interface were chosen, as this will allow the developer to understand how to take advantage of the very complete documentation provided by IBM (which will B be reproduced here). For each command documented in 'Definitions of the Programmable Command Formats' (there is a specific page listing all of the commands, grouped by type), there is a corresponding method in this class. For example, there is a method named 'InquireQueueManager', for the obvious (I hope) PCF command. All of these methods take a hash of key/value pairs as an argument, with the keys being those defined in the documentation for each command. When writing C code to produce PCF messages, the parameter names are macros, such as: MQIACF_Q_MGR_ATTRS to specify a list of queue manager attributes. Rather than use these names directly, the key strings are taken from the IBM documentation. In this example, the string used for this key is: QMgrAttrs The values depend on the structure type of the parameter. If the structure is a string (MQCFST), byte string (MQBACF) or an integer (MQCFIN) then the value of the key is simply a scalar string or integer in perl. If it either a string list (MQCFSL) or an integer list (MQCFIL), then the value of the key is an array reference (see the InquireQueueManager example in the SYNOPSIS) of scalar strings or integers. =head2 RETURN VALUES Most of the individual methods map to underlying commands which do not return any data. For all of these, the return value is simply Boolean; true or false. That is, the command either worked or failed. Only the methods associated with those commands documented as producing data responses: Escape Inquire AuthInfo Inquire AuthInfo Names Inquire Authority Records Inquire Authority Service Inquire Channel Inquire Channel Initiator Inquire Channel Listener Inquire Channel Listener Status Inquire Channel Names Inquire Channel Status Inquire Cluster Queue Manager Inquire Connection Inquire Entiry Authority Inquire Group Inquire Log Inquire Namelist Inquire Namelist Names Inquire Process Inquire Process Names Inquire Queue Inquire Queue Manager Inquire Queue Manager Status Inquire Queue Names Inquire Queue Status Inquire Service Inquire Service Status Inquire Subscription Inquire Subscription Status Inquire System Inquire Topic Inquire Topic Status Reset Queue Statistics plus the following equivalents for MQSC Inquire CFStruc Inquire CFStruc Status Inquire Usage Inquire Security Inquire StorageClass return interesting information. Most of these will return an array of hash references, one for each object matching the query criteria. For example. The specific keys are documented in the section of the IBM documentation which discusses the "Data responses to commands", on the summary page "PCF commands and responses in groups". If you have read the IBM documentation as requested above, you should have found this page. Note that in an array context, the entire list is returned, but in a scalar context, only the first item in the list is returned. Some of these commands, however, have a simplified return value. All of the below: Inquire AuthInfo Names Inquire CFStruct Names Inquire Channel Names Inquire Namelist Names Inquire Process Names Inquire Queue Names Inquire StorageClass Names Inquire Topic Names simply return an array of strings, containing the names which matched the query criteria. =head1 METHODS =head2 new The arguments to the constructor are a hash, with the following key/value pairs: Key Value === ===== QueueManager String or MQSeries::QueueManager object ProxyQueueManager String or MQSeries::QueueManager object RealQueueManager String ReplyToQ String or MQSeries::Queue object CommandQueueName String CommandQueue MQSeries::Queue object CommandVersion Numeric (1 or 3) DynamicQName String ModelQName String Type String ("PCF" or "MQSC") Expiry Numeric Wait Numeric ReplyToQMgr String Carp CODE Reference StrictMapping Boolean =over 4 =item QueueManager This argument specifies the queue manager to which commands are to be sent. If the 'ProxyQueueManager' argument is specified, this should be the name of the target queue manager. If the 'ProxyQueueManager' argument is not specified, this can either be an C object, or the name of the Queue Manager. Specifying the queue manager name is deprecated and may stop working in a future release. The 'QueueManager' argument can be omitted, in which case the "default queue manager" is assumed. =item ProxyQueueManager The name of the queue manager to which to MQCONN(), and submit messages on the QueueManagers behalf. This is to be used if a direct connection to the QueueManager is not possible, for example MVS queue managers with support for direct client access. The messages will be put to what is assumed to be a remote queue definition which routes to the command queue on the desired QueueManager. In order to specify the "default" queue manager as the ProxyQueueManager, an empty string must be explicitly given. =item RealQueueManager If a remote queue manager is controlled through a proxy, by having the C parameter specify the proxy and the C an object descriptor, then by default any output will print the queue manager name incorrectly. The C parameter specifies the name used for display purposes. =item ReplyToQ The ReplyToQ can be opened by the application, and the MQSeries::Queue object passed in to the MQSeries::Command constructor, if so desired, os, a fixed queue name can be given. This is a somewhat advanced usage of the API, since the default behavior of opening a temporary dynamic queue under the covers is usually prefered, and much simpler. The responses are retrieved from the reply queue using gets by CorrelId, so there should be no issue with using a pre-defined, shared queue for this, if so desired. =item CommandQueueName This specifies the queue to which the command messages will be put. The defaults are usually reasonable, and depend on the command message Type (PCF or MQSC). PCF => SYSTEM.ADMIN.COMMAND.QUEUE MQSC => SYSTEM.COMMAND.INPUT If the ProxyQueueManager has been specified, then we assume the messages are being written to a remote queue definition, and the defaults are then: PCF => SYSTEM.ADMIN.COMMAND.QUEUE."QueueManager" MQSC => SYSTEM.COMMAND.INPUT."QueueManager" See "MQSC NOTES" for some examples of how to use this in practice. Alternatively, the C parameter can be used. =item CommandQueue For complex set-ups, where a remote queue manager is managed by the name of the command queue does not fit the standard scheme, the command queue can be opened manually and specified in the MQSeries::Command constructor as the C parameter. The C parameter must be combined with the C parameter. In the example below, a mainframe queue manager C is managed from the queue manager C with a non-standard queue name: $queue = MQSeries::Queue::-> new('QueueManager' => 'UNIXQM', 'Queue' => 'REMOTE.FOR.COMMANDQ.ON.CSQ1', 'Mode' => 'output', ) || die "Cannot open queue"; $cmd = MQSeries::Command::-> new('ProxyQueueManager' => 'UNIXQM', # Also ReplyToQMgr 'RealQueueManager' => 'CSQ1', # Displayed in messages 'Type' => "MQSC", 'CommandQueue' => $queue, ) || die "Cannot create command"; If both queue managers run MQ v6, then PCF commands can be used to communicate with MQ on z/OS. The above example would be changed as follows to use PCF commands: $cmd = MQSeries::Command::-> new('ProxyQueueManager' => 'UNIXQM', # Also ReplyToQMgr 'RealQueueManager' => 'CSQ1', # Displayed in messages 'Type' => 'PCF', 'CommandVersion' => MQSeries::MQCFH_VERSION_3, 'CommandQueue' => $queue, ) || die "Cannot create command"; This mechanism can also be used when the command queue needs to be opened with special options. This is typically combined with a call to the C method: my $queue = MQSeries::Queue::-> new('QueueManager' => 'UNIXQM', 'Queue' => 'SYSTEM.ADMIN.COMMAND.QUEUE', 'Options' => (MQSeries::MQOO_FAIL_IF_QUIESCING | MQSeries::MQOO_OUTPUT | MQSeries::MQOO_SET_IDENTITY_CONTEXT, ), ) || die "Cannot open queue"; $cmd = MQSeries::Command::-> new('RealQueueManager' => 'UNIXQM', 'Type' => 'PCF', 'CommandQueue' => $queue, ) || die "Cannot create command"; $cmd->MsgDesc('ApplIdentityData' => "MyData", 'UserIdentifier' => "mqm", ); =item CommandVersion This argument is introduced mainly to support the new PCF commands in MQ v6 (such as FilterCommand filter options) and to use PCF with QueueManagers on OS/390. It indicates the required PCF Command Version (MQCFH_CURRENT_VERSION) and Type. This is an optional argument and Version "1" is used as default. If a value '3' or higher is specified, then the Command Type "MQCFT_COMMAND_XR" will be used and also the version is set to "MQCFH_CURRENT_VERSION". Any value less then 3 will be ignored and the defaults are used (MQCFT_COMMAND and default version). =item Type This argument indicates whether the command server on the QueueManager supports either PCF or MQSC messages. The default is PCF. See the section "MQSC NOTES" for the Ugly Truth about the MQSC support. =item Expiry This value is used as the MQMD.Expiry field on all requests sent to the command server. The value is passed to the MQSeries::Message constructor, and should specify the time in B. The default is 600, or 60 seconds. A symbolic value ending on 's' for seconds or 'm' for minutes may also be specified, e.g. the symbolic value '45s' will have the same meaning as the number 450. =item Wait This value is used as the Wait argument to the MQSeries::Queue->Get() method call made against the ReplyToQ (a dynamic reply queue). and should be a time specified in B. The default is 60000, or 60 seconds. A symbolic value ending on 's' for seconds or 'm' for minutes may also be specified, e.g. the symbolic value '45s' will have the same meaning as the number 45000. NOTE: Both the Expiry and Wait defaults may be too slow for heavily loaded queue managers. Tune them appropriately. =item ReplyToQMgr The ReplyToQMgr normally defaults to the QueueManager, but it can be overridden, perhaps as a means of specifying an alternate return path over a specific channel. For example, the author uses special channels for SYSTEM related traffic, over which we forward MQSeries events from one queue manager to another, and also over which we wish the command server queries to flow. The "default" path between QMA and QMB flows over a channel called QMA.QMB, but this traffic is application data, not administrative system data. The system queries flow over QMA.QMB.SYSTEM, and we need to ensure that replies to queries follow a similar reverse path. Specifying the ReplyToQMgr as "QMB.SYSTEM" accomplishes this. =item Carp This key specifies a code reference to a routine to replace all of the carp() calls in the API, allowing the user of the API to trap and handle all of the error message generated internally, or simply redirect how they get logged. For example, one might want everything to be logged via syslog: sub MyLogger { my ($message) = @_; foreach my $line (split(/\n+/, $message)) { syslog("err", $line); } } Then, one tells the object to use this routine: my $command = MQSeries::Command->new( QueueManager => 'some.queue.manager', Carp => \&MyLogger, ) or die("Unable to connect to queue manager.\n"); The default, as one might guess, is Carp::carp(); =item StrictMapping If this argument has a true value, then strict mapping of PCF parameters and values will be enforced. Normally, if you feed a bogus string into the API, it will attempt to map it to the underlying PCF macro value, and if the mapping fails, it will quietly forgive you, and ignore the parameter. Enabling this feature will cause the translation of an encoded PCF message into the data structure for a Response, or the translation of a Request into an encoded PCF message, to fail if any of the mappings fail. Usually, the command server will generate errors if you feed bogus data into the API. but that will only occur after the data has been encoded and sent to the command server. This feature will allow you to detect this error before the data is ever sent. =back =head2 CompCode This method will return the MQI CompCode for the most recent MQI call made by the API. =head2 Reason This method will return the MQI Reason for the most recent MQI call made by the API. =head2 ReasonText This method will return different strings depending on whether the command is MQSC or PCF. For MQSC, the command server sends back some text explaining the reason code, and for PCF, we simply call MQReasonToText on the reason code and return that instead. MQSC sends back far less information encoded into the reason than PCF does, and the interesting information is usually found in the ReasonText. Therefore, this method should be used when raising exceptions, in order to get the most descriptive explanation for any given error. =head2 Responses Normally, the data of interest is returned from the method in question, but the individual responses are available via this method. This returns a list of MQSeries::Command::Response objects, one for each individual message recieved. =head2 DataParameters This method will return a list of parameters structures from all of the responses messages sent back which were B error responses. Some errors will send back responses with parameters, and these could easily be confused with real data (until you start looking at the actual data, of course). =head2 ErrorParameters This method will return a list of parameters structures from all of the responses messages sent back which B error responses. If a command fails, the Reason() will usually tell you enough about the cause of the failure, but if the reason is MQRCCF_CFIN_PARM_ID_ERROR, then the parameters in the error message will indicate which Parameter key was invalid. =head2 MsgDesc In some cases, it is useful or necessary to override specific MQMD fields in outgoing request messages. Examples include manually changeing the Persistence field, or changing the message identity or origin context. The MsgDesc method allows you to do so: $cmd->MsgDesc('Persistence' => 0); or $cmd->MsgDesc('ApplIdentityData' => "MyData", 'UserIdentifier' => "mqm", ); Note that setting the message identity or origin context requires you to open the command queue with the relevant open options; see the description of the C parameter to C. Note that the C, C and C fields for the request message descriptor can be specified in the constructor C. The C method is intended for the more obscure options. =head2 CreateObject This is a generic "wrapper" method for creating any generic MQSeries object. The arguments to this method are a hash, with the following key/value pairs: Key Value === ===== Attrs HASH reference Verify Boolean Clear Boolean Quiet Boolean Force Boolean Callback CODE reference The key/value pairs in the Attrs argument are passed directly to the corresponding CreateQueue(), CreateChannel(), or CreateProcess() method. However, there is more to this than just creating the object. For clarity, this discussion will use creation of a queue as an example. First, InquireQueue is called to see if the object already exists. If it does not exist, then the object is simply created, with the specified attributes. If it does exist, and the QType matches, then the actual object attributes are compared against those passed to the CreateObject method, and if they match, then no action is taken, and the object is not modified. If it does exist, but of a different QType, then the existing object is deleted, and the new object created as requested. The idea here is to match the modification of the object conditional on the need for modifying the object. If the same CreateObject method call, with the same arguments, is called twice, then the second method invocation should be a noop, as far as the actual MQSeries object is concerned. =over =item Attrs As discussed above, this is a HASH reference, whose key/value pairs are used to determine what type of object is being created or updated, and those key/value pairs are passed as-is to the appropriate Create* method call for the specified MQSeries object type. =item Verify If this key has a true value, then no changes will actually be implemented. They will merely be reported via test messages on stdout. =item Quiet CreateObject is by default rather chatty about what it is doing, but all of the messages, other than errors, can be suppressed if this key has a true value. =item Clear Normally, when a Local queue is being replaced with another QType (eg. Remote or Alias), then the Local queue is not cleared before being deleted. If there are messages on the queue, this will cause an error. If the queue needs to be cleared, then this key must be passed with a true value. This is a seperate option due to the inherit danger of destroying data accidentally. If you really want to clear the queues before recreating them as another QType, you will have to be explicitl about it. =item Force This option will be passed to the Change* method, if the object already exists and is of type Queue, forcing changes to be applied to objects which are currently in use. It is ignored for the other object types. Note that this should B be passed as a key to the Attrs hash, since this is not really an object attribute. If Force is given as an Attrs key, and the underlying Create* method is called, since the object does not already exist, then the command server will return an error. =item Callback This optional argument allows you to provide an object comparison subroutine, instead of relying on the built-in function. This is virtually never required, unless your request contains some 'meta' attributes that translate to multiple or different MQSeries attributes. The C parameter must be a reference to a subroutine that receives three parameters: a reference to the requested attributes, a reference to the attributes found in the object, and a reference to a subroutine that compares two single attribute values. The return value from the subroutine must be a reference to a hash with those attribute names and values that actually should be changed. Please see the source code of C, specifically the C<_CompareAttributes> method, for the default callback. Any callback you provide must behave in a similar way. =back =head1 COMMAND REQUESTS This section is B intended to replace the IBM "MQSeries Programmable System Management" documentation, and it merely serves to document the specific keys and values used by the perl implementation of the API. In all cases, the keys which can be passed to the command are identical to the strings found in the documentation. However, some of the values also have more convenient string mapping as well, as these are not easily intuited by reading IBMs docs, thus these are clarified here. The IBM docs list the possible values for each key as a list of MQSeries C macros (eg. MQCHT_SENDER), and if given, these will be respected. It will be faster to use the strings given, since these map directly to the actual macro values via a hash lookup, rather than a function call (all of the various C macros are implemented as function calls via an AUTOLOAD). Also, the author finds the replacement strings far more readable. YMMV. For each key shown, the format of value is one of the following: =over 4 =item (string) The value given must be a scalar value, interpretable as a text string. =item (string list) The value must be an ARRAY reference of scalar values, all of which must be interpretable as strings. NOTE: Some of the parameters (for example, the MsgExit parameter for the various Channel commands) can take either a string or string list. In this case, the API will be forgiving, and try to determine what you meant automatically. If you pass a reference, it will create a string list parameter, and if you pass a plain scalar, it will create a string. ANOTHER NOTE: if you pass a simple scalar string where a string list is explicitly required (as opposed to optional), then the API will create a list of one string for you. =item (integer) The value given must be a scalar value, interpretable as an integer. In some cases, a table will show a mapping from more readable text strings to the macros documented in the IBM manuals. =item (integer list) The value must be an ARRAY reference of scalar values, all of which must be interpretable as integers. As with the integer format, in some cases, a table will show a mapping from more readable text strings to the macros documented in the IBM manuals. =item (Boolean) The value given need only be 0 or 1. This was done for some of the integer types which are documented to take a pair of macros, whose values were simply zero or one, and when the true/false nature of the key was considered to be intiuitive. For example, "DataConversion" is either on (true) or off (false). =back Note: List of keys are not documented anymore to avoid needless redundancy. Please refer to the IBM documentation for the names and values of keys. =head2 AuthInfo Commands Change AuthInfo Copy AuthInfo Create AuthInfo Delete AuthInfo Inquire AuthInfo Inquire AuthInfo Names =head2 Authority Records Commands Delete Authority Records Inquire Authority Records Set Authority Record =head2 Channel Commands Change Channel Change Channel Listener Copy Channel Copy Channel Listener Create Channel Create Channel Listener Delete Channel Delete Channel Listener Inquire Channel Inquire Channel Initiator Inquire Channel Listener Inquire Channel Listener Status Inquire Channel Names Inquire Channel Status Ping Channel Reset Channel Resolve Channel Start Channel Start Channel Initiator Start Channel Listener Stop Channel Stop Channel Initiator Stop Channel Listener =head2 CFStruc Commands Change CFStruc Create CFStruc Delete CFStruc Inquire CFStruc Inquire CFStruc Names =head2 Cluster Commands Inquire Cluster Queue Manager Refresh Cluster Reset Cluster Resume Queue Manager Cluster Suspend Queue Manager Cluster =head2 Connection Commands Inquire Connection Stop Connection =head2 Namelist Commands Change Namelist Copy Namelist Create Namelist Delete Namelist Inquire Namelist Inquire Namelist Names =head2 Process Commands Change Process Copy Process Create Process Delete Process Inquire Process Inquire ProcessNames =head2 Queue Commands Change Queue Clear Queue Copy Queue Create Queue Delete Queue Inquire Queue Inquire Queue Names Move Queue Reset Queue Statistics =head2 Inquire Queue Status Command The C command does not behave like the other queue commands. Please refer to IBM documentation for complete details. For a request, the following keys are supported: QName StatusType OpenType QStatusAttrs The output of the C is dependent on the C specified. =head2 Queue Manager Commands Change Queue Manager Inquire Queue Manager Inquire Queue Manager Status Ping Queue Manager RefreshQueueManager ResetQueueManager ResumeQueueManager SuspendQueueManager =head2 Security Commands ChangeSecurity InquireSecurity RefreshSecurity ReverifySecurity =head2 Service Commands Change Service Copy Service Create Service Delete Service Inquire Service Inquire Service Status Start Service Stop Service =head2 StorageClass Commands Change StorageClass Create StorageClass Delete StorageClass Inquire StorageClass Inquire StorageClass Names =head2 Subscription Commands Change Subscription Copy Subscription Create Subscription Delete Subscription Inquire Subscription Inquire Subscription Status =head2 System Commands Inquire Group Inquire Log Inquire System Inquire Usage Set Log Set System =head2 Topic Commands Change Topic Clear Topic String Copy Topic Create Topic Delete Topic Inquire Topic Inquire Topic Names Inquire Topic Status =head2 Escape Command This command is not really part of a grouping, so its all by itself here. The following keys have special value mappings: =over 4 =item EscapeType (integer) Key Macro === ===== MQSC MQET_MQSC =back =head1 COMMAND RESPONSES There are several commands which return data. The return value of the actual command methods depends on the specific command. All of the "Inquire*Names" commands return a list of the actual names returned, which greatly simplifies the parsing of the return value. For example: @qnames = $command->InquireQueueNames( QName => 'FOO.*' ); will result in @qnames containing a list of strings of all of the queue names starting with FOO. The rest of the commands return a list of Parameters HASH references, extracted from each of the messages sent back from the command server. In a scalar context, only the first Parameters HASH reference is returned. Refer to the L section for the list of commands. For example: @queues = $command->InquireQueue( QName => 'FOO.*', QAttrs => 'All' ); will result in @queues containing a list of HASH references, each of which has key/value pairs for the attributes of one of the queues starting with FOO. The keys in the Parameters HASH references are mapped from the numeric macro values back into the same strings described above for simplifying the input Parameters. However, there are a few keys in the responses which are not supported as keys in the inquiry. In general, the return values are left unmolested. =head1 FILTERS For PCF requests to MQ v6 and above, filter commands can be used to have the queue manager filter out only objects of interest. For example, you can inquire for queues with a certain maximum message length, for connections from specific IP addresses, or for queue status with a minimum queue depth. Filters require that the command object is created with CommandVersion 3, i.e. $command = MQSeries::Command::-> new('QueueManager' => 'SOME.QUEUE.MANAGER', 'Type' => 'PCF', 'CommandVersion' => MQSeries::MQCFH_VERSION_3); Following that, most Inquire commands accept an optional FilterCommand parameter that specifies an attribute to filter on, a comparison operator, and a value. (See the IBM documentation for which Inquire commands support filters.) The filter can be specified in pseudo-SQL syntax, e.g. : @queues = $command-> InquireQueue('FilterCommand' => "QName like 'SYSTEM.PE*'"); @queues = $command-> InquireQueue('FilterCommand' => "MaxQDepth > 10000"); Alternatively, if the value used contains quotes, or if the filter is constructed dynamically, the filter can be specified as a hash reference: @queues = $command-> InquireQueue('FilterCommand' => { Parameter => 'MaxQDepth', Operator => '>', Value => 100_000, }); The supported filter parameters (field names) are almost any field returned by the Inquire command (see the IBM documentation for details). The supported value can be an integer, string, string wildcard (a value ending on a '*'), or (for enumerated types) the constant name as returned by the Inquire command. For example, for an enumerated type, to query for alias queues that have a rmote queue as their base type: @queues = $command-> InquireQueue('FilterCommand' => "BaseType == Remote"); The operators supported depend on the parameter type. All types support equal (==) and unequal (<>, !=); numeric types also support the numeric comparators (<, >, <=, >=); string types support 'like' and 'not like' with a wildcard value ending on '*'; integer list types support 'contains' and 'excludes'; and string list types support 'contains', 'excludes', 'contains_gen' and 'excludes_gen', where the latter to values perform a wildcard comparison. The examples below illustrate supported usage. BaseType == Local BaseType <> Local BytesSent > 10000 CurrentQDepth > 10000 Msgs <= 100 ConnectionName like '144.14.*' BaseQName not like 'SYSTEM.*' HeaderCompression contains 'gzip' HeaderCompression excludes 'gzip' Note that the filter is not evaluated as a perl expression. The MQSeries::Command class parses the expression into a parameter, operator and value; then converts the value to the appropriate type (a value lookup for enumerated types, a conversion to integer for numbers, and conversion to string for strings). In other words, the expected amount of DWIM is in place. =head1 SEE ALSO MQSeries(3), MQSeries::Queue(3), MQSeries::Message(3), MQSeries::Command::Request(3), MQSeries::Command::Response(3) In addition, the MQSeries documentation is the primary source of documentation for the commands and their arguments, especially the following sections. For MQSeries 5.2 and before, this is: "MQSeries Programmable System Management" Part 2. Programmable Command Formats Chapter 8. Definitions of the Programmable Command Formats Chapter 9. Structures used for commands and responses For WebSphere MQ 5.3, this is: "WebSphere MQ Programmable Command Formats and Administration Interface" Part 1. Programmable Command Formats Chapter 4. Definitions of the Programmable Command Formats Chapter 5. Structures used for commands and responses For WebSphere MQ 6.0 and 7.0, this is: Same as 5.3 except the Chapter numbers. In 6.0 doc they are Chapter 3 and 4. =cut