From 70355b2257d4576aff65bb7f37166e116ab7b015 Mon Sep 17 00:00:00 2001 From: Ryan Miles Date: Wed, 25 Mar 2020 14:47:18 -0700 Subject: [PATCH 1/2] update regi headless to only work with CDA updates to the latest libraries and runs on Java 21 --- build.gradle | 10 +- .../regi-headless.deps-conventions.gradle | 16 - .../regi-headless.java-conventions.gradle | 3 +- gradle/libs.versions.toml | 20 +- regi-headless/build.gradle | 42 ++- .../usace/rowcps/headless/CLIOptions.java | 341 ------------------ .../headless/HeadlessRegiDomainFactory.java | 188 ++++------ .../usace/rowcps/headless/LoggingOptions.java | 2 - .../java/usace/rowcps/headless/RegiCLI.java | 65 ++-- .../ScriptableImportSigStagesImpl.java | 70 ++-- settings.gradle | 6 +- 11 files changed, 172 insertions(+), 591 deletions(-) delete mode 100644 regi-headless/src/main/java/usace/rowcps/headless/CLIOptions.java diff --git a/build.gradle b/build.gradle index e83d7ab..67c00f8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,18 +1,10 @@ plugins { - id "com.palantir.git-version" version "3.0.0" id "org.sonarqube" version "4.0.0.2929" } -def versionLabel(gitInfo) { - def branch = gitInfo.branchName // all branches are snapshots, only tags get released - def tag = gitInfo.lastTag - // tag is returned as is. Branch may need cleanup - return branch == null ? tag : "99." + branch.replace("/","-") + "-SNAPSHOT" -} - allprojects { group = 'mil.army.wmist.regi-headless' - version = versionLabel(versionDetails()) + version = project.findProperty('projectVersion') ?: "unversioned" } tasks.register('logTeamCityBuildStatus') { diff --git a/buildSrc/src/main/groovy/regi-headless.deps-conventions.gradle b/buildSrc/src/main/groovy/regi-headless.deps-conventions.gradle index 9e8db3d..260d3c0 100644 --- a/buildSrc/src/main/groovy/regi-headless.deps-conventions.gradle +++ b/buildSrc/src/main/groovy/regi-headless.deps-conventions.gradle @@ -1,24 +1,8 @@ -def checkForNexusCredentials() { - if(!project.hasProperty('nexusUser')) { - println ('Please set the nexusUser property in the GRADLE_USER_HOME ($userHome/.gradle/gradle.properties) file or via -PnexusUser= .') - } - if(!project.hasProperty('nexusPassword')) { - println ('Please set the nexusPassword property in the GRADLE_USER_HOME ($userHome/.gradle/gradle.properties) file or via -PnexusPassword= .') - } -} repositories { maven { url 'https://www.hec.usace.army.mil/nexus/repository/maven-public' } - maven { - url 'https://www.hec.usace.army.mil/nexus/repository/hec-internal' - credentials { - checkForNexusCredentials() - username "$nexusUser" - password "$nexusPassword" - } - } mavenCentral() } diff --git a/buildSrc/src/main/groovy/regi-headless.java-conventions.gradle b/buildSrc/src/main/groovy/regi-headless.java-conventions.gradle index 5b5d71d..58c965d 100644 --- a/buildSrc/src/main/groovy/regi-headless.java-conventions.gradle +++ b/buildSrc/src/main/groovy/regi-headless.java-conventions.gradle @@ -3,8 +3,7 @@ plugins { } compileJava { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + options.release = 21 } dependencies { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7c8dacb..39f8b1e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,14 +1,15 @@ [versions] # HEC Dependencies -service-annotations = "1.2.2" +service-annotations = "2.1.0" win-java-heclib = "7-IE-win-x64" solaris-java-heclib = "7-IE-Solaris64" -hec-core = "6.1-SNAPSHOT" # required for PasswordFileEditor +hec-monolith = "7.0.4" # required for PasswordFileEditor +hec-server-suite = "8.4.5" # REGI Dependencies -regi-tools = "3.4.3" -regi = "3.4.4" +regi-tools = "9.1.1" +regi = "3.5.0-alpha001" # Third Party jython-standalone = "2.7.2" @@ -26,9 +27,14 @@ junit = "5.9.3" [libraries] # HEC Dependencies service-annotations = { module = "mil.army.usace.hec:service-annotations", version.ref = "service-annotations" } -hec-core = { module="mil.army.usace.hec:hec-core", version.ref = "hec-core" } +hec-monolith = { module="mil.army.usace.hec:hec-monolith", version.ref = "hec-monolith" } win-java-heclib = {module="mil.army.usace.hec:javaHeclib", version.ref = "win-java-heclib"} solaris-java-heclib = {module="mil.army.usace.hec:javaHeclib", version.ref = "solaris-java-heclib"} +serversuite = { module = "mil.army.usace.hec:hec-server-suite", version.ref = "hec-server-suite" } +serversuite-cda = { module = "mil.army.usace.hec:cda-server-suite", version.ref = "hec-server-suite" } +serversuite-jdbc = { module = "mil.army.usace.hec:jdbc-server-suite", version.ref = "hec-server-suite" } +hec-db-cda = { module = "mil.army.usace.hec:hec-db-cda", version = "14.1.0" } +hec-cwms-ratings-cda = { module = "mil.army.usace.hec:hec-cwms-ratings-io-cda", version = "4.2.2"} # REGI Dependencies regi-basinpie-ui = {module = "mil.army.wmist.regi:basin-pie-ui", version.ref = "regi"} @@ -48,6 +54,7 @@ regi-tools-regi-cache-ui = {module = "mil.army.wmist.regi-tools:regi-cache-ui", # Third Party jython-standalone = {module = "org.python:jython-standalone", version.ref = "jython-standalone"} args4j = {module = "args4j:args4j", version.ref = "args4j"} +otel = {module = "io.opentelemetry:opentelemetry-api", version="1.58.0"} # Natives windows_jre = { module = "com.oracle:oracle-jre", version.ref = "windows-jre" } @@ -76,12 +83,13 @@ regi-tools = [ "regi-tools-regi-data" ] hec = [ - "hec-core" + "hec-monolith" ] sys = [ "args4j", "jython-standalone" ] +serversuite = ["serversuite", "serversuite-cda", "serversuite-jdbc", "hec-db-cda"] junit-api = ["junit-jupiter-api", "junit-jupiter-params", "junit4"] junit-engine = ["junit-jupiter-engine", "junit-vintage-engine"] \ No newline at end of file diff --git a/regi-headless/build.gradle b/regi-headless/build.gradle index e528b8c..2075ac4 100644 --- a/regi-headless/build.gradle +++ b/regi-headless/build.gradle @@ -1,24 +1,64 @@ plugins { id 'regi-headless.deps-conventions' id 'regi-headless.java-conventions' + id 'application' } dependencies { implementation(libs.bundles.hec) + implementation(libs.bundles.serversuite) implementation(libs.jython.standalone) implementation(libs.args4j) implementation(libs.bundles.regi) implementation(libs.bundles.regi) {artifact {extension = "jar"}} implementation(libs.bundles.regi.tools) implementation(libs.bundles.regi.tools) {artifact {extension = "jar"} } + implementation(libs.otel) + runtimeOnly(libs.hec.cwms.ratings.cda) testImplementation(libs.bundles.junit.api) testRuntimeOnly(libs.bundles.junit.engine) } +configurations.configureEach { + exclude group: 'mil.army.usace.hec', module: 'hec-cwmsvue' + exclude group: 'mil.army.usace.hec', module: 'hec-gt-crs' + exclude group: 'org.openjfx', module: '*' + exclude group: 'mil.army.usace.hec.swingx', module: '*' + exclude group: 'org.swinglabs', module: '*' + exclude group: 'org.jfree', module: '*' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + + jar { manifest { attributes('Implementation-Version': project.version) } -} \ No newline at end of file +} + +application { + mainClass = 'usace.rowcps.headless.RegiCLI' +} + +tasks.named('run') { + environment 'CDA_URL', project.findProperty('CDA_URL') + environment 'API_KEY', project.findProperty('API_KEY') + environment 'OFFICE_ID', project.findProperty('OFFICE_ID') + environment 'SCRIPT', project.findProperty('SCRIPT') +} + +distributions { + main { + contents { + exclude "**/*.nbm" + } + } +} + diff --git a/regi-headless/src/main/java/usace/rowcps/headless/CLIOptions.java b/regi-headless/src/main/java/usace/rowcps/headless/CLIOptions.java deleted file mode 100644 index bf6a1f6..0000000 --- a/regi-headless/src/main/java/usace/rowcps/headless/CLIOptions.java +++ /dev/null @@ -1,341 +0,0 @@ -package usace.rowcps.headless; - -import hec.lang.PasswordFileEntry; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.TimeZone; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.kohsuke.args4j.CmdLineException; -import org.kohsuke.args4j.Option; -import rma.services.ServiceLookup; -import rma.services.tz.TimeZoneDisplayService; - -/** - * - * @author ryan - */ -public class CLIOptions -{ - private static final Logger logger = Logger.getLogger(CLIOptions.class.getName()); - //public String rowcpsTimezone; - //public String oracleUrl; - //public String oracleUser; - //public String oraclePassword; - //private Map properties = new HashMap(); - Properties props; - - public final static String URL = "oracle.url"; - public final static String USER = "oracle.user"; - public final static String PASSWORD = "oracle.password"; - public final static String OFFICEID = "oracle.officeId"; - - public final static String TIMEZONE = "rowcps.timezone"; - public final static String PROJ_DIR = "rowcps.projectDir"; - public final static String PROJ_NAME = "rowcps.projectName"; - - public final static String HEC_PASSWD_FILE = "hec.passwd"; - - public CLIOptions() - { - this(System.getProperties()); - } - - public CLIOptions(Properties defaultProperties) - { - props = new Properties(defaultProperties); - } - - @Option(name = "-D", metaVar = "=", usage = "use value for given property") - private void setProperty(final String property) throws CmdLineException - { - String[] arr = property.split("="); - setProperty(arr); - } - - public void setProperty(String[] arr) throws CmdLineException - { - if (arr.length != 2) { - throw new CmdLineException("Properties must be specified in the form:" + - "="); - } - props.setProperty(arr[0], arr[1]); - //properties.put(arr[0], arr[1]); - } - - public Object getProperty(String key) - { - return props.get(key); - } - - /** - * @return the rowcpsTimezone - */ - public String getRowcpsTimezone() - { - return props.getProperty(TIMEZONE); - //return rowcpsTimezone; - } - - public File getRowcpsProjectDir() - { - File retval = null; - - String path = props.getProperty(PROJ_DIR); - if(path != null){ - retval = new File(path); - } - - return retval; - } - - @Option(name = "-D" + PROJ_DIR, metaVar = "", usage = "directory containing Regi project") - public void setRowcpsProjectDir(String filepath) - { - props.setProperty(PROJ_DIR, filepath); - } - - @Option(name = "-D" + HEC_PASSWD_FILE, metaVar = "", usage = "directory containing Regi project") - public void setHecPasswordFilepath(String filepath) - { - props.setProperty(HEC_PASSWD_FILE, filepath); - } - - public String getHecPasswordFilepath(){ - return props.getProperty(HEC_PASSWD_FILE); - } - - public PasswordFileEntry getHecPasswordFileEntry() - { - PasswordFileEntry retval = null; - - // String office = System.getProperty("cwms.dbi.OfficeId"); - String dburl = getOracleUrl(); - if (dburl != null && !dburl.isEmpty()) { - String instance = dburl; - - int idx = dburl.indexOf("@"); - if (idx != -1) { - instance = dburl.substring(idx + 1); - } - - hec.io.PasswordFile passwordFile = null; - try { - String filePath = getHecPasswordFilepath(); - passwordFile = new hec.io.PasswordFile(filePath, false); - retval = passwordFile.getEntry(instance); - - if (retval == null) { - /* - * System.out.println( - * "getConnectionInfo: Failed to find Password Entry for instance " - * + instance); - */ - logger.severe("getConnectionInfo: Failed to find Password Entry for instance " + instance); - - } -// // _connectionInfo = new -// // ConnectionInfo(office,dburl,entry.getUserName(),entry.getPassword()); -// _connectionLoginInfo = new ConnectionLoginInfoImpl(dburl, entry.getUserName(), entry.getPassword(), -// getOfficeId()); - } catch (java.io.IOException ioe) { - /* - * System.out.println( - * "getConnectionInfo: Error reading password file " + ioe); - */ - logger.severe("getConnectionInfo: Error reading password file " + ioe); - - } finally { - if (passwordFile != null) { - passwordFile.close(); - } - } - } - - return retval; - } - - - public String getRowcpsProjectName() - { - return props.getProperty(PROJ_NAME); - } - - @Option(name = "-D" + PROJ_NAME, usage = "name of Regi project") - public void setRowcpsProjectName(String name) - { - props.setProperty(PROJ_NAME, name); - } - - public String getOracleOfficeId() - { - return props.getProperty(OFFICEID); - } - - @Option(name = "-D" + OFFICEID, usage = "office id") - public void setOracleOfficeId(String id) - { - props.setProperty(OFFICEID, id); - } - - /** - * @param rowcpsTimezone the rowcpsTimezone to set - */ - @Option(name = "-D" + TIMEZONE) - public void setRowcpsTimezone(String rowcpsTimezone) throws CmdLineException - { - setProperty(new String[]{TIMEZONE, rowcpsTimezone}); - TimeZone timeZone = TimeZone.getTimeZone(rowcpsTimezone); - if(timeZone == null) - { - timeZone = TimeZone.getDefault(); - Logger.getLogger(CLIOptions.class.getName()).log(Level.WARNING, "Attempted to set invalid time zone to Regi Domain: "+rowcpsTimezone); - } - TimeZoneDisplayService timeZoneDisplayService = ServiceLookup.getTimeZoneDisplayService(); - timeZoneDisplayService.setTimeZone(timeZone); - } - - /** - * @return the oracleUrl - */ - public String getOracleUrl() - { - return props.getProperty(URL); - //return oracleUrl; - } - - /** - * @param oracleUrl the oracleUrl to set - */ - @Option(name = "-D" + URL) - public void setOracleUrl(String oracleUrl) throws CmdLineException - { - //this.oracleUrl = oracleUrl; - setProperty(new String[]{URL, oracleUrl}); - } - - /** - * @return the oracleUser - */ - public String getOracleUser() - { - String user = props.getProperty(USER); - - if (user == null) { - - PasswordFileEntry entry = getHecPasswordFileEntry(); - if (entry != null) { - user = entry.getUserName(); - } - - } - return user; - //return oracleUser; - } - - /** - * @param oracleUser the oracleUser to set - */ - @Option(name = "-D" + USER) - public void setOracleUser(String oracleUser) throws CmdLineException - { - //this.oracleUser = oracleUser; - setProperty(new String[]{USER, oracleUser}); - } - - /** - * @return the oraclePassword - */ - public char[] getOraclePassword() - { - char[] pass = null; - //return oraclePassword; - String passStr = props.getProperty(PASSWORD); - if (passStr != null) { - pass = passStr.toCharArray(); - } else { - PasswordFileEntry entry = getHecPasswordFileEntry(); - if(entry != null){ - pass = entry.getPassword().toCharArray(); - } - } - - return pass; - } - - /** - * @param oraclePassword the oraclePassword to set - */ - @Option(name = "-D" + PASSWORD) - public void setOraclePassword(String oraclePassword) throws CmdLineException - { - setProperty(new String[]{PASSWORD, oraclePassword}); - //this.oraclePassword = oraclePassword; - } - - @Option(name = "-p", aliases = {"-properties"}, metaVar = "", - usage = "import properties from given file") - public void importProperties(File file) - { - if (file != null && file.exists()) { - Properties fileProps = new Properties(); - - try (BufferedReader br = new BufferedReader(new FileReader(file))) { - fileProps.load(br); - - Set> entrySet = fileProps.entrySet(); - for (Map.Entry entry : entrySet) { - Object keyObj = entry.getKey(); - Object valueObj = entry.getValue(); - - if (keyObj != null && valueObj != null) { - props.put(keyObj, valueObj); - } - } - } catch (FileNotFoundException ex) { - Logger.getLogger(CLIOptions.class.getName()).log(Level.SEVERE, null, ex); - } catch (IOException ex) { - Logger.getLogger(CLIOptions.class.getName()).log(Level.SEVERE, null, ex); - } - } - else{ - Logger.getLogger(CLIOptions.class.getName()).log(Level.SEVERE, "Unable to find credentials file at: "+(file ==null ? "null" : file)); - } - } - - @Option(name = "-f", aliases = {"-file"}, metaVar = "", - usage = "script file to execute") - public void setScriptFile(File file) - { - props.setProperty("script", file.getAbsolutePath()); - } - - public String getScriptPath() - { - return props.getProperty("script"); - } - - public File getScriptFile() - { - File retval = null; - String scriptPath = getScriptPath(); - if (scriptPath != null && !scriptPath.isEmpty()) { - File afile = new File(scriptPath); - if (afile.exists()) { - retval = afile; - } - } - return retval; - } - - Properties getProperties() { - return new Properties(props); - } - -} diff --git a/regi-headless/src/main/java/usace/rowcps/headless/HeadlessRegiDomainFactory.java b/regi-headless/src/main/java/usace/rowcps/headless/HeadlessRegiDomainFactory.java index f631bf0..2ecf3fb 100644 --- a/regi-headless/src/main/java/usace/rowcps/headless/HeadlessRegiDomainFactory.java +++ b/regi-headless/src/main/java/usace/rowcps/headless/HeadlessRegiDomainFactory.java @@ -1,154 +1,102 @@ package usace.rowcps.headless; -import com.rma.io.FileManager; -import com.rma.io.FileManagerImpl; import com.rma.io.RmaFile; -import com.rma.model.Manager; import com.rma.model.Project; +import hec.db.DataAccessFactory; import hec.db.DbConnectionException; +import hec.db.DbIoException; import hec.db.DbPluginNotFoundException; -import hec.db.InvalidDbConnectionException; -import hec.io.Identifier; +import hec.db.cwms.CwmsSecurityDao; import hec.lang.LoginException; +import hec.serversuite.ServerSuite; import hec.serversuite.ServerSuiteUtil; -import hec.serversuite.data.DirectOracleAuthenticationSource; -import java.io.File; -import java.util.List; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.logging.Level; import java.util.logging.Logger; +import mil.army.usace.hec.serversuite.cda.CdaAuthenticationSource; +import mil.army.usace.hec.serversuite.cda.CwmsApiKeyAuthExtension; import rma.services.ServiceLookup; import rma.services.tz.TimeZoneDisplayService; +import usace.rowcps.regi.executor.ManagerIdType; import usace.rowcps.regi.factories.RegiDomainFactory; import usace.rowcps.regi.interfaces.model.ManagerIdProvider; import usace.rowcps.regi.model.DatabaseConnectionManager; import usace.rowcps.regi.model.ManagerId; -import usace.rowcps.regi.executor.ManagerIdType; - import usace.rowcps.regi.model.RegiDomain; -/** - * - * @author ryan - */ public class HeadlessRegiDomainFactory { private static final Logger logger = Logger.getLogger(HeadlessRegiDomainFactory.class.getName()); + private ManagerIdProvider idProvider = buildNewProvider(); - public void setPluginsDirFromClasspath() - { - String cp = System.getProperties().getProperty("java.class.path"); - String[] split = cp.split(File.pathSeparator); - final String dbiClientjar = "dbiClient-v3.1.1.jar"; - for (String cpentry : split) { - if (cpentry.endsWith(dbiClientjar)) { - String pluginDir = cpentry.split(dbiClientjar)[0]; - logger.log(Level.INFO, "Setting plugin dir to: {0}", pluginDir); - System.setProperty("PLUGINS", pluginDir); - } - } - } - - public RegiDomain createDomain(CLIOptions options, ManagerId managerId) throws DbConnectionException, - DbPluginNotFoundException, InvalidDbConnectionException - { - RegiDomain regiDomain = null; - - setPluginsDirFromClasspath(); - File rowcpsPojectDir = options.getRowcpsProjectDir(); - - String rowcpsProjectName = options.getRowcpsProjectName(); - logger.log(Level.INFO, "Creating project dir: "+ (rowcpsPojectDir == null ? "null" : rowcpsPojectDir), rowcpsProjectName == null ? "null" : rowcpsProjectName); - File projectDir = new File(rowcpsPojectDir, rowcpsProjectName); - - if (projectDir == null) { - String missingProjDirMessage - = "A Rowcps Project Dir is required and must be specified on the command line or in a properties file."; - throw new IllegalArgumentException(missingProjDirMessage); - } else { - if (!projectDir.exists()) { - // If we are being run headlessly I'm not sure how much hand-holding and sanity checking we have to do. - projectDir.mkdirs(); - if (!projectDir.exists()) { - throw new IllegalArgumentException("The directory " + projectDir.getAbsolutePath() + - " did not exist and could not be created."); - } - } - - String testProjDir = projectDir.getAbsolutePath(); - FileManager fileManager = FileManagerImpl.getFileManager(); - final String projectFilePath = testProjDir + "/" + options.getRowcpsProjectName() + ".prj"; - - RmaFile prjFile; - if (!fileManager.fileExists(projectFilePath)) { - final Identifier identifier = new Identifier(projectFilePath); - Identifier prjId = fileManager.createFile(identifier); - prjFile = fileManager.getFile(prjId.getPath()); - } else { - prjFile = fileManager.getFile(projectFilePath); - } - - logger.log(Level.INFO, "Temp project file: " + prjFile.getAbsolutePath()); + public RegiDomain createDomain() throws DbConnectionException, + DbPluginNotFoundException, IOException { - File projReportsDir = new File(projectDir, "reports"); - File projXmlDir = new File(projectDir, "xml"); - projReportsDir.mkdir(); - projXmlDir.mkdir(); + Path projectDir = Paths.get("regi-projects", "regi-cli"); + logger.log(Level.INFO, "Creating project dir: "+ projectDir); + Files.createDirectories(projectDir); - String name = "Headless"; - String description = "Created for Headless execution."; - regiDomain = new RegiDomainFactory().createProject(name, description, prjFile); - - regiDomain.loadProjectFile(); - - DatabaseConnectionManager connectionManager = (DatabaseConnectionManager) regiDomain.getManager( - RegiDomain.DOMAIN_CONNECTION_MANAGER, DatabaseConnectionManager.class); - - if (connectionManager == null) { - connectionManager = regiDomain.buildDatabaseConnectionManager(); - } - - //conigure the database connection. - String dbUrl = options.getOracleUrl(); - String username = options.getOracleUser(); - char[] password = options.getOraclePassword(); - String tzId = options.getRowcpsTimezone(); - String officeId = options.getOracleOfficeId(); - - DirectOracleAuthenticationSource directOracleAuthenticationSource = new DirectOracleAuthenticationSource("", dbUrl, officeId); - connectionManager.setTimeZoneId(tzId); - connectionManager.setUsername(username); - connectionManager.setUserOfficeId(officeId); - connectionManager.saveData(); - - TimeZoneDisplayService tsDS = ServiceLookup.getTimeZoneDisplayService(); - tsDS.setTimeZone(connectionManager.getTimeZone()); - try - { - ServerSuiteUtil.login("REGI Headless", directOracleAuthenticationSource, username, password); - } - catch(LoginException ex) - { - throw new DbConnectionException(ex); - } - - regiDomain.connect(ServerSuiteUtil.getServerSuite()); - List managerList = regiDomain.getManagerList(); - - regiDomain.saveProject(); - Project.setCurrentProject(regiDomain); + Path projectFile = projectDir.resolve("regi-cli.prj"); + if(!Files.exists(projectFile)) { + Files.createFile(projectFile); } - return regiDomain; + + Files.createDirectories(projectDir.resolve("reports")); + Files.createDirectories(projectDir.resolve("xml")); + + String name = "Headless"; + String description = "Created for Headless execution."; + RegiDomain regiDomain = new RegiDomainFactory().createProject(name, description, new RmaFile(projectFile.toAbsolutePath().toString())); + + regiDomain.loadProjectFile(); + + DatabaseConnectionManager connectionManager = (DatabaseConnectionManager) regiDomain.getManager( + RegiDomain.DOMAIN_CONNECTION_MANAGER, DatabaseConnectionManager.class); + + if (connectionManager == null) { + connectionManager = regiDomain.buildDatabaseConnectionManager(); + } + + String cdaUrl = System.getenv("CDA_URL"); + String apiKey = System.getenv("API_KEY"); + String officeId = System.getenv("OFFICE_ID"); + + CdaAuthenticationSource cdaAuthenticationSource = new CdaAuthenticationSource("", cdaUrl, officeId, new CwmsApiKeyAuthExtension(apiKey)); + try + { + ServerSuite serverSuite = ServerSuiteUtil.login("REGI CLI", cdaAuthenticationSource, false, false, false); + DataAccessFactory dataAccessFactory = serverSuite.getDataAccessFactory(); + try(var key = dataAccessFactory.getDataAccessKey("REGI CLI")) { + String username = dataAccessFactory.getDao(CwmsSecurityDao.class).getCurrentUserId(key); + connectionManager.setUsername(username); + } + connectionManager.setTimeZoneId("UTC"); + connectionManager.setUserOfficeId(officeId); + connectionManager.saveData(); + TimeZoneDisplayService tsDS = ServiceLookup.getTimeZoneDisplayService(); + tsDS.setTimeZone(connectionManager.getTimeZone()); + regiDomain.connect(ServerSuiteUtil.getServerSuite()); + regiDomain.getManagerList(); + regiDomain.saveProject(); + RegiDomain.setCurrentProject(regiDomain); + Project.setCurrentProject(regiDomain); + return regiDomain; + } + catch(LoginException | DbIoException ex) + { + throw new DbConnectionException(ex); + } } - public ManagerId getManagerId(CLIOptions opt) + public ManagerId getManagerId() { - ManagerId retval = idProvider.getManagerId(); - - return retval; + return idProvider.getManagerId(); } - private ManagerIdProvider idProvider = buildNewProvider(); private static ManagerIdProvider buildNewProvider() { diff --git a/regi-headless/src/main/java/usace/rowcps/headless/LoggingOptions.java b/regi-headless/src/main/java/usace/rowcps/headless/LoggingOptions.java index c955c60..9797122 100644 --- a/regi-headless/src/main/java/usace/rowcps/headless/LoggingOptions.java +++ b/regi-headless/src/main/java/usace/rowcps/headless/LoggingOptions.java @@ -9,7 +9,6 @@ import java.util.logging.Logger; import usace.rowcps.headless.metrics.RegiHeadlessMetricsServiceProvider; import usace.rowcps.metrics.RegiMetricsService; -import wcds.dbi.DbiProperties; /** * @@ -42,7 +41,6 @@ private LoggingOptions() */ public static void setDbMessageLevel(int messageLevel) { - DbiProperties.setMessageLevel(messageLevel); LOGGER.log(Level.FINE, "Setting Db Message Level to: {0}", messageLevel); } diff --git a/regi-headless/src/main/java/usace/rowcps/headless/RegiCLI.java b/regi-headless/src/main/java/usace/rowcps/headless/RegiCLI.java index b4dc93f..d6317df 100644 --- a/regi-headless/src/main/java/usace/rowcps/headless/RegiCLI.java +++ b/regi-headless/src/main/java/usace/rowcps/headless/RegiCLI.java @@ -1,13 +1,17 @@ package usace.rowcps.headless; -import usace.rowcps.headless.interfaces.ScriptEvaluator; import hec.db.DbConnectionException; import hec.db.DbIoException; import hec.db.DbPluginNotFoundException; import hec.db.InvalidDbConnectionException; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Scope; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; +import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -15,50 +19,33 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.kohsuke.args4j.CmdLineException; -import org.kohsuke.args4j.CmdLineParser; -import usace.rowcps.metrics.RegiMetricsService; +import usace.rowcps.headless.interfaces.ScriptEvaluator; import usace.rowcps.regi.factories.RowcpsExecutorService; import usace.rowcps.regi.model.ManagerId; import usace.rowcps.regi.model.RegiDomain; -import usace.rowcps.regi.preferences.RegiPreferences; -/** - * - * @author ryan - */ public class RegiCLI { private static final Logger LOGGER = Logger.getLogger(RegiCLI.class.getName()); - static - { - RegiMetricsService.init(RegiPreferences.getClientNode().node("Metrics"), "REGI Headless"); - } - public static void main(String[] args) { - CLIOptions opt = new CLIOptions(System.getProperties()); - CmdLineParser parser = new CmdLineParser(opt); - - try + Span rootSpan = GlobalOpenTelemetry.getTracer("regi-headless") + .spanBuilder("runHeadless") + .startSpan(); + try(Scope scope = rootSpan.makeCurrent()) { - runHeadless(parser, args, opt); + runHeadlessTest(args); } - catch (DbConnectionException | DbPluginNotFoundException | InvalidDbConnectionException ex) + catch (DbConnectionException | DbPluginNotFoundException | IOException | RuntimeException ex) { + rootSpan.recordException(ex); + rootSpan.setStatus(StatusCode.ERROR); LOGGER.log(Level.SEVERE, "Headless error connecting to database.", ex); System.exit(-1); return; } - catch (CmdLineException | RuntimeException e) - { - LOGGER.log(Level.SEVERE, "Error running headless", e); - System.err.println("java -jar myprogram.jar [options...] arguments..."); - parser.printUsage(System.err); - System.exit(-1); - return; - } LOGGER.info("Exiting."); System.exit(0); @@ -67,37 +54,29 @@ public static void main(String[] args) /** * Used by TestHeadless unit test class to run headless without calling System.exit(0) * - * @param args + * @param unused * @throws DbConnectionException * @throws InvalidDbConnectionException * @throws CmdLineException * @throws DbPluginNotFoundException */ - static void runHeadlessTest(String[] args) throws DbConnectionException, InvalidDbConnectionException, CmdLineException, DbPluginNotFoundException - { - CLIOptions opt = new CLIOptions(System.getProperties()); - CmdLineParser parser = new CmdLineParser(opt); - runHeadless(parser, args, opt); - } - - private static void runHeadless(CmdLineParser parser, String[] args, CLIOptions opt) throws DbConnectionException, InvalidDbConnectionException, CmdLineException, DbPluginNotFoundException - { - parser.parseArgument(args); - System.setProperties(opt.getProperties()); + static void runHeadlessTest(String[] unused) + throws DbConnectionException, DbPluginNotFoundException, + IOException { HeadlessRegiDomainFactory factory = new HeadlessRegiDomainFactory(); - ManagerId managerId = factory.getManagerId(opt); - RegiDomain regiDomain = factory.createDomain(opt, managerId); + RegiDomain regiDomain = factory.createDomain(); if (regiDomain != null) { ScriptEvaluator pe = new PythonEvaluator(); Map vars = new HashMap<>(); - + + ManagerId managerId = factory.getManagerId(); RegiCalcRegistry reg = new RegiCalcRegistry(regiDomain, managerId); vars.put("registry", reg); - File scriptFile = opt.getScriptFile(); + File scriptFile = new File(System.getenv("SCRIPT")); try { diff --git a/regi-headless/src/main/java/usace/rowcps/headless/sigstages/importdb/ScriptableImportSigStagesImpl.java b/regi-headless/src/main/java/usace/rowcps/headless/sigstages/importdb/ScriptableImportSigStagesImpl.java index 14ae0d6..b37ea76 100644 --- a/regi-headless/src/main/java/usace/rowcps/headless/sigstages/importdb/ScriptableImportSigStagesImpl.java +++ b/regi-headless/src/main/java/usace/rowcps/headless/sigstages/importdb/ScriptableImportSigStagesImpl.java @@ -10,8 +10,6 @@ import hec.db.DbIoException; import java.nio.file.Path; import java.nio.file.Paths; -import java.sql.Connection; -import java.sql.SQLException; import java.util.Date; import java.util.List; import java.util.logging.Level; @@ -19,8 +17,8 @@ import usace.rowcps.headless.interfaces.ScriptableCalc; import usace.rowcps.regi.level.importer.CSVLocationLevelImportUtil; import usace.rowcps.regi.model.AtLocationLevelManager; -import usace.rowcps.regi.model.RegiDomain; import usace.rowcps.regi.model.ManagerId; +import usace.rowcps.regi.model.RegiDomain; /** * @@ -28,12 +26,8 @@ */ public class ScriptableImportSigStagesImpl implements ScriptableImportSigstages, ScriptableCalc { - private RegiDomain _regiDomain; - private ManagerId _managerId; - - public static final String DELIMETER = System.getProperty("sigstages.delim", ";"); - public static final String CSVDELIMETER = System.getProperty("sigstages.csv.delim", "\n"); - public static final String CSVSEPARATOR = System.getProperty("sigstages.csv.separator", ","); + private final RegiDomain _regiDomain; + private final ManagerId _managerId; public ScriptableImportSigStagesImpl(RegiDomain regiDomain, ManagerId managerId) { @@ -46,48 +40,28 @@ public boolean importSigStages(String file, Date effectiveDate) { boolean retval = true; AtLocationLevelManager atLocLevelMgr = _regiDomain.getAtLocationLevelManager(_managerId); - Connection c = null; - try { - Path p = Paths.get(file); - c = atLocLevelMgr.getPooledConnection(); - CSVLocationLevelImportUtil importUtil = new CSVLocationLevelImportUtil(); - importUtil.setPath(p); - importUtil.readFile(true); - List locationLevels = importUtil.getLocationLevelList(); - for(int locationLevelIndex = 0; locationLevelIndex < locationLevels.size(); locationLevelIndex++) - { - ILocationLevel level = locationLevels.get(locationLevelIndex); - try { - level.setDate(effectiveDate); - atLocLevelMgr.addLocationLevel(level); - } catch (DbConnectionException | DbIoException ex) { - Logger.getLogger(ScriptableImportSigStagesImpl.class.getName()).log(Level.SEVERE, null, ex); - retval = false; - break; - } - } - - if(retval) - { - try { - atLocLevelMgr.commitData(); - } catch (DbConnectionException | DbIoException ex) { - Logger.getLogger(ScriptableImportSigStagesImpl.class.getName()).log(Level.SEVERE, null, ex); - retval = false; - } + Path p = Paths.get(file); + CSVLocationLevelImportUtil importUtil = new CSVLocationLevelImportUtil(); + importUtil.setPath(p); + importUtil.readFile(true); + List locationLevels = importUtil.getLocationLevelList(); + for (ILocationLevel level : locationLevels) { + try { + level.setDate(effectiveDate); + atLocLevelMgr.addLocationLevel(level); + } catch (DbConnectionException | DbIoException ex) { + Logger.getLogger(ScriptableImportSigStagesImpl.class.getName()).log(Level.SEVERE, null, ex); + retval = false; + break; } - } catch (DbConnectionException ex) { - Logger.getLogger(ScriptableImportSigStagesImpl.class.getName()).log(Level.SEVERE, null, ex); } - finally - { - try - { - if(c != null && !c.isClosed()) c.close(); - } - catch (SQLException ex) - { + + if (retval) { + try { + atLocLevelMgr.commitData(); + } catch (DbConnectionException | DbIoException ex) { Logger.getLogger(ScriptableImportSigStagesImpl.class.getName()).log(Level.SEVERE, null, ex); + retval = false; } } return retval; diff --git a/settings.gradle b/settings.gradle index a5682e7..b97cc54 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,6 @@ rootProject.name = 'regi-headless-parent' include "regi-headless" -include "regi-headless-installer-common" -include "regi-headless-installer-windows" -include "regi-headless-installer-solaris" \ No newline at end of file +//include "regi-headless-installer-common" +//include "regi-headless-installer-windows" +//include "regi-headless-installer-solaris" \ No newline at end of file From 7079477d481482b0c32e3d02f8ea8593a928bb8d Mon Sep 17 00:00:00 2001 From: Adam Korynta Date: Tue, 13 Jan 2026 16:18:17 -0800 Subject: [PATCH 2/2] REGI-425 add jpype Python launcher for headless REGI execution this changes execution from using RegiCLI main to Jpype for orchestration instead used a modified GateFlowCalc2_Jpype.py example script that runs all flow calcuations for SWT's TENK reservoir add example of how we could update the CWBI docker image to run headless add Dockerfile build for regi headless utilizes gradle application plugins installDist for bundling utilizes opentelemetry -> jaeger, loki, prometheus -> grafana for observability in docker-compose does not use the cwbi-wm-images method of jpype. this retains the existing jython scripts. this may be an initial cut before moving onto that TODO: Dockerfile does not pin any versions TODO: Would want to expand to include CDA into the docker-compose rather than relying on an external service TODO: only tested flow computations for SWT's TENK reservoir. Did not do an analysis of coverage --- Dockerfile | 41 +++++++ Dockerfile-cwbi-copy.dockerfile | 30 +++++ build.gradle | 2 +- cwbi-docker/entrypoint.sh | 57 ++++++++++ cwbi-docker/requirements.txt | 52 +++++++++ datasources.yaml | 16 +++ docker-compose-cwbi.yml | 66 +++++++++++ docker-compose.yml | 64 +++++++++++ gradle.properties | 11 ++ gradle/docs/jpype.png | Bin 0 -> 57922 bytes gradle/docs/jpype.puml | 34 ++++++ otel-config.yaml | 36 ++++++ prometheus.yaml | 4 + regi-headless/build.gradle | 74 ++++++++++++ regi-headless/src/main/python/pyproject.toml | 17 +++ .../src/main/python/regi_cli/__init__.py | 10 ++ .../src/main/python/regi_cli/regi_cli.py | 107 ++++++++++++++++++ .../rowcps/headless/examples/GateFlowCalc2.py | 6 +- .../headless/examples/GateFlowCalc2_Jpype.py | 44 +++++++ 19 files changed, 667 insertions(+), 4 deletions(-) create mode 100644 Dockerfile create mode 100644 Dockerfile-cwbi-copy.dockerfile create mode 100644 cwbi-docker/entrypoint.sh create mode 100644 cwbi-docker/requirements.txt create mode 100644 datasources.yaml create mode 100644 docker-compose-cwbi.yml create mode 100644 docker-compose.yml create mode 100644 gradle.properties create mode 100644 gradle/docs/jpype.png create mode 100644 gradle/docs/jpype.puml create mode 100644 otel-config.yaml create mode 100644 prometheus.yaml create mode 100644 regi-headless/src/main/python/pyproject.toml create mode 100644 regi-headless/src/main/python/regi_cli/__init__.py create mode 100644 regi-headless/src/main/python/regi_cli/regi_cli.py create mode 100644 regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2_Jpype.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c209c7c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +FROM eclipse-temurin:21-jdk AS build +WORKDIR /home/gradle/project + +# 1. Copy Gradle wrapper and configuration files first. +# This includes the 'gradle' directory with the wrapper and libs.versions.toml +COPY gradlew . +COPY gradle gradle +COPY build.gradle settings.gradle ./ + +COPY regi-headless/build.gradle regi-headless/ +COPY buildSrc buildSrc + +RUN sed -i 's/\r$//' gradlew && chmod +x gradlew + +# 2. Pre-download dependencies. +# This layer is cached until you change your version catalog or build scripts. +RUN ./gradlew :regi-headless:dependencies --no-daemon + +# 3. Copy the actual source code and build the distribution +COPY . . +RUN sed -i 's/\r$//' gradlew && chmod +x gradlew +RUN ./gradlew :regi-headless:installDist --no-daemon + +# Download the OpenTelemetry Java Agent +ADD https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar /home/gradle/project/opentelemetry-javaagent.jar + +FROM eclipse-temurin:21-jre-alpine +WORKDIR /app + +# Copy the application and the OTel agent +COPY --from=build /home/gradle/project/regi-headless/build/install/regi-headless ./ +COPY --from=build /home/gradle/project/opentelemetry-javaagent.jar ./ + +# AWS Batch/Lambda often prefer non-root, and it's better for OTel file permissions +RUN addgroup -S appgroup && adduser -S appuser -G appgroup +RUN chown -R appuser:appgroup /app +USER appuser + +RUN chmod +x /app/bin/regi-headless + +ENTRYPOINT ["/app/bin/regi-headless"] diff --git a/Dockerfile-cwbi-copy.dockerfile b/Dockerfile-cwbi-copy.dockerfile new file mode 100644 index 0000000..01fc536 --- /dev/null +++ b/Dockerfile-cwbi-copy.dockerfile @@ -0,0 +1,30 @@ +FROM ghcr.io/usace/usace-wm-python:3.11 + +USER root + +RUN apk update && apk add --no-cache git openjdk-23-jre \ + && mkdir -p /jobs \ + && chown appuser:appuser /jobs + +# Set JAVA_HOME to the directory of Java 23 +ENV JAVA_HOME=/usr/lib/jvm/java-23-openjdk + +# Add Java to the PATH so it's available globally +# Add python bin for python command line tools like cwms-cli +ENV PATH="$JAVA_HOME/bin:/appuser/.local/bin:$PATH" + +RUN mkdir -p /jobs && chown appuser:appuser /jobs + +COPY --chown=appuser:appuser cwbi-docker/entrypoint.sh /entrypoint.sh +COPY --chown=appuser:appuser cwbi-docker/requirements.txt /requirements.txt + +# Set the user to the non-root user +USER appuser + +RUN chmod +x /entrypoint.sh && \ + pip install --no-cache-dir -r /requirements.txt + +ENTRYPOINT [ "/entrypoint.sh" ] + +# CMD ["sleep", "infinity"] +CMD ["/jobs/bin/daily.sh"] \ No newline at end of file diff --git a/build.gradle b/build.gradle index 67c00f8..ec60644 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { allprojects { group = 'mil.army.wmist.regi-headless' - version = project.findProperty('projectVersion') ?: "unversioned" + version = project.findProperty('projectVersion') ?: "99.99.99+unversioned" } tasks.register('logTeamCityBuildStatus') { diff --git a/cwbi-docker/entrypoint.sh b/cwbi-docker/entrypoint.sh new file mode 100644 index 0000000..f92ca0b --- /dev/null +++ b/cwbi-docker/entrypoint.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +## Check if OFFICE is set +#if [ -z "$OFFICE" ]; then +# echo "OFFICE is not set" +# exit 1 +#fi +# +## Lowercase the OFFICE variable +#OFFICE_LOWER="${OFFICE,,}" +# +## Check if GITHUB_TOKEN is set +#if [ -z "$GITHUB_TOKEN" ]; then +# echo "GITHUB_TOKEN is not set" +# exit 1 +#fi +# +#if [ -z "$ENVIRONMENT" ]; then +# echo "ENVIRONMENT not set" +# exit 1 +#fi +# +#GITHUB_BRANCH="cwbi-$ENVIRONMENT" +# +#echo "Using GITHUB_BRANCH: $GITHUB_BRANCH" +# +## Clone the office repo +#git clone --branch $GITHUB_BRANCH \ +# https://$GITHUB_TOKEN@github.com/USACE-WaterManagement/$OFFICE_LOWER-wm-cwbi-jobs.git /jobs +#if [ $? -ne 0 ]; then +# echo "Failed to clone the repository for $OFFICE" +# exit 1 +#fi +# +#unset GITHUB_TOKEN + +# Set all shell scripts to executable +chmod +x /jobs/bin/*.sh + + +# Check if the requirements.txt file exists and install Python dependencies +REQUIREMENTS_FILE="/jobs/python/requirements.txt" + +if [ -f "$REQUIREMENTS_FILE" ]; then + echo "Installing Python dependencies from $REQUIREMENTS_FILE..." + pip install -r "$REQUIREMENTS_FILE" +else + echo "No requirements.txt found at $REQUIREMENTS_FILE" +fi + +if [ -d "/app/local_dist" ]; then + echo "Installing local wheels from /app/local_dist..." + pip install --force-reinstall /app/local_dist/*.whl +fi + +# Run whatever CMD was passed in +exec "$@" diff --git a/cwbi-docker/requirements.txt b/cwbi-docker/requirements.txt new file mode 100644 index 0000000..1bddc7d --- /dev/null +++ b/cwbi-docker/requirements.txt @@ -0,0 +1,52 @@ +cwms-python==1.0.1 +hec-python-library==1.0 +hecdss==0.1.29 +cwms-cli==0.1.5 +shef-parser==1.6.2 +beautifulsoup4==4.13.5 +bokeh==3.8.2 +boto3==1.40.39 +botocore==1.40.39 +certifi==2025.8.3 +charset-normalizer==3.4.3 +click==8.3.0 +colorama==0.4.6 +contourpy==1.3.3 +cycler==0.12.1 +dataretrieval==1.0.12 +flexcache==0.3 +flexparser==0.4 +fonttools==4.60.2 +idna==3.10 +Jinja2==3.1.6 +jmespath==1.0.1 +kiwisolver==1.4.9 +MarkupSafe==3.0.2 +matplotlib==3.10.6 +narwhals==2.5.0 +numpy==2.3.3 +packaging==25.0 +pandas==2.3.2 +pillow==11.3.0 +Pint==0.25 +Pint-Pandas==0.7.1 +platformdirs==4.4.0 +plotly==6.3.0 +pyparsing==3.2.5 +python-dateutil==2.9.0.post0 +pytz==2025.2 +PyYAML==6.0.2 +requests==2.32.5 +requests-toolbelt==1.0.0 +s3transfer==0.14.0 +scipy==1.16.2 +seaborn==0.13.2 +six==1.17.0 +soupsieve==2.8 +tornado==6.5.2 +typing_extensions==4.15.0 +tzdata==2025.2 +tzlocal==5.3.1 +urllib3==2.6.3 +xarray==2025.9.0 +xyzservices==2025.4.0 diff --git a/datasources.yaml b/datasources.yaml new file mode 100644 index 0000000..684c2d6 --- /dev/null +++ b/datasources.yaml @@ -0,0 +1,16 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + url: http://prometheus:9090 + access: proxy + isDefault: true + - name: Jaeger + type: jaeger + url: http://jaeger:16686 + access: proxy + - name: Loki + type: loki + url: http://loki:3100 + access: proxy \ No newline at end of file diff --git a/docker-compose-cwbi.yml b/docker-compose-cwbi.yml new file mode 100644 index 0000000..834a952 --- /dev/null +++ b/docker-compose-cwbi.yml @@ -0,0 +1,66 @@ +name: regi-headless-cwbi + +services: + app: + build: + context: . + dockerfile: Dockerfile-cwbi-copy.dockerfile + command: ["python", "/jobs/GateFlowCalc2_Jpype.py"] + environment: + - OTEL_SERVICE_NAME=regi-headless + - OTEL_TRACES_EXPORTER=otlp + - OTEL_METRICS_EXPORTER=otlp + - OTEL_LOGS_EXPORTER=otlp + - OTEL_BSP_SCHEDULE_DELAY=1000 + - OTEL_EXPORTER_OTLP_PROTOCOL=grpc + - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 + - OTEL_INSTRUMENTATION_OKHTTP_ENABLED=true + - OTEL_INSTRUMENTATION_METHODS_ENABLED=true + - CDA_URL=http://host.docker.internal:7001/swt-data + - API_KEY=apikey M5HECTEST + - OFFICE_ID=SWT + depends_on: + - otel-collector + volumes: + - ./regi-headless/build/install/regi-headless/dist:/app/local_dist + - ./regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2_Jpype.py:/jobs/GateFlowCalc2_Jpype.py + - ./empty_dir:/jobs/bin + - ./empty_dir:/jobs/python + + otel-collector: + image: otel/opentelemetry-collector-contrib:latest + command: ["--config=/etc/otel-collector-config.yaml"] + volumes: + - ./otel-config.yaml:/etc/otel-collector-config.yaml + + jaeger: + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" + - "4317:4317" + + prometheus: + image: prom/prometheus + volumes: + - ./prometheus.yaml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + + loki: + image: grafana/loki:latest + ports: + - "3100:3100" + + grafana: + image: grafana/grafana:latest + ports: + - "3000:3000" + volumes: + - ./datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml + environment: + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + depends_on: + - prometheus + - jaeger + - loki \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f6d44fd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,64 @@ +name: regi-headless-java + +services: + app: + build: . + environment: + - OTEL_SERVICE_NAME=regi-headless + - OTEL_TRACES_EXPORTER=otlp + - OTEL_METRICS_EXPORTER=otlp + - OTEL_LOGS_EXPORTER=otlp + - OTEL_BSP_SCHEDULE_DELAY=1000 + - OTEL_EXPORTER_OTLP_PROTOCOL=grpc + - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 + - OTEL_INSTRUMENTATION_OKHTTP_ENABLED=true + - OTEL_INSTRUMENTATION_METHODS_ENABLED=true +# - REGI_HEADLESS_OPTS=-javaagent:/app/opentelemetry-javaagent.jar + - JAVA_TOOL_OPTIONS=-javaagent:/app/opentelemetry-javaagent.jar + - CDA_URL=http://host.docker.internal:7001/swt-data + - API_KEY=apikey M5HECTEST + - OFFICE_ID=SWT + - SCRIPT=/scripts/GateFlowCalc2.py + depends_on: + - otel-collector + volumes: + - ./otel-config.yaml:/etc/otel-collector-config.yaml + - ./regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2.py:/scripts/GateFlowCalc2.py + + otel-collector: + image: otel/opentelemetry-collector-contrib:latest + command: ["--config=/etc/otel-collector-config.yaml"] + volumes: + - ./otel-config.yaml:/etc/otel-collector-config.yaml + + jaeger: + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" + - "4317:4317" + + prometheus: + image: prom/prometheus + volumes: + - ./prometheus.yaml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + + loki: + image: grafana/loki:latest + ports: + - "3100:3100" + + grafana: + image: grafana/grafana:latest + ports: + - "3000:3000" + volumes: + - ./grafana/datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml + environment: + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + depends_on: + - prometheus + - jaeger + - loki \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..f41f616 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,11 @@ +# +# Copyright (c) 2026 +# United States Army Corps of Engineers - Hydrologic Engineering Center (USACE/HEC) +# All Rights Reserved. USACE PROPRIETARY/CONFIDENTIAL. +# Source may not be released without written approval from HEC +# + +CDA_URL=http://localhost:7001/swt-data +API_KEY=apikey M5HECTEST +OFFICE_ID=SWT +SCRIPT=C:/Git/HEC/BitBucket/regi-headless/regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2.py \ No newline at end of file diff --git a/gradle/docs/jpype.png b/gradle/docs/jpype.png new file mode 100644 index 0000000000000000000000000000000000000000..d1e26861acdb178965c971ac9dc35ef66ee5e679 GIT binary patch literal 57922 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

wW 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 literal 0 HcmV?d00001 diff --git a/gradle/docs/jpype.puml b/gradle/docs/jpype.puml new file mode 100644 index 0000000..4884599 --- /dev/null +++ b/gradle/docs/jpype.puml @@ -0,0 +1,34 @@ +@startuml +autonumber +skinparam BoxPadding 10 + +box "Python Environment (CPython)" #LightBlue + participant "test_script.py" as Test + participant "regi_cli" as Pkg + participant "JPype Engine" as JPype +end box + +box "Java Virtual Machine" #LightYellow + participant "JVM Internal" as JVM + participant "regi-headless.jar" as Jar +end box + +Note over Test: env variables set externally \nJAVA_HOME\nCDA_URL\nAPI_KEY\nOFFICE_ID + +Test -> Pkg: import regi_cli +Pkg -> JPype: startJVM(classpath=["lib/*"]) + +group JVM Initialization + JVM -> Jar: Scan Classpath + JVM --> Pkg: JVM Started Successfully +end group + +Test -> JPype: Call Java-backed functions (ex. registry.getCalculation(1.0, "Gate Flow")) +JPype -> Jar: Reflect & Load Class + +JPype -> Jar: Instantiate Object / Call Method +activate Jar #DarkSalmon +Note right of Jar: Java Logic Executes +deactivate Jar + +@enduml \ No newline at end of file diff --git a/otel-config.yaml b/otel-config.yaml new file mode 100644 index 0000000..5ee34fc --- /dev/null +++ b/otel-config.yaml @@ -0,0 +1,36 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +exporters: + debug: + verbosity: detailed + # Jaeger handles OTLP natively + otlp/jaeger: + endpoint: jaeger:4317 + tls: + insecure: true + # Standard Prometheus exporter + prometheus: + endpoint: "0.0.0.0:8889" + # Use the standard OTLP HTTP exporter to talk to Loki + otlphttp/loki: + endpoint: "http://loki:3100/otlp" + tls: + insecure: true + +service: + pipelines: + traces: + receivers: [otlp] + exporters: [otlp/jaeger, debug] + metrics: + receivers: [otlp] + exporters: [prometheus, debug] + logs: + receivers: [otlp] + exporters: [otlphttp/loki, debug] \ No newline at end of file diff --git a/prometheus.yaml b/prometheus.yaml new file mode 100644 index 0000000..cbbe048 --- /dev/null +++ b/prometheus.yaml @@ -0,0 +1,4 @@ +scrape_configs: + - job_name: 'otel-collector' + static_configs: + - targets: ['otel-collector:8889'] \ No newline at end of file diff --git a/regi-headless/build.gradle b/regi-headless/build.gradle index 2075ac4..37aedb8 100644 --- a/regi-headless/build.gradle +++ b/regi-headless/build.gradle @@ -4,6 +4,10 @@ plugins { id 'application' } +configurations { + otelAgent +} + dependencies { implementation(libs.bundles.hec) implementation(libs.bundles.serversuite) @@ -15,6 +19,7 @@ dependencies { implementation(libs.bundles.regi.tools) {artifact {extension = "jar"} } implementation(libs.otel) runtimeOnly(libs.hec.cwms.ratings.cda) + otelAgent("io.opentelemetry.javaagent:opentelemetry-javaagent:2.23.0") testImplementation(libs.bundles.junit.api) @@ -62,3 +67,72 @@ distributions { } } +task bundlePython(type: Sync) { + description = 'Bundles Python scripts and creates the java_lib directory' + into "${buildDir}/install/${project.name}/" + from('src/main/python') { + include 'pyproject.toml' + filter { line -> line.replaceAll('@VERSION@', project.version.toString()) } + } + + from('src/main/python') { + exclude 'pyproject.toml' + } + + into('regi_cli/lib') { + from configurations.runtimeClasspath + from jar.archiveFile + exclude "**/*.nbm" + } + + from(configurations.otelAgent) { + into "regi_cli/lib" + rename { "opentelemetry-javaagent.jar" } + } +} + +task buildPythonWheel(type: Exec) { + group = 'distribution' + description = 'Builds a Python .whl file from the installDist output' + dependsOn bundlePython + + workingDir "${buildDir}/install/${project.name}" + + commandLine 'python', '-m', 'build', '--wheel' + + doLast { + println "Python Wheel built in: ${workingDir}/dist" + } +} + +tasks.register('testPythonWheel') { + group = 'verification' + description = 'Creates a venv, installs the wheel, and runs a test script.' + dependsOn buildPythonWheel + + doLast { + def venvName = "test_venv" + def wheelDir = "${projectDir}/build/install/regi-headless/dist" + def testScript = "src\\test\\resources\\usace\\rowcps\\headless\\examples\\GateFlowCalc2_Jpype.py" + + // 1. Create VENV if it doesn't exist + exec { + commandLine 'cmd', '/c', "python -m venv ${venvName}" + } + + // 2. Install Wheel and Run Script + // We chain these using '&&' so they run in the same shell session where the venv is active + exec { + workingDir projectDir + def wheelFile = fileTree(dir: wheelDir, include: '*.whl').singleFile.absolutePath + def venvActivate = file("${venvName}/Scripts/activate.bat").absolutePath + environment 'CDA_URL', project.findProperty('CDA_URL') ?: "" + environment 'API_KEY', project.findProperty('API_KEY') ?: "" + environment 'OFFICE_ID', project.findProperty('OFFICE_ID') ?: "" + environment 'JAVA_HOME', System.getProperty('java.home') + commandLine 'cmd', '/c', "\"${venvActivate}\" && pip install --force-reinstall \"${wheelFile}\" && python -u ${testScript}" + standardOutput = System.out + errorOutput = System.err + } + } +} diff --git a/regi-headless/src/main/python/pyproject.toml b/regi-headless/src/main/python/pyproject.toml new file mode 100644 index 0000000..f32f65d --- /dev/null +++ b/regi-headless/src/main/python/pyproject.toml @@ -0,0 +1,17 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "regi-cli" +version = "@VERSION@" +description = "USACE Regi-Headless Python Bridge" +dependencies = [ + "JPype1>=1.4.1", +] + +[tool.setuptools] +packages = ["regi_cli"] + +[tool.setuptools.package-data] +"regi_cli" = ["lib/*.jar"] \ No newline at end of file diff --git a/regi-headless/src/main/python/regi_cli/__init__.py b/regi-headless/src/main/python/regi_cli/__init__.py new file mode 100644 index 0000000..e3f8a5c --- /dev/null +++ b/regi-headless/src/main/python/regi_cli/__init__.py @@ -0,0 +1,10 @@ +from .regi_cli import regi_session, run_headless +from importlib.metadata import version, PackageNotFoundError + +__all__ = ["regi_session", "run_headless"] + +try: + __version__ = version("regi_cli") # Use the 'name' from pyproject.toml +except PackageNotFoundError: + # package is not installed + __version__ = "unknown" \ No newline at end of file diff --git a/regi-headless/src/main/python/regi_cli/regi_cli.py b/regi-headless/src/main/python/regi_cli/regi_cli.py new file mode 100644 index 0000000..f5fcc5f --- /dev/null +++ b/regi-headless/src/main/python/regi_cli/regi_cli.py @@ -0,0 +1,107 @@ +# Copyright (c) 2026 +# United States Army Corps of Engineers - Hydrologic Engineering Center (USACE/HEC) +# All Rights Reserved. USACE PROPRIETARY/CONFIDENTIAL. +# Source may not be released without written approval from HEC + +import os +import logging +import jpype +import jpype.imports +from contextlib import contextmanager +from pathlib import Path + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger("regi-launcher") + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +LIB_PATH = os.path.join(BASE_DIR, "lib", "*") +java_home = os.environ.get('JAVA_HOME') +java_bin = os.path.join(java_home, 'bin') +if java_bin not in os.environ['PATH']: + os.environ['PATH'] = java_bin + os.pathsep + os.environ['PATH'] + +@contextmanager +def regi_session(): + """ + Context manager to handle JVM lifecycle. + Usage: + with regi_session(): + run_headless(my_func) + """ + if not jpype.isJVMStarted(): + agent_jar = Path(os.path.join(BASE_DIR, "lib", "opentelemetry-javaagent.jar")) + agent_flag = f"-javaagent:{agent_jar.absolute()}" + + logger.info(f"Starting JVM with agent: {agent_flag}") + logger.info("Starting JVM...") + jpype.startJVM( + jpype.getDefaultJVMPath(), + agent_flag, + convertStrings=True, + classpath=[LIB_PATH] + ) + + try: + yield + finally: + if jpype.isJVMStarted(): + logger.info("Shutting down JVM...") + jpype.shutdownJVM() + +def run_headless(calculation_callback): + # We must import these inside the function or after JVM starts + from usace.rowcps.headless import HeadlessRegiDomainFactory, RegiCalcRegistry + from usace.rowcps.regi.factories import RowcpsExecutorService + from java.util.concurrent import TimeUnit + GlobalOpenTelemetry = jpype.JClass("io.opentelemetry.api.GlobalOpenTelemetry") + builder = GlobalOpenTelemetry.getTracer("regi-headless").spanBuilder("runHeadless") + builder.setAttribute("cda.url", os.environ.get("CDA_URL", "unknown")) + builder.setAttribute("cwms.office", os.environ.get("OFFICE_ID", "unknown")) + root_span = builder.startSpan() + scope = root_span.makeCurrent() + try: + factory = HeadlessRegiDomainFactory() + logger.info("Attempting to create RegiDomain...") + regi_domain = factory.createDomain() + + if regi_domain is not None: + manager_id = factory.getManagerId() + registry = RegiCalcRegistry(regi_domain, manager_id) + + try: + logger.info("Executing callback...") + calculation_callback(registry) + regi_domain.commitData(manager_id) + except Exception as e: + logger.error("Execution failed.", exc_info=True) + raise + finally: + _shutdown_executor(manager_id) + regi_domain.closing() + except (jpype.JException, Exception) as ex: + # 2. Mirroring the Java catch block + root_span.recordException(ex) + StatusCode = jpype.JClass("io.opentelemetry.api.trace.StatusCode") + root_span.setStatus(StatusCode.ERROR) + + if isinstance(ex, jpype.JException): + logger.error("Java Exception occurred during headless execution:") + ex.printStackTrace() + else: + logger.error(f"Python Exception occurred during headless execution: {ex}") + + # Mirroring: System.exit(-1) + scope.close() + root_span.end() + finally: + # 3. Ensuring cleanup + scope.close() + root_span.end() + +def _shutdown_executor(manager_id): + from usace.rowcps.regi.factories import RowcpsExecutorService + from java.util.concurrent import TimeUnit + res = RowcpsExecutorService.getInstance(manager_id) + res.shutdown() + if not res.awaitTermination(3000, TimeUnit.MILLISECONDS): + res.shutdownNow() \ No newline at end of file diff --git a/regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2.py b/regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2.py index e5d6542..2606628 100644 --- a/regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2.py +++ b/regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2.py @@ -10,16 +10,16 @@ timeZone = TimeZone.getTimeZone("US/Central") startCal = Calendar.getInstance(timeZone) startCal.clear() -startCal.set(Calendar.YEAR, 2015) +startCal.set(Calendar.YEAR, 2022) startCal.set(Calendar.MONTH, 4) endCal = Calendar.getInstance(timeZone) endCal.clear() -endCal.set(Calendar.YEAR, 2015) +endCal.set(Calendar.YEAR, 2022) endCal.set(Calendar.MONTH, 6) #gateCalc.computeFlowGroup("SWF", "LEWT2", startCal.getTimeInMillis(), endCal.getTimeInMillis(), "Flow.LEWT2.ConduitGate_Total") -gateCalc.computeAll("SWF", "LEWT2", startCal.getTimeInMillis(), endCal.getTimeInMillis()) +gateCalc.computeAll("SWT", "TENK", startCal.getTimeInMillis(), endCal.getTimeInMillis()) diff --git a/regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2_Jpype.py b/regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2_Jpype.py new file mode 100644 index 0000000..62538d3 --- /dev/null +++ b/regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2_Jpype.py @@ -0,0 +1,44 @@ +import logging +from regi_cli import regi_session, run_headless + +# Initialize logger for this specific script +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +def compute_gate_flow(registry): + """ + This function contains your specific calculation logic. + The 'registry' object is passed in by the launcher. + """ + # Java imports must happen here (after JVM has started) + from java.util import Calendar, TimeZone + + logger.info("Starting Gate Flow Calculation...") + + # Use the registry passed into the function + gate_calc = registry.getCalculation(1.0, "Gate Flow") + + # Time zone must be set because the Solaris time zone is UTC + time_zone = TimeZone.getTimeZone("US/Central") + + start_cal = Calendar.getInstance(time_zone) + start_cal.clear() + start_cal.set(2022, 4, 1) # YEAR, MONTH (0-indexed, 4=May), DAY + + end_cal = Calendar.getInstance(time_zone) + end_cal.clear() + end_cal.set(2022, 6, 1) # YEAR, MONTH (0-indexed, 6=July), DAY + + logger.info(f"Computing for SWT/TENK from {start_cal.getTime()} to {end_cal.getTime()}") + + # Execute the calculation + gate_calc.computeAll("SWT", "TENK", start_cal.getTimeInMillis(), end_cal.getTimeInMillis()) + + logger.info("Calculation successful.") + +if __name__ == "__main__": + # Use the context manager to handle JVM lifecycle and run the logic + with regi_session(): + run_headless(compute_gate_flow) + +