Soap Wisdom

Overview

All of the HTCondor daemons can talk soap as detailed in the 7.6 manual

For a very clear and concise intro to SOAP messaging, see http://www.soapware.org/bdg

A good resource is a 2006 HTCondor Week Powerpoint presentation that gives an overview using HTCondor's SOAP interface and some code snippets in Java: http://www.cs.wisc.edu/condor/CondorWeek2006/presentations/farrellee_tannenba_APIs.ppt

Barebones netcat example: (assumes somehost.example.com:9618 is your soap-enabled collector)

[griswold@nmi-s005 build]$ nc somehost.example.com 9618 <<EOF
> POST / HTTP/1.1
> User-Agent: whatever
> Content-Type: text/xml; charset=utf-8
> SOAPAction: ""
>
> <?xml version="1.0" encoding="UTF-8"?>
> <SOAP-ENV:Envelope
>  xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
>  xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
>  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
>  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
>  xmlns:ns="urn:condor">
>  <SOAP-ENV:Body SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
>      <ns:getVersionString />
>  </SOAP-ENV:Body>
> </SOAP-ENV:Envelope>
> EOF
HTTP/1.1 200 OK
Server: gSOAP/2.7
Content-Type: text/xml; charset=utf-8
Content-Length: 547
Connection: close

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:condor="urn:condor">
<SOAP-ENV:Body>
<condor:getVersionStringResponse>
<response>
<status>SUCCESS</status>
<message>$CondorVersion: 7.1.4 Nov 10 2008 BuildID: 113473 $</message></response>
</condor:getVersionStringResponse>
</SOAP-ENV:Body></SOAP-ENV:Envelope>

Valid function calls such as getVersionString in the above example can be found in the different wsdl files provided with HTCondor. HTCondor ships with wsdl files for all daemons and they are located in lib/webservice in the binary releases. Also note the use of the namespace urn:condor, you may have to specify this in your client library.

Each daemon provides a separate SOAP server. The only HTCondor daemon that runs on a static port by default is the collector (port 9618). Therefore, in order to connect to other daemons in your pool (whose ports are randomized), you have two options:

  1. Configure any daemons you care about to have static port configurations. In the case of the schedd, this would be done using something like SCHEDD_ARGS = -p 8080. This can simplify client code. You can further simplify client code by updating the location parameter found at the bottom of any wsdl files you make use of.

  2. Don't worry about static ports and discover all daemon addresses in your pool dynamically from the collector. This is the route chosen in the below tutorial and in birdbath ( http://www.cs.wisc.edu/condor/birdbath/ )

Tutorial

This is a minimalist example to get you going. It is tested with suds-0.3.5 and python 2.4 (should be python 2.3 compatible). It should be helpful to get going with other languages, as well.

HTCondor Configuration

Note that this is a really insecure configuration , especially for public hosts. See the manual for more info.

Also note that we are exposing the wsdl files on each server using WEB_ROOT_DIR. This is especially important in mixed HTCondor version environments since it ties the wsdl to the server it is used on.

Add the below to all HTCondor configurations

ENABLE_SOAP = TRUE
ENABLE_WEB_SERVER = TRUE
WEB_ROOT_DIR=$(RELEASE_DIR)/lib/webservice
ALLOW_SOAP = */*
QUEUE_ALL_USERS_TRUSTED = TRUE

Note: In order to process and receive API calls, the machine acting as the SOAP gateway must have the ability to submit jobs. A dedicated central manager will be unable to process the request.

Barebones suds client with xml debugging output

This is equivalent to the netcat example above. It connects to the collector and queries the version.

#!/usr/bin/env python

from suds.client import Client
import logging

logging.basicConfig(level=logging.INFO)
logging.getLogger('suds.client').setLevel(logging.DEBUG)

collector_url = 'http://somehost.example.com:9618/'

if __name__ == '__main__':
    url = '%scondorCollector.wsdl' % collector_url
    collector = Client(url, cache=None, location=collector_url)

    print collector.service.getVersionString()

Notes:

  1. We are using the (currently) undocumented "location" suds.client.Client argument to override any location defined in the WSDL.

Slightly more advanced client to find a schedd

This thing will pick a random schedd among those with TotalRunningJobs < 5 and print its suds structure. Should add transactional job submission to this example.

#!/usr/bin/env python

from suds.client import Client
import urllib
import random
import logging
import re

collector_url = 'http://nighthawk.cs.wisc.edu:9618/'

def classad_dict(ad):
    native = {}
    attrs = ad[0]
    for attr in attrs:
        native[attr['name']] = attr['value']
    return native


condor_addr_re = re.compile('<(?P<ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(?P<port>\d+)>')

def find_schedd(collector, constraint=''):
    '''
    Pick a schedd matching the given, optional constraint
    raises ValueError if no matching schedd can be found
    '''

    schedds = collector.service.queryScheddAds(constraint)
    try:
        schedd = random.choice(schedds[0])
    except IndexError:
        raise ValueError('no schedds found')

    print schedds
    schedd_ad = classad_dict(schedd)
    m = condor_addr_re.match(schedd_ad['ScheddIpAddr'])
    assert m, 'bad address %s' % schedd_ad['ScheddIpAddr']

    url = 'http://%(ip)s:%(port)s/' % m.groupdict()

    schedd = Client('%scondorSchedd.wsdl' % url, cache=None, location=url)

    return schedd

if __name__ == '__main__':
    url = '%s/condorCollector.wsdl' % collector_url
    collector = Client(url, cache=None, location=collector_url)

    schedd = find_schedd(collector, 'TotalRunningJobs < 5')
    print schedd

Notes:

  1. The HTCondor classad structures are a little strange. Instead being a mapping of string keys to native soap types, they are an array of mappings that look like this:
    (item){
       name = "CondorVersion"
       type = "STRING-ATTR"
       value = "$CondorVersion: 7.3.1 May 19 2009 BuildID: 154007 $"
     }
    
    Consider adding conversion to native types in your language of choice (the classad_dict function is really naive).

  2. The structures seem slightly too deep. For example, in order to access the ad listing for the first schedd returned from queryScheddAds, you must index result[0][0][0]. The result structure looks like this:
    (ClassAdStructArray){
       item[] =
          (item){
             item[] =
                (item){
                   name = "MyType"
                   type = "STRING-ATTR"
                   value = "Scheduler"
                },... etc
          },
     }
    

Additional Examples

This blog post explains how to submit a DAGman job to HTCondor using the SOAP interface from Java.

Here are a few additional examples of using the HTCondor SOAP interface, this time in Perl. These examples assume that the condor_schedd has been configured to run on well-known port 8080 as described above. Note that you should be using a very recent version of the SOAP::Lite module. 0.60 won't work, 0.712 will.

Get Version String in Perl

use SOAP::Lite ;
my $soap = SOAP::Lite->new(
    proxy => 'http://localhost:8080/soap',
        default_ns => 'urn:condor'
);


my $som = $soap->call(
    "getVersionString"
);
die $som->fault->{ faultstring } if ($som->fault);

my %result = %{$som->result};
print "$result{'message'}\n";

$som = $soap->call(
    "getPlatformString"
);
die $som->fault->{ faultstring } if ($som->fault);

%result = %{$som->result};
print "$result{'message'}\n";

Get Job Queue in Perl

This code example will produce output similar to condor_q -l . An optional command line argument can pass a constraint, similar to the -constraint option with condor_q.
use SOAP::Lite ;

#
# Create a connection to the schedd
#
my $soap = SOAP::Lite->new(
    proxy => 'http://localhost:8080/soap',
        default_ns => 'urn:condor'
);

#
# Invoke getJobAds()
#
my $som = $soap->call(
    "getJobAds",
        SOAP::Data->name('constraint')->value( $ARGV[0] || 'TRUE'),
);
die $som->fault->{ faultstring } if ($som->fault);
my %result = %{$som->result};

#
# For each ad returned, print all attributes
#
my @ads;
if( ref ($result{'classAdArray'}{'item'}) eq 'HASH') {
        @ads = $result{'classAdArray'}{'item'};
} else {
        @ads = @{$result{'classAdArray'}{'item'}};
}
foreach my $ad_ref (@ads) {
        my @ad = @{$ad_ref->{'item'}};
        foreach my $attr_ref (@ad) {
                my %attr = %$attr_ref;
                print "  $attr{'name'} = $attr{'value'} \n";
        }
        print "===============================\n";
}