1. BEP

1. Set up Server TrustStore, copy from RabbitMQ we set up in the previous article.
# mkdir /l01/app/oracle/rabbitmqcertificates
# cp "emb1.pprd.odu.edu folder"/server/cert.pem -> /l01/app/oracle/rabbitmqcertificates
# cp "emb1.pprd.odu.edu folder"/client/keycert.p12 -> /l01/app/oracle/rabbitmqcertificates

Create a new truststore file rabbitstore
# keytool -import -alias rabbitmqserver -file /l01/app/oracle/rabbitmqcertificates/cert.pem -keystore rabbitstore

2. Install through ESM, and then, configure the application
# BannerEventPublisher_configuration.groovy
/*******************************************************************************
 *                                                                              *
 *              Banner Event Publisher DataSource Configuration                  *
 *                                                                              *
 *******************************************************************************/

dataSource_cdcadmin {
    //JNDI configuration for use in 'production' environment
	jndiName = "java:comp/env/jdbc/cdcadmin"
    transactional = false
}

dataSource_events {
    //JNDI configuration for use in 'production' environment
	jndiName = "java:comp/env/jdbc/events"
	transactional = false
}

dataSource_bannerSsbDataSource {
    //JNDI configuration for use in 'production' environment
	jndiName = "java:comp/env/jdbc/bannerSsbDataSource"
	transactional = false
}

dataSource_bannerDataSource {
    //JNDI configuration for use in 'production' environment
        jndiName = "java:comp/env/jdbc/bannerDataSource"
        transactional = false
}


bep {
	//App Server
	//Possible values are either "TOMCAT" or "WEBLOGIC"
    app.server = "TOMCAT"
	
	//Message Broker
	//Possible values are "RABBITMQ" or "WEBLOGIC" or "RABBITMQ/WEBLOGIC"
	message.broker = "RABBITMQ"

	//Retry interval to publish to broker in SECONDS
    publish.retry.interval = 45
}

//RabbitMQ configuration
rabbitmq {
	host = "emb1.pprd.odu.edu"
	port = "5671"
	userName = "ellucian"
	password = "password"
        virtualHostName = "bep_events_host"
	exchangeName = "bep_events_topic"    
	enableSSL = "true"

	//Validate rabbit connections
        validate = true

	//Put an actual path to a file starting with "file:" otherwise leave the value as NO_FILE
	keyStoreFileName = "file:/l01/app/oracle/rabbitmqcertificates/keycert.p12" 		
	keyStorePassPhrase = "pass"
    
	//Put an actual path to a file starting with "file:" otherwise leave the value as NO_FILE
	trustStoreFileName = "file:/l01/app/oracle/rabbitmqcertificates/rabbitstore" 	    
	trustStorePassPhrase = "pass"
}

jms {
	validate = true
}

// This configuration needs to be done in milliseconds for the footer to appear in the screen
footerFadeAwayTime=2000

// Application Navigator opens embedded applications within an iframe. To protect against the clickjacking vulnerability,
// integrating applications will have to set the X-Frame options to protect the "login/auth" URI from loading in the iframe and
// set it to denied mode. This setting is needed if the application needs to work inside Application Navigator and
// the secured application pages will be accessible as part of the single-sign on solution.
grails.plugin.xframeoptions.urlPattern = '/login/auth'
grails.plugin.xframeoptions.deny = true


// End of configuration

3. Set up Tomcat
# server.xml, configure database connection

  <GlobalNamingResources>
    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
     <Resource name="jdbc/bannerDataSource" auth="Container" type="javax.sql.DataSource"
       driverClassName="oracle.jdbc.OracleDriver"
       url="jdbc:oracle:thin:@//bannerdb.pprd2.odu.edu:2336/PPRD2"
       username="banproxy" password="password"
        initialSize="5" maxTotal="600" maxIdle="-1" maxWaitMillis="30000"
        validationQuery="select 1 from dual"
        accessToUnderlyingConnectionAllowed = "true"
        removeAbandonedOnBorrow = "true"
        testOnBorrow="true"/>
    <Resource name="jdbc/bannerSsbDataSource" auth="Container"
        type="javax.sql.DataSource"
        driverClassName="oracle.jdbc.OracleDriver"
        url="jdbc:oracle:thin:@//bannerdb.pprd2.odu.edu:2336/PPRD2"
        username="ban_ss_user" password="password"
        initialSize="5" maxTotal="600" maxIdle="-1" maxWaitMillis="30000"
        validationQuery="select 1 from dual" 
        accessToUnderlyingConnectionAllowed = "true"
        removeAbandonedOnBorrow = "true"
        testOnBorrow="true"/>

    <Resource name="jdbc/events" auth="Container"
        type="javax.sql.DataSource"
        driverClassName="oracle.jdbc.OracleDriver"
        url="jdbc:oracle:thin:@//database.domain.edu:2336/PPRD2"
        username="events" password="password"
        initialSize="5" maxTotal="600" maxIdle="-1" maxWaitMillis="30000"
        validationQuery="select 1 from dual" 
        accessToUnderlyingConnectionAllowed = "true"
        removeAbandonedOnBorrow = "true"
        testOnBorrow="true"/>

    <Resource name="jdbc/cdcadmin" auth="Container"
        type="javax.sql.DataSource"
        driverClassName="oracle.jdbc.OracleDriver"
        url="jdbc:oracle:thin:@//database.domain.edu:2336/PPRD2"
        username="cdcadmin" password="password"
        initialSize="5" maxTotal="600" maxIdle="-1" maxWaitMillis="30000"
        validationQuery="select 1 from dual" 
        accessToUnderlyingConnectionAllowed = "true"
        removeAbandonedOnBorrow = "true"
        testOnBorrow="true"/>
  </GlobalNamingResources>

# context.xml, configure datasource
     <ResourceLink global="jdbc/bannerDataSource" name="jdbc/bannerDataSource" type="javax.sql.DataSource"/>
     <ResourceLink global="jdbc/bannerSsbDataSource" name="jdbc/bannerSsbDataSource" type="javax.sql.DataSource"/>
     <ResourceLink global="jdbc/events" name="jdbc/events" type="javax.sql.DataSource"/>
     <ResourceLink global="jdbc/cdcadmin" name="jdbc/cdcadmin" type="javax.sql.DataSource"/> 

# The war file which deployed in Tomcat should be named as BannerEventPublisher.war

4. The user who needs to log into BEP should be granted:
Grant object BEP_ADMIN_OBJECT / BAN_DEFAULT_M to the user who needs access through INB/GSASECR, then 
https://ellucian.force.com/clients/s/article/Logging-into-BEP-gives-you-You-are-not-Authorized-to-view-this-page-error

SQL> GRANT EXECUTE ON GOKFGAC TO <BEP_USER>;
SQL> GRANT EXECUTE ON GB_COMMON TO <BEP_USER>;
SQL> GRANT SELECT ON TWGRMENU TO <BEP_USER>;
SQL> GRANT SELECT ON TWGRWMRL TO <BEP_USER>;
SQL> GRANT SELECT ON TWGRROLE TO <BEP_USER>;

2. Messaging Service

Configure ESM with the configuration of rabbitmq I created before.
./ConfigureEMS ellucian oracle123 bep_events_host 

3. Messaging Adapter
Before configure Messaging Adapter, we need:
   1. Create two applications in the Ethos cloud, one for student API and another for integration API. Add all resources into them. And get the API keys of them
   2. One user in the local banner database, Ethos Cloud will use it to access banner to fetch database. Make sure you grant necessary permission to it.

$ ./ConfigureEMS.sh
<?xml version="1.0" encoding="UTF-8"?>
<emsConfig>
    <clientErpType>Banner</clientErpType> <!-- Colleague or Banner -->
    <configId>ETHOS-INTEGRATION</configId> <!-- Change this to match config name for Colleague (HUB is delivered default), set to ETHOS-INTEGRATION for Banner -->
    <amqpUsername>ellucian</amqpUsername> <!-- EMS (RabbitMQ) username -->
    <amqpPassword>oracle123</amqpPassword> <!-- EMS (RabbitMQ) password -->

    <colleagueApiConfig>
        <baseUrl>https://server:port/ColleagueApi/</baseUrl> <!-- Colleague Web Api Url -->
        <username>required</username> <!-- Colleague Web Api username -->
        <password>required</password> <!-- Colleague Web Api password -->
        <hubApiKey>required</hubApiKey> <!-- API key from  your Ethos Integration application -->
    </colleagueApiConfig>

    <bannerStudentConfig>
        <baseUrl>https://apiserver:7005/studentapi/api</baseUrl> <!-- Banner Student Api Url -->
        <username>apiuser</username> <!-- Banner Student Api username -->
        <password>password</password> <!-- Banner Student Api password -->
        <apiKeys> <!-- Banner MEP clients add new apiKey sections for each VPDI code -->
            <apiKey>
                <hubApiKey>apikey</hubApiKey> <!-- API key from  your Ethos Integration application -->
                <vpdiCode></vpdiCode> <!-- Leave blank if MEP is not used -->
            </apiKey>
        </apiKeys>
        <mepSharedDataApiKey></mepSharedDataApiKey> <!-- API key to use when publishing shared data in a MEP environment -->
    </bannerStudentConfig>

    <bannerIntegrationConfig>
        <baseUrl>https://apiserver:7005/intgrationapi/api</baseUrl> <!-- Banner Integration Api Url -->
        <username>apiuser</username> <!-- Banner Integration Api username -->
        <password>passwd</password> <!-- Banner Integration Api password -->
        <apiKeys> <!-- Banner MEP clients add new apiKey sections for each VPDI code -->
            <apiKey>
                <hubApiKey>apikey</hubApiKey> <!-- API key from  your Ethos Integration application -->
                <vpdiCode></vpdiCode> <!-- Leave blank if MEP is not used -->
            </apiKey>
        </apiKeys>
        <mepSharedDataApiKey></mepSharedDataApiKey> <!-- API key to use when publishing shared data in a MEP environment -->
    </bannerIntegrationConfig>

    <!-- startup logging level...this will be overridden by the value returned from the api config endpoint -->
    <logLevel>INFO</logLevel>

    <!-- Number of messages to pull from RabbitMQ for parallel processing api calls -->
    <amqpBatchSize>80</amqpBatchSize>

    <!-- Parallel processing settings for making api calls -->
    <autoConfigurePool>true</autoConfigurePool> <!-- Pool grows as messages come in (max # of threads is limited by batch size). 60 sec idle threads will be evicted -->
    <processingThreads>4</processingThreads> <!-- Number of connection pool threads to use for parallel api calls.  This is not used if 'autoConfigurePool' it true -->

    <!-- Api timeout retry settings -->
    <apiTimeoutInterval>300000</apiTimeoutInterval> <!-- value in milliseconds -->
    <maxRetryAttempts>1</maxRetryAttempts>

    <!-- Number of messages to publish to ethos integration at once...max of 20 -->
    <ethosPublishBatchSize>20</ethosPublishBatchSize>

    <!-- Banner database connection settings.  This is only necessary for a Banner MEP environment -->
    <bannerConnectString>jdbc:oracle:thin:@database.domain.edu:2336:PPRD2</bannerConnectString>
    <jdbcDriver>oracle.jdbc.pool.OracleDataSource</jdbcDriver>

</emsConfig>

# set up a key used to encipher communication in the Tomcat's setenv.sh
export EMA_CONFIG=oracle123

# The Adapter cannot be deployed to the same Tomcat with API


Summay:
   1.BEP pushs the data changes which BEP captures from Banner to RabbitMQ.
   2.Internally, Messaging Service is a wrapper of RabiitMQ.
   3.Messaging Adapter will fetch data out from RabbitMQ and push it to Ethos Cloud.

1. Packages

rabbitmq-server-3.7.17-1.el6.noarch
https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.7.17/rabbitmq-server-3.7.17-1.el7.noarch.rpm

erlang-22.0.6-1.el6.x86_64
https://dl.bintray.com/rabbitmq-erlang/rpm/erlang/22/el/6/x86_64/:erlang-22.0.7-1.el6.x86_64.rpm

2. Generate SSL cert & key

Create Self-signing Authority and Root certificate

# mkdir testca ; cd testca
# mkdir certs; mkdir private; chmod 700 private
# touch index.txt; echo 01 > serial
# vi openssl.cnf
[ ca ]
default_ca = testca
[ testca ]
dir = .
certificate = $dir/cacert.pem
database = $dir/index.txt
new_certs_dir = $dir/certs
private_key = $dir/private/cakey.pem
serial = $dir/serial
default_crl_days = 7
default_days = 365
default_md = sha1
policy = testca_policy
x509_extensions = certificate_extensions
[ testca_policy ]
commonName = supplied
stateOrProvinceName = optional
countryName = optional
emailAddress = optional
organizationName = optional
organizationalUnitName = optional
[ certificate_extensions ]
basicConstraints = CA:false
[ req ]
default_bits = 2048
default_keyfile = ./private/cakey.pem
default_md = sha1
prompt = yes
distinguished_name = root_ca_distinguished_name
x509_extensions = root_ca_extensions
[ root_ca_distinguished_name ]
commonName = hostname
[ root_ca_extensions ]
basicConstraints = CA:true
keyUsage = keyCertSign, cRLSign
[ client_ca_extensions ]
basicConstraints = CA:false
keyUsage = digitalSignature
extendedKeyUsage = 1.3.6.1.5.5.7.3.2
[ server_ca_extensions ]
basicConstraints = CA:false
keyUsage = keyEncipherment
extendedKeyUsage = 1.3.6.1.5.5.7.3.1

Create the Root Certificate for the self-signing authority
# openssl req -x509 -config openssl.cnf -newkey rsa:2048 -days 3650 -out cacert.pem -outform PEM -subj /CN=MyTestCA/ -nodes

Convert the public key to the DER format
# openssl x509 -in cacert.pem -out cacert.cer -outform DER
			
The files of root certificate: cacert.pem / cacert.cer

Generate RabbitMQ Server certificate
# mkdir server; cd server

Generate a key
# openssl genrsa -out key.pem 2048

Generate a request
# openssl req -new -key key.pem -out req.pem -outform PEM -subj /CN=emb1.pprd.odu.edu/O=server/ -nodes 

CA sign the server certificate
# cd ..
# openssl ca -config openssl.cnf -in server/req.pem -out server/cert.pem -notext -batch -extensions server_ca_extensions

Generate the keystore
# cd server
# openssl pkcs12 -export -out keycert.p12 -in cert.pem -inkey key.pem -passout pass:oreol410g    <- keyStorePassPhrase.

Generate RabbitMQ Client certificate
# mkdir client; # cd client

Generate a key
# openssl genrsa -out key.pem 2048

Generate a request
# openssl req -new -key key.pem -out req.pem -outform PEM -subj /CN=bep.pprd.odu.edu/O=client/ -nodes

CA sign the client certificate
# cd ..
# openssl ca -config openssl.cnf -in client/req.pem -out client/cert.pem -notext -batch -extensions client_ca_extensions

Generate the keystore
# cd client
# openssl pkcs12 -export -out keycert.p12 -in cert.pem -inkey key.pem -passout pass:oreol410g    <- keyStorePassPhrase.

3. Configure

$ cat /etc/rabbitmq/rabbitmq.config|grep -v -E %%|grep -v -E "^$"
[
 {rabbit,
     {ssl_listeners, [5671]},
     {ssl_options, [{cacertfile,           "/l01/app/ca_certs/testca/cacert.pem"},
                    {certfile,             "/l01/app/ca_certs/testca/server/cert.pem"},
                    {keyfile,              "/l01/app/ca_certs/testca/server/key.pem"}, 
                    {verify,               verify_peer},
                    {fail_if_no_peer_cert, false},
                    {versions, ['tlsv1.2', 'tlsv1.1', tlsv1]},
                    {ciphers, ["ECDHE-ECDSA-AES256-GCM-SHA384","ECDHE-RSA-AES256-GCM-SHA384",
                      "ECDHE-ECDSA-AES256-SHA384","ECDHE-RSA-AES256-SHA384", "ECDHE-ECDSA-DES-CBC3-SHA",
                      "ECDH-ECDSA-AES256-GCM-SHA384","ECDH-RSA-AES256-GCM-SHA384","ECDH-ECDSA-AES256-SHA384",
                      "ECDH-RSA-AES256-SHA384","DHE-DSS-AES256-GCM-SHA384","DHE-DSS-AES256-SHA256",
                      "AES256-GCM-SHA384","AES256-SHA256","ECDHE-ECDSA-AES128-GCM-SHA256",
                      "ECDHE-RSA-AES128-GCM-SHA256","ECDHE-ECDSA-AES128-SHA256","ECDHE-RSA-AES128-SHA256",
                      "ECDH-ECDSA-AES128-GCM-SHA256","ECDH-RSA-AES128-GCM-SHA256","ECDH-ECDSA-AES128-SHA256",
                      "ECDH-RSA-AES128-SHA256","DHE-DSS-AES128-GCM-SHA256","DHE-DSS-AES128-SHA256",
                      "AES128-GCM-SHA256","AES128-SHA256","ECDHE-ECDSA-AES256-SHA",
                      "ECDHE-RSA-AES256-SHA","DHE-DSS-AES256-SHA","ECDH-ECDSA-AES256-SHA",
                      "ECDH-RSA-AES256-SHA","AES256-SHA","ECDHE-ECDSA-AES128-SHA",
                      "ECDHE-RSA-AES128-SHA","DHE-DSS-AES128-SHA","ECDH-ECDSA-AES128-SHA",
                      "ECDH-RSA-AES128-SHA","AES128-SHA"]},
                    {honor_cipher_order, true}
                     ]}



  ]},
 {kernel,
  ]},
 {rabbitmq_management,
  ]},
 {rabbitmq_management_agent,
  ]},
 {rabbitmq_shovel,
  [{shovels,
    ]}
  ]},
 {rabbitmq_stomp,
  ]},
 {rabbitmq_mqtt,
  ]},
 {rabbitmq_amqp1_0,
  ]},
 {rabbitmq_auth_backend_ldap,
  ]}
].

4. Define resources

# rabbitmqctl add_user ellucian oracle123
# rabbitmqctl add_vhost bep_events_host
# rabbitmqctl set_permissions -p bep_events_host ellucian ".*" ".*" ".*"
# rabbitmqctl authenticate_user ellucian oracle123
# rabbitmqctl set_user_tags ellucian administrator

1. Structure


Ethos has two data flows:
 1. red line, the third party requests data through Ethos from an institution
 2. orange line, the Banner push the modifications to the subscribers.

The following components need to be installed if you only use the first one
 1. Ethos API DB Upgrade
 2. Ethos API Management Center - emb1
 3. Banner Student API - emb1
 4. Banner Integration API - emb1


If you plan to use subscription, you need to install
 1. BEP - bep
 2. Ellucian Messaging Service - emb1
 3. Ellucian Messaging Adapter - emb1

2. compatibility
We should go to
https://ecommunities.ellucian.com/thread/60814
to check the compatibility matrix at first to plan our installation.

3. Prepare
The password of the following users
  1. apiuser – Used by Ethos cloud to access the local banner database.
  2. odsstg
  3. banproxy

The following user will be created for Ethos
  1. cdcadmin
  2. events
  3. banguidgen

The rabbitmq is required when you install BEP and Ellucian Messaging Service.
I will give you a guide about the install and configure BEP in another article.

4. Ethos API DB Upgrade
Using ESM to install. No application componment
From Ethos 9.9, the GUID generation which is very time consuming was splited from the DB install.

5. Ethos API Management Center
Using ESM to install.
Configuration:

  • encipher banproxy passowrd
      https://ellucian.force.com/clients/s/article/Ethos-API-Management-Center-login-denied
bash-4.1$ bash ./encrypt.sh input=<banproxy password> password=<encipher password> algorithm=PBEWithMD5AndDES


----ENVIRONMENT-----------------
Runtime: Sun Microsystems Inc. OpenJDK 64-Bit Server VM 23.41-b41 

----ARGUMENTS-------------------
algorithm: PBEWithMD5AndDES
input: <banproxy password>
password: <encipher key>

----OUTPUT----------------------
A93QdvdVv5pufChfukAEoor66iZ4FBJJQmJ/7KF+oKtHdQxNUHilZA==

The output here is encrypted password strings.

  • application.properties
# *******************************************************************************
#   Copyright 2018 Ellucian Company L.P. and its affiliates.
# *******************************************************************************

spring.datasource.url=jdbc:oracle:thin:@bannerdb.pprd2.odu.edu:2336:PPRD2
spring.datasource.password=ENC(A93QdvdVv5pufChfukAEoor66iZ4FBJJQmJ/7KF+oKtHdQxNUHilZA==)     <- banproxy password
app.oracle.useRadiusAuthentication=false
  • Tomcat - setenv.sh
export JAVA_OPTS="-server -Xms3g -Xmx5g -XX:MaxPermSize=1024m 
                  -Doracle.jdbc.autoCommitSpecCompliant=false 
                  -Dlog4j.configuration=config.properties 
                  -Djasypt.encryptor.password=<encipher key>"  <- The key which used to encipher
  • The users you want to use to log into Ethos Management Center, you have to grant BAN_EEAMC_C / BAN_GENERAL_C to them.

6. Banner Student / Integration API
Just use ESM to install

Until now, we have done the installation of Ethos, only for data requesst.
You can go to Ethos Cloud to configuration applications and use postman for testing.
There is a example: https://ecommunities.ellucian.com/docs/DOC-11172


We are using Banner BDM RESTful API to upload file from the local desktop to the BDM as batch.
The key point here is that we should use form-data with multipe parts as body to post data.
In python, I found requests_toolbelt.MultipartEncoder can achieve this goal.

And there are two API:

  1. axbatches is used to create a batch, and return the id of the batch.
  2. axbatchpages, with batch id to add pages to it
import json
import os
import datetime
import requests_toolbelt
import requests

class AppXtenderReST:
    site = "https://Server/AppXtenderReST/api/axdatasources/PPRD2"

    def __init__(self, auth, appid):
        self.auth = auth
        self.appid = appid

    def createHeader(self, contentType):
        headers = {
            'Accept': 'application/vnd.emc.ax+json',
            'Authorization': 'Basic ' + self.auth,
            'Host': 'webxt.pprd.odu.edu',
            'Content-Type': contentType
        }
        return headers

    def getFName(self, filepath):
        head, tail = os.path.split(filepath)
        return tail;

    def axbatches(self, name,  filepath):
        url = self.site + "/" + "axbatches" + "/" + str(self.appid)
        payload = {'Name': name ,
                   'Description': 'created by Python Automation Script for App ' + str(self.appid)
                                  + ' on '+ datetime.datetime.now().strftime('%Y-%m-%d'),
                   'Private': False
                   }
        m = requests_toolbelt.MultipartEncoder(
            fields={'data': (None, json.dumps(payload), 'application/vnd.emc.ax+json; charset=utf-8'),
                    'bin': (self.getFName(filepath), open(filepath, 'rb'), 'application/bin')}
                    # 'annotation' : ()
                    # 'text' : ()
            )
        response = requests.post(url, headers=self.createHeader(m.content_type), data=m)
        info = response.json()
        if response.status_code == 200:
            return info['ID'], info['PageCount']
        else:
            raise Exception(response.status_code, info)

    def axbatchpages(self, batchid, filepath):
        url = self.site + "/axapps/" + str(self.appid) + '/' + "axbatchpages" + "/" + str(batchid)
        m = requests_toolbelt.MultipartEncoder(
            fields={'bin': (self.getFName(filepath), open(filepath, 'rb'), 'application/bin')}
            )

        response = requests.post(url, headers=self.createHeader(m.content_type), data=m)
        info = response.json()
        if response.status_code == 200:
            return info['Page']
        else:
            raise Exception(response.status_code, info)



if __name__ == "__main__":
    # generate key by using b64encode 
    # from base64 import b64encode
    # auth = b64encode(b"user:password").decode("ascii")  
    rest = AppXtenderReST("key", 509)  # replace appid here  
    rest.upload('batch_test_5', 'C:/Users/q1zhang/Downloads/test')


Due to the situation of Conorvius, all stuff in my university works from home.
In order to forward fax to BDM directly, we asked DSG to convert fax to PDF to a shared folder at first.
And then, I am writing a python code to invoke ApplicationExtender API to index PDFs to a new created app.

I have done the code for the Login, Logout, CreateNewDocument, unlockDocumentByRef, CreateBatchPage, and UploadImageStream.
But unfortunately, CreateBatchPage and UploadImageStream are not working.
The document of ApplicationExtender is ridiculous, full of the error.
And there is no way for debuging.

We are using RESTful API to upload file as batch. In stead of going on the research of CreateBatchPage and UploadImageStream to save the time.

The code will iterator files in one folder, then, call CreateNewDocument to index each PDF with two field: ID and Time.

import xml.dom.minidom
from xml.dom.minidom import parse, parseString
import requests
import base64
import os
import datetime
import sys
import smtplib

user = 'user'
password = 'password'
datasource = 'datasource'

class AxServices:
    # constant variable
    url = 'https://xxx.odu.edu/AppXtenderServices/axservicesinterface.asmx'
    xsi = 'http://www.w3.org/2001/XMLSchema-instance'
    xsd = 'http://www.w3.org/2001/XMLSchema'
    ax = 'http://www.emc.com/ax'
    true = "true"
    false = "false"
    buffer_size = 8192

    # classes
    class AxObject:
        def __init__(self, tag, attr):
            self.doc = xml.dom.minidom.Document()
            self.data = self.doc.createElement('ax:' + tag)
            self.data.setAttribute('xmlns:xsi', AxServices.xsi)
            self.data.setAttribute('xmlns:xsd', AxServices.xsd)
            for key in attr:
                self.data.setAttribute(key, attr[key])
            self.data.setAttribute('xmlns:ax', AxServices.ax)
            self.doc.appendChild(self.data)

        def getData(self):
            return self.data

        def toXML(self):
            return self.doc.toxml('utf-8').decode()

    class AxDocCrtData(AxObject):
        def __init__(self, appid, dsn, filepath):
            attr = {}
            attr['dsn'] = dsn
            attr['appid'] = appid
            attr['filepath'] = filepath
            attr['filetype'] = 'FT_PDF'     # AxTypes.FileType
                                            #   FT_UNKNOWN = 0 / FT_Text = 1 / FT_CompressedText = 2
                                            #   FT_ForeignFile = 3 / FT_OLE = 4 / FT_RTF = 5
                                            #   FT_HTML = 6 / FT_PDF = 7 / FT_IMAGE = 8 / FT_Annotation = 255
            attr['ignore_dls'] = AxServices.true          # Indicates whether to ignore document-level security checking while saving index values
            attr['ignore_dup_index'] = AxServices.true    # Indicates whether to ignore duplicated indexes while saving index values
            attr['splitimg'] = AxServices.true            # Indicates whether to split multi-page image files such as PDF, TIFF, and text.
            attr['subpages'] = '0'          # Number of sub-pages in the image file
            super().__init__('AxDocCrtData', attr)

    class QueryItem(AxObject):
        def __init__(self):
            attr = {}
            attr['id'] = '-1'
            super().__init__('QueryItem', attr)
            data = super().getData()
            self.attributes = self.doc.createElement('ax:Attributes')
            self.fields = self.doc.createElement('ax:Fields')
            data.appendChild(self.attributes)
            data.appendChild(self.fields)

        def addAttribute(self):
            print('addAttribute')

        def addField(self, id, val, nul):
            field = self.doc.createElement('ax:Field')
            field.setAttribute('id', str(id))
            field.setAttribute('value', str(val))
            field.setAttribute('isnull', str(nul))
            self.fields.appendChild(field)

    class AxPageUploadData(AxObject):
        def __init__(self, filepath):
            attr = {}
            attr['act'] = 'InsertAfter'
            attr['filepath'] = filepath
            attr['filetype'] = 'FT_PDF'
            attr['position'] = '1'
            attr['splitimg'] = AxServices.true
            attr['subpages'] = '0'
            super().__init__('AxImgUploadData', attr)

    class AxStreamData(AxObject):
        def __init__(self, key, fname, data, startpos):
            print(startpos)
            self.attr = {}
            self.attr['encryption'] = AxServices.false
            self.attr['key'] = key
            self.attr['origFile'] = fname
            self.attr['startbyte'] = str(startpos)
            self.attr['len'] = str(AxServices.buffer_size)
            super().__init__('AxStreamData', self.attr)
            sdata = super().getData();
            sdata.setAttribute('ImageBytes', str(base64.b64encode(data)))

    class Request:
        header = {}
        action_dict = {'Login': 'Login',
                       'CreateNewDocument': 'CreateNewDocument',
                       'CreateBatchPage': 'CreateBatchPage',
                       'UploadImageStream': 'UploadImageStream',
                       'Logout':'Logout',
                       'UnlockDocumentByRef':'UnlockDocumentByRef'
                      }

        def __init__(self, data):
            self.header['Content-Type'] = 'text/xml; charset=utf-8'
            self.header['Host'] = 'xxxx.odu.edu'
            self.header['SOAPAction'] = "http://documentum.com/AX/WebServices" + '/' + self.action_dict[type(self).__name__]
            self.doc=''
            self.response=''
            self.setBody(data)

        def setBody(self, data):
            self.doc = xml.dom.minidom.Document()
            envelope = self.doc.createElement('soap:Envelope')
            envelope.setAttribute('xmlns:soap', 'http://schemas.xmlsoap.org/soap/envelope/')
            envelope.setAttribute('xmlns:xsi', AxServices.xsi)
            envelope.setAttribute('xmlns:xsd', AxServices.xsd)
            self.doc.appendChild(envelope)
            body = self.doc.createElement('soap:Body')
            envelope.appendChild(body)
            action = self.doc.createElement(self.action_dict[type(self).__name__])
            action.setAttribute('xmlns', 'http://documentum.com/AX/WebServices')
            body.appendChild(action)
            for key in data:
                ele = self.doc.createElement(key)
                ele.appendChild(self.doc.createTextNode(str(self.data[key])))
                action.appendChild(ele)
            return body

        def toXML(self):
            return self.doc.toxml('utf-8').decode()

        def toPXML(self):
            return self.doc.toprettyxml('  ')

        def getBody(self):
            return self.toXML()

        def post(self):
            self.response = requests.post(AxServices.url, headers=self.header, data=self.getBody())
            return self.response.status_code

        def getResponse(self):
            return self.response;

        def getResponseText(self):
            self.response.encoding = 'utf-8'
            return self.response.text;

        def getResponseBody(self):
            xml = self.getResponseText();
            dom = parseString(xml)
            return dom.getElementsByTagName("soap:Envelope")[0].getElementsByTagName("soap:Body")[0]

        def getResponseResult(self):
            return self.getResponseBody().firstChild.firstChild.firstChild.nodeValue

        def getEleByTagFromResponse(self, tag):
            retxml = self.getResponseResult();
            return xml.dom.minidom.parseString(retxml).getElementsByTagName(tag)

    class Login(Request):
        def __init__(self, ds, user, password):
            self.data = {'sessionTicket': '', 'dataSource': ds, 'userId': user, 'password': password, 'features': 1}
            super().__init__(self.data)

        def getSessionTicket(self):
            return super().getResponseResult()

    class Logout(Request):
        def __init__(self, ticket):
            self.data = {'sessionTicket': ticket}
            super().__init__(self.data)
        
    class CreateNewDocument(Request):
        def __init__(self, ticket, data, idx):
            self.data = {'sessionTicket': ticket, 'xmlAxDocumentCreationData': str(data), 'xmlDocIndex': str(idx)}
            super().__init__(self.data)

        def getDoc(self):
            return super().getEleByTagFromResponse('ax:Doc')[0]

        def getDocID(self):
            return self.getDoc().getAttribute('id')

        def getRef(self):
            return self.getDoc().getAttribute('objref')

    class UnlockDocumentByRef(Request):
        def __init__(self, ticket, ref):
            self.data = {'sessionTicket': ticket, 'docReference': ref}
            super().__init__(self.data)
        
    class CreateBatchPage(Request):
        def __init__(self, ticket, ds, appid, batchname, data):
            self.data = {'sessionTicket': ticket, 'dataSource': ds, 'appid': appid, 'batchname': batchname,  'xmlPageUploadData': str(data)}
            super().__init__(self.data)

    class UploadImageStream(Request):
        def __init__(self, ticket, ds, data):
            self.data = {'sessionTicket': ticket, 'dataSource': ds, 'xmlAxStreamData': str(data)}
            super().__init__(self.data)

        def getUploadKey(self):
            body = super().getResponseBody()
            return body.firstChild.firstChild.firstChild.nodeValue

    # functions
    def __init__(self):
        self.sessionTicket = ''
        self.dataSource = ''

    def login(self, ds, user, password):
        obj = self.Login(ds, user, password)
        ret = obj.post()
        if ret == 200:
            print("login successfully")
            self.sessionTicket = obj.getSessionTicket()
            self.dataSource = ds
        else:
            raise Exception(ret, obj.getResponseText())
        return self.sessionTicket

    def logout(self):
        obj = self.Logout(self.sessionTicket)
        ret = obj.post()
        if ret != 200:
            raise Exception(ret, obj.getResponseText())
    
    def getSessionTicket(self):
        return self.sessionTicket;

    def getDataSource(self):
        return self.dataSource;

    def createnewdocument(self, data, idx):
        obj = self.CreateNewDocument(self.sessionTicket, data.toXML(), idx.toXML())
        ret = obj.post()
        if ret == 200:
            print('success, doc id: ' + obj.getDocID() + ' ref: ' + obj.getRef())
            return obj.getRef()
        else:
            raise Exception(ret, obj.getResponseText())
        
    def unlockdocumentbyref(self, ref):
        obj = self.UnlockDocumentByRef(self.sessionTicket, ref)
        ret = obj.post()
        if ret != 200:
            raise Exception(ret, obj.getResponseText())
        
    def createbatchpage(self, appid, batchname, data):
        obj = self.CreateBatchPage(self.sessionTicket, self.dataSource, appid, batchname, data.toXML())
        ret = obj.post()
        if ret == 200:
            print('success')
        else:
            raise Exception(ret, obj.getResponseText())

    def uploadimagestream(self, data):
        obj = self.UploadImageStream(self.sessionTicket, self.dataSource, data.toXML())
        ret = obj.post()
        if ret == 200:
            return obj.getUploadKey()
        else:
            raise Exception(ret, obj.getResponseText())

    def uploadfile(self, filename):
        head, tail = os.path.split(filename)
        file = open(filename, 'rb', self.buffer_size)
        idx = 0
        key = ''
        while True:
            print(key)
            data = file.read(self.buffer_size)
            if not data:
                break
            sdata = srv.AxStreamData(key, tail, data, idx * self.buffer_size)
            key = srv.uploadimagestream(sdata)
            idx = idx+1

        return key

    def indexDocByFolder(self, appid, dir, tpath, fpath):
        failed = 0
        first = True;
        seq = 0;
        for file in os.listdir(dir):
            seq = seq + 1
            filePath = str(dir)+'/' + str(file)
            tgtPath = str(tpath) + '/' + str(file)
            faildPath = str(fpath) + '/' + str(file)
            if file.upper().endswith('.PDF'):
                # t = datetime.datetime.now()
                t = datetime.datetime.fromtimestamp(os.path.getmtime(filePath))
                id = str(appid) + '_' + str(file).replace(' ', '_').replace('.','_').replace('(','').replace(')','') + '_' + str(t.strftime('%Y%m%d%H%M%S')) + '_'  + str(seq)
                print("[START] "+ filePath + "," + id)
                data = srv.AxDocCrtData(str(appid), self.dataSource, filePath)
                idx = srv.QueryItem()
                idx.addField(1, id, AxServices.false)
                #idx.addField(2, t.strftime('%Y-%m-%d %H:%M:%S'), AxServices.false)
                idx.addField(2, t.strftime('%m-%d-%Y'), AxServices.false)
                try:
                    ref = srv.createnewdocument(data, idx)
                    srv.unlockdocumentbyref(ref)
                    os.rename(filePath, tgtPath)
                except Exception as e:
                    os.rename(os.rename(filePath, failedPath))
                    failed = failed + 1
                print("[END] " + filePath)
        print(str(seq) + " files processed, " + str(failed) + " failed.")
        return seq, failed        
                
if __name__ == "__main__":
    if len(sys.argv) != 5:
        print('Four Args, appid, source path, processed path, and failed path')
        exit(-1)
    appid = sys.argv[1]
    spath = sys.argv[2]
    tpath = sys.argv[3]
    fpath = sys.argv[4]

    failed = 0

    try:
        srv = AxServices()
        ticket = srv.login(datasource, user, password)
        processed, failed = srv.indexDocByFolder(appid, spath, tpath, fpath)
        srv.logout()
    
        if failed > 0:
            failed = -1
            raise Exception('400','login failed')
    except Exception as e:
      smtp = smtplib.SMTP('smtp.odu.edu')
      smtp.sendmail('dba-dist@odu.edu','dba-dist@odu.edu','[BDM Automation]: '+ str(failed) + ' failed.')

    # CreateNewDocument
    # data = srv.AxDocCrtData('509', srv.dataSource, 'c:/bdm_file/test.pdf')
    # idx = srv.QueryItem()
    # import random
    # idx.addField(1, str(random.randint(80000,90000)), AxServices.false)
    # idx.addField(2, '45237', AxServices.false)
    # idx.addField(3, 'REF4', AxServices.false)
    # idx.addField(14, 'UI', AxServices.false)
    # srv.createnewdocument(data, idx)

    # CreateBatchPage
    # data1 = srv.AxPageUploadData('c:/bdm_file/test.pdf')
    # print(data1.toXML())
    # data2 = srv.AxPageUploadData('c:/bdm_file/test.pdf')
    # srv.createbatchpage( '509', 'batch1', data1)

    # UploadImageStream
    # filekey = srv.uploadfile('C:/Users/q1zhang/Downloads/test2.pdf')
    # data = srv.AxDocCrtData('509', srv.dataSource, filekey)
    # idx = srv.QueryItem()
    # idx.addField(1, '78538', AxServices.false)
    # idx.addField(2, '45237', AxServices.false)
    # idx.addField(3, 'REF4', AxServices.false)
    # idx.addField(14, 'UI', AxServices.false)
    # srv.createnewdocument(data, idx)