Yann Neuhaus
M-Files didn’t fix your problem, your organization did.
Last week a wrote about the reasons why ECM projects fails.
M-Files is a wonderful tool and probably my favorite ECM, but it won’t solve your problem.
Companies that initiate digitalization projects like to convince themselves that investing in an ECM will solve all their problems: no more chaos, data loss, inefficiency, or lack of collaboration.
I’m sorry to break your dreams, but it won’t!
Tools AmplifySoftware like M-Files is powerful. No doubt about it.
It structures information, automates workflows, enables collaboration, and improves visibility. However, all of that depends on one thing:
What you put into it.
If your processes are unclear, M-Files will exacerbate the confusion.
Likewise, if your governance is weak, M-Files will amplify that weakness.
M-Files won’t magically make your teams start collaborating.
Tools don’t create discipline; they expose it, or the lack of it.
Think of M-Files less as a solution and more as a multiplier.
- Good processes become great.
- Broken processes break faster.
So why do organizations keep expecting tools to “fix everything”?
Because it’s easier.
Buying software seems like progress. It’s tangible, measurable and budgeted. It shows action.
On the other hand, fixing an organization requires:
- Alignment between teams
- Clear ownership
- Hard decisions on processes
- Cultural change
That’s messy, Political and Slow.
Instead, companies subconsciously shift the responsibility.
Once we have the tool, things will improve.
But what if they don’t?
They blame:
- The tool
- The implementation
- The users
Almost never the organization itself.
The Real Success FactorsWhen M-Files works really well, it’s never just about the tool.
It’s because a few key things were already in place or built alongside it.
Before digitizing anything, successful teams answer:
- What actually happens today?
- What should happen?
- Who is responsible at each step?
M-Files then becomes the execution layer, not the definition layer. It is not the job of M-Files to determine what should be done, but rather to ensure that the process is under control and follows organizational rules.
Ownership & AccountabilityEvery document, workflow, and decision needs an owner.
Without that:
- Workflows are stuck.
- Approvals take forever.
- Nobody feels responsible.
With it:
- M-Files flows naturally.
- Decisions are made faster.
- Accountability is visible.
Over-engineered systems fail.
The best M-Files setups are:
- Simple
- Intuitive
- Close to how people already work
Not “perfect.” Just usable and adopted.
Improving a work process is possible at any stage, but simplifying an existing one is challenging.
Change ManagementThe biggest challenge isn’t technical, it’s human.
People need to:
- Understand why things are changing
- Trust the system
- See personal value
Without that, even the best setup gets ignored.
Continuous ImprovementSuccessful organizations don’t “finish” their M-Files project.
They:
- Iterate
- Adjust workflows
- Refine metadata
- Listen to users
The system evolves with the business, which is the most important aspect, in my opinion, and one that is often overlooked.
Thinking differentlyThe real question is not: “What can M-Files do for us?”
Instead think: “Are we mature enough to successfully implement M-Files?”
It’s obviously not easy to answer that question, especially when you’re focused on your core business.
And that’s perfectly normal, that’s why we’re here to bridge the gap between software and the realities of business.
We’re not just here to install and configure systems; when we take on a digital transformation project, our primary role is to assess the organization’s readiness from an outside perspective, without passing judgment. Then we identify the necessary steps to reach the target.
To me, the real reward at the end of a project isn’t when the client says, “M-Files works well,” but when they say, “Thanks to M-Files, we’ve improved our collaboration and streamlined our interactions” and that makes a big difference.
L’article M-Files didn’t fix your problem, your organization did. est apparu en premier sur dbi Blog.
PostgreSQL 19: get_*_ddl functions
PostgreSQL already comes with plenty of system information functions to reconstruct the commands to create various objects, e.g. constraints or indexes. Starting with PostgreSQL 19 more functions will be available, namely those:
- pg_get_database_ddl
- pg_get_role_ddl
- pg_get_tablespace_ddl
As the names imply they can be used to recreate the commands to create a database, a role, or a tablespace.
To see what they do lets create a small setup:
postgres=# select version();
version
---------------------------------------------------------------------------------------
PostgreSQL 19devel dbi services build on x86_64-linux, compiled by gcc-15.1.1, 64-bit
(1 row)
postgres=# create user u with login password 'u';
CREATE ROLE
postgres=# \! mkdir /var/tmp/tbs
postgres=# create tablespace tbs location '/var/tmp/tbs' with ( random_page_cost = 1.1 );
CREATE TABLESPACE
postgres=# create database d with owner = u tablespace = tbs;
CREATE DATABASE
postgres=# alter database d connection limit = 10;
ALTER DATABASE
postgres=# \l
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
-----------+----------+----------+-----------------+-------------+-------------+-------------+-----------+-----------------------
d | u | UTF8 | icu | en_US.UTF-8 | en_US.UTF-8 | en-US-x-icu | |
postgres | postgres | UTF8 | icu | en_US.UTF-8 | en_US.UTF-8 | en-US-x-icu | |
template0 | postgres | UTF8 | icu | en_US.UTF-8 | en_US.UTF-8 | en-US-x-icu | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | icu | en_US.UTF-8 | en_US.UTF-8 | en-US-x-icu | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
(4 rows)
To get the commands to recreate that database the new function “pg_get_database_ddl” can be used:
postgres=# select * from pg_get_database_ddl ( 'd'::regdatabase );
pg_get_database_ddl
---------------------------------------------------------------------------------------------------------------------------------------------------------
CREATE DATABASE d WITH TEMPLATE = template0 ENCODING = 'UTF8' LOCALE_PROVIDER = icu LOCALE = 'en_US.UTF-8' ICU_LOCALE = 'en-US-x-icu' TABLESPACE = tbs;
ALTER DATABASE d OWNER TO u;
ALTER DATABASE d CONNECTION LIMIT = 10;
(3 rows)
There are some options to control the output format and what gets reconstructed, e.g.:
postgres=# select * from pg_get_database_ddl ( 'd'::regdatabase, 'pretty', 'true' );
pg_get_database_ddl
-----------------------------------------
CREATE DATABASE d +
WITH TEMPLATE = template0 +
ENCODING = 'UTF8' +
LOCALE_PROVIDER = icu +
LOCALE = 'en_US.UTF-8' +
ICU_LOCALE = 'en-US-x-icu' +
TABLESPACE = tbs;
ALTER DATABASE d OWNER TO u;
ALTER DATABASE d CONNECTION LIMIT = 10;
(3 rows)
postgres=# select * from pg_get_database_ddl ( 'd'::regdatabase, 'pretty', 'true', 'owner', 'false' );
pg_get_database_ddl
-----------------------------------------
CREATE DATABASE d +
WITH TEMPLATE = template0 +
ENCODING = 'UTF8' +
LOCALE_PROVIDER = icu +
LOCALE = 'en_US.UTF-8' +
ICU_LOCALE = 'en-US-x-icu' +
TABLESPACE = tbs;
ALTER DATABASE d CONNECTION LIMIT = 10;
(2 rows)
postgres=# select * from pg_get_database_ddl ( 'd'::regdatabase, 'pretty', 'true', 'owner', 'false', 'tablespace', 'false' );
pg_get_database_ddl
-----------------------------------------
CREATE DATABASE d +
WITH TEMPLATE = template0 +
ENCODING = 'UTF8' +
LOCALE_PROVIDER = icu +
LOCALE = 'en_US.UTF-8' +
ICU_LOCALE = 'en-US-x-icu';
ALTER DATABASE d CONNECTION LIMIT = 10;
(2 rows)
The other two functions behave the same (but do not have exactly the same options):
postgres=# select * from pg_get_tablespace_ddl('tbs');
pg_get_tablespace_ddl
---------------------------------------------------------------
CREATE TABLESPACE tbs OWNER postgres LOCATION '/var/tmp/tbs';
ALTER TABLESPACE tbs SET (random_page_cost='1.1');
(2 rows)
postgres=# select * from pg_get_tablespace_ddl('tbs', 'pretty', 'true');
pg_get_tablespace_ddl
----------------------------------------------------
CREATE TABLESPACE tbs +
OWNER postgres +
LOCATION '/var/tmp/tbs';
ALTER TABLESPACE tbs SET (random_page_cost='1.1');
(2 rows)
postgres=# select * from pg_get_tablespace_ddl('tbs', 'pretty', 'true', 'owner', 'false');
pg_get_tablespace_ddl
----------------------------------------------------
CREATE TABLESPACE tbs +
LOCATION '/var/tmp/tbs';
ALTER TABLESPACE tbs SET (random_page_cost='1.1');
(2 rows)
… and finally for the roles:
postgres=# select * from pg_get_role_ddl ('u');
pg_get_role_ddl
--------------------------------------------------------------------------------------------
CREATE ROLE u NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB LOGIN NOREPLICATION NOBYPASSRLS;
(1 row)
postgres=# select * from pg_get_role_ddl ('u', 'pretty', 'true');
pg_get_role_ddl
-------------------
CREATE ROLE u +
NOSUPERUSER +
INHERIT +
NOCREATEROLE +
NOCREATEDB +
LOGIN +
NOREPLICATION+
NOBYPASSRLS;
(1 row)
postgres=# select * from pg_get_role_ddl ('u', 'pretty', 'true', 'memberships', 'false');
pg_get_role_ddl
-------------------
CREATE ROLE u +
NOSUPERUSER +
INHERIT +
NOCREATEROLE +
NOCREATEDB +
LOGIN +
NOREPLICATION+
NOBYPASSRLS;
(1 row)
Nice, and again: Thanks to all involved.
L’article PostgreSQL 19: get_*_ddl functions est apparu en premier sur dbi Blog.
PostgreSQL 19: json format for “copy to”
PostgreSQL already has impressive support for working with data in json format. If you look at the jsonb data type and all the built-in functions and operators you can use, there is so much you can do with it by default. Starting with PostgreSQL 19 there is one feature more when it comes to working with data in json format.
“COPY” already is quite powerful and the fastest way to get data in and out of PostgreSQL (you may read some previous posts about copy here, here, and here).
As usual lets start with a simple table:
postgres=# create table t ( a int primary key, b text );
CREATE TABLE
postgres=# insert into t select i, md5(i::text) from generate_series(1,1000000) i;
INSERT 0 1000000
To get that data out in text format you might simply do this:
postgres=# copy t to '/var/tmp/t';
COPY 1000000
postgres=# \! head /var/tmp/t
1 c4ca4238a0b923820dcc509a6f75849b
2 c81e728d9d4c2f636f067f89cc14862c
3 eccbc87e4b5ce2fe28308fd9f2a7baf3
4 a87ff679a2f3e71d9181a67b7542122c
5 e4da3b7fbbce2345d7772b0674a318d5
6 1679091c5a880faf6fb5e6087eb1b2dc
7 8f14e45fceea167a5a36dedd4bea2543
8 c9f0f895fb98ab9159f51fd0297e236d
9 45c48cce2e2d7fbdea1afc51c7c6ad26
10 d3d9446802a44259755d38e6d163e820
Starting with PostgreSQL 19 you can do the same in json format:
postgres=# copy t to '/var/tmp/t1' with (format json);
COPY 1000000
postgres=# \! head /var/tmp/t1
{"a":1,"b":"c4ca4238a0b923820dcc509a6f75849b"}
{"a":2,"b":"c81e728d9d4c2f636f067f89cc14862c"}
{"a":3,"b":"eccbc87e4b5ce2fe28308fd9f2a7baf3"}
{"a":4,"b":"a87ff679a2f3e71d9181a67b7542122c"}
{"a":5,"b":"e4da3b7fbbce2345d7772b0674a318d5"}
{"a":6,"b":"1679091c5a880faf6fb5e6087eb1b2dc"}
{"a":7,"b":"8f14e45fceea167a5a36dedd4bea2543"}
{"a":8,"b":"c9f0f895fb98ab9159f51fd0297e236d"}
{"a":9,"b":"45c48cce2e2d7fbdea1afc51c7c6ad26"}
{"a":10,"b":"d3d9446802a44259755d38e6d163e820"}
Specifying a SQL is also supported:
postgres=# copy (select a from t) to '/var/tmp/t1' with (format json);
COPY 1000000
postgres=# \! head /var/tmp/t1
{"a":1}
{"a":2}
{"a":3}
{"a":4}
{"a":5}
{"a":6}
{"a":7}
{"a":8}
{"a":9}
{"a":10}
As noted in the commit message there are some options which are not compatible with the json format:
- HEADER
- DEFAULT
- NULL
- DELIMITER
- FORCE QUOTE
- FORCE NOT NULL
- and FORCE NULL
Also not supported (currently) is “copy from”.
L’article PostgreSQL 19: json format for “copy to” est apparu en premier sur dbi Blog.
OGG-30007 : How To Register Certificates In GoldenGate ?
After working on a GoldenGate deployment recently, I felt that the OGG-30007 error would be worth writing about. This error happens when registering certificates in GoldenGate. Whether you do it from the web UI or with the REST API, this GoldenGate error is detailed as such:
Processing of the certificate PEM portion of the certificate bundle resulted in more than one (1) certificate objects.
Code: OGG-30007
Cause: The read and decode processing of the specified portion of the certificate bundle produced more than one (1) objects. More than one PEM encoded object is present in the data.
Action: Review the specified portion of the certificate bundle and correct as needed. Only a single PEM encoded object is expected.
This error occurs because GoldenGate expects exactly one PEM object per import, while a certificate chain file contains multiple certificates. In the web UI, a pop-up will alert you that something is wrong:
As mentioned, this typically happens when registering a certificate chain. For instance, you could face the issue when connecting two deployments secured with NGINX. The server presents a certificate chain including the intermediate, while the client (GoldenGate) must trust both the root and the intermediate.
But when the Certificate Authority doesn’t sign your certificate directly with the Root Certificate, but with an Intermediate Certificate, the server presents a certificate chain including the intermediate. A certificate like the following will generate an OGG-30007 error if you try to add it to the truststore:
-----BEGIN CERTIFICATE-----
(intermediate)
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
(root)
-----END CERTIFICATE-----
And to make sure that your connections work, you should not only add the root certificate, but also the intermediate certificate. Because of the way GoldenGate stores these certificates, two separate entries must be created in the truststore. To ease monitoring and certificate management, you can name them rootCA_ogg_target and intermediateCA_ogg_target
With this, you should have no problem connecting GoldenGate deployments ! To avoid OGG-30007, ensure that each certificate is imported separately. In practice, this means extracting the root and intermediate certificates from the chain file and registering them as individual entries in the GoldenGate truststore.
L’article OGG-30007 : How To Register Certificates In GoldenGate ? est apparu en premier sur dbi Blog.
PostgreSQL 19: The “repack” command
Before PostgreSQL 19 you had two commands to completely rewrite a table: Either you can use the “vacuum full” or the “cluster” command to achieve this. Both operations are blocking and the table cannot be used until those operations complete. This can easily be verified with the following simple test cases:
-- session 1
postgres=# create table t ( a int primary key, b text );
CREATE TABLE
postgres=# insert into t select i, md5(i::text) from generate_series(1,10000000) i;
INSERT 0 1000000
postgres=# vacuum full t;
-- session 2
postgres=# select count(*) from t; -- this blocks until vacuum full completes
The same is true for the “cluster” command:
-- session 1
postgres=# \d t
Table "public.t"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
a | integer | | not null |
b | text | | |
Indexes:
"t_pkey" PRIMARY KEY, btree (a)
postgres=# cluster t using t_pkey;
-- session 2
postgres=# select count(*) from t; -- this blocks until clustering completes
Starting with PostgreSQL 19 (scheduled to be released later this year) these two functionalities are combined into the “repack” command. The commit message makes the reason behind this pretty clear:
Introduce the REPACK command
REPACK absorbs the functionality of VACUUM FULL and CLUSTER in a single
command. Because this functionality is completely different from
regular VACUUM, having it separate from VACUUM makes it easier for users
to understand; as for CLUSTER, the term is heavily overloaded in the
IT world and even in Postgres itself, so it's good that we can avoid it.
We retain those older commands, but de-emphasize them in the
documentation, in favor of REPACK; the difference between VACUUM FULL
and CLUSTER (namely, the fact that tuples are written in a specific
ordering) is neatly absorbed as two different modes of REPACK.
This allows us to introduce further functionality in the future that
works regardless of whether an ordering is being applied, such as (and
especially) a concurrent mode.
So, instead of spreading the functionality over two commands, there is a new command which combines both:
postgres=# \h repack
Command: REPACK
Description: rewrite a table to reclaim disk space
Syntax:
REPACK [ ( option [, ...] ) ] [ table_and_columns [ USING INDEX [ index_name ] ] ]
REPACK [ ( option [, ...] ) ] USING INDEX
where option can be one of:
VERBOSE [ boolean ]
ANALYZE [ boolean ]
CONCURRENTLY [ boolean ]
and table_and_columns is:
table_name [ ( column_name [, ...] ) ]
URL: https://www.postgresql.org/docs/devel/sql-repack.html
The really cool stuff about this is, that this can be run concurrently which means the table is not locked for others while the command is doing its work:
-- session 1
postgres=# repack (concurrently) t;
-- or
postgres=# repack (concurrently) t using index t_pkey;
-- session 2
postgres=# select count(*) from t; -- not blocking
Nice, thanks to all involved.
L’article PostgreSQL 19: The “repack” command est apparu en premier sur dbi Blog.
Why ECM Projects fail (even with good tools)
Today, I would like to take a step back and think about failure, because it’s part of life and sometimes things don’t go as planned. Analyzing failures can help to prevent them from happening again.
Firstly, in most cases, your ECM project did not fail because of the tool!
That’s the uncomfortable truth
It wasn’t because the platform was bad.
It wasn’t because the features were missing.
It wasn’t even because users ‘resisted change’.
And yet, the project still underdelivered. Or worse, it quietly failed.
After years of working on ECM implementations, I have seen the same pattern emerge time and time again: Good tools. Smart people. Disappointing results.
So what’s really going wrong?
The illusion of the “Right Tool”Let’s start with the most common misconception:
If we choose the right ECM solution, everything else will fall into place.
It won’t.
Modern ECM platforms are powerful. They can manage documents, automate workflows, enforce governance and integrate with almost anything.
But here’s the reality, A tool can’t fix a broken organization. It exposes it.
- If your processes are unclear, the system will amplify the confusion.
- If ownership is undefined, governance will collapse.
- If users don’t see the value, adoption will stall.
The tool is not the problem. It’s a mirror.
Business and IT are not solving the same problemMost ECM projects start with a misalignment that no one explicitly addresses.
IT is thinking:
- architecture
- security
- scalability
The business is thinking:
- “Where is my document?”
- “Why is this so slow?”
- “Why do I have to click 12 times?”
Both are valid. But they are not the same problem.
So what happens?
You get:
- technically solid systems
- that don’t solve day-to-day frustrations
And users disengage, not because they’re resistant, but because the system doesn’t help them.
Over-engineering everythingThis is an incredibly common issue.
In the name of ‘doing things properly’, projects end up with overly complex metadata models.
- overly complex metadata models
- workflows for every edge case
- validation rules everywhere.
On paper, this looks impressive.
In reality, the system becomes fragile, slow and difficult to use.
Complexity is often mistaken for maturity. In ECM, complexity is usually the fastest way to kill off adoption.
Governance that exists only in slidesEvery project has governance.
Or at least, a document describing it:
- Naming conventions
- Validation rules
- Lifecycle definitions
Everything is defined and then… ignored.
Because:
- no one owns it
- no one enforces it
- no one adapts it
It looks good during the project, but it disappears in real life.
The “Quick Win” trapQuick wins are supposed to build momentum and it’s true, however they often create long-term problems.
Why? Because they:
- bypass proper design
- introduce shortcuts
- create inconsistencies
And what about those “temporary” solutions? They never get fixed.
Months later, the system becomes:
- inconsistent
- hard to maintain
- confusing for users
You haven’t gained any speed. You just made things more complicated.
Quick wins are essential for driving user adoption, but this must be taken into account when designing the project as a whole. Consider the pros and cons, but do not compromise on the project’s overall quality.
No one owns the system after Go-liveThis is where many projects quietly fail.
Once the system is live:
- the project team disappears
- IT moves to other priorities
- the business assumes “it’s done”
ECM is not a one-time project. It’s a living system.
Without ownership:
- governance degrades
- usage diverges
- value decreases over time
And nobody reacts until it’s too late.
What actually worksHaving seen what fails, the question becomes:
What makes an ECM project succeed?
Not perfectly. But sustainably.
From my experience, here’s what consistently makes the difference:
Start with real user problemsNot features. Not architecture.
Ask:
- Where do people lose time?
- What frustrates them daily?
Solve that first.
Keep it simpler than you think- Fewer metadata fields
- Fewer workflows
- Clearer rules
You can always add complexity later, but it’s rare that you can remove it.
Make ownership explicitSomeone must:
- own the system
- enforce governance
- evolve the platform
No owner = slow decay
Design for reality, not theoryPeople will:
- take shortcuts
- ignore rules
- prioritize speed
During the design phase keep that in mind.
Treat ECM as a product, not a projectProjects end, but Products evolve.
To create long-term value, you need:
- continuous improvement
- feedback loops
- adaptation.
When I started working in ECM, data was made up of static documents with few or no relationships between them. Now, we have various types of content, such as pictures and videos, as well as more interaction and workflows. Therefore, what is suitable now might not be in two or three years.
Final ThoughtIn conclusion, most ECM projects don’t fail spectacularly. They fail quietly.
Users stop using advanced features, workarounds appear and value slowly erodes.
Eventually, people say:
The system is not that useful.
Not because the tool was bad. It’s because the project misunderstood what success actually looks like.
A good ECM system is not necessarily the one with the most or the trendiest features. Rather, it’s the one that people actually use because it simplifies their work.
As a result, the tool itself is not as important as you might expect. Of course, some are more relevant than others depending on the case. That is why, at dbi services, we support several of them.
The real key to the success of an ECM project lies in its management and long-term vision.
L’article Why ECM Projects fail (even with good tools) est apparu en premier sur dbi Blog.
Overcome GoldenGate 26ai Bug On Custom Profiles
In GoldenGate, processes have a few options that can be managed to fit your needs. When creating an extract, for instance, you can act on the following parameters:
autoStart.enabled: whether the process will start automatically after the Administration Server starts.autoStart.delay: delay in seconds before starting the process.autoRestart.enabled: whether to restart the process after it fails.autoRestart.onSuccess: the process is only restarted if it fails.autoRestart.delay: waiting time (in seconds) before attempting to restart a process once it fails.autoRestart.retries: maximum number of retries before stopping restart attempts.autoRestart.window: timeframe before GoldenGate will attempt to restart the process again.autoRestart.disableOnFailure: if set to True, GoldenGate will disable the restart if it fails to restart within theretries/windowsetting. You will have to start the process manually if this happens.
In the web UI, an extract profile can be customized during the third step of an extract creation called Managed Options. Here, you can choose to use the default profile, a user-defined profile or custom settings specific to the new process.
However, a bug was introduced in GoldenGate 26ai. If you create a custom profile with the adminclient, the web UI will sometimes hang when creating any new extract or replicat. This also affects 23ai installations that are patched to 26ai, so be very careful when patching an existing GoldenGate 23ai setup containing custom profiles ! After patching, you will not be able to access and manage your profiles anymore from the web UI.
To reproduce the bug, simply create an extract from the adminclient.
OGG (https://vmogg ogg_test_01) 1> info profile *
Auto Delay Auto Wait Reset Disable
Name Start Seconds Restart Retries Seconds Seconds on Failure
-------------------------------- ----- -------- ------- -------- -------- -------- ----------
Default No No
OGG (https://vmogg ogg_test_01) 2> add profile TestProfile autostart no
OGG (https://vmogg ogg_test_01) 3> info profile *
Auto Delay Auto Wait Reset Disable
Name Start Seconds Restart Retries Seconds Seconds on Failure
-------------------------------- ----- -------- ------- -------- -------- -------- ----------
Default No No
TestProfile No No
In a GoldenGate 23ai web UI, you will see the following when creating an extract, in the Managed Options step:
However, in GoldenGate 26ai, the extract creation hangs indefinitely, and no error gets reported.
Before the bug is solved by Oracle, what can you do ? The first thing you can do is modify the Default GoldenGate profile, so that all extracts with this profile are affected. However, it means that you cannot fine-tune the settings for different processes, with different needs. After all, it is the reason why you should define profiles in the first place.
But if you still want to overcome this bug and having working profiles on 26ai until the bug gets corrected, you have two ways of doing this:
- Create profiles from the web UI. You can do this in the Managed Process Profiles tab of your deployment. Profiles created through this tab do not make the web UI hang indefinitely.
- Create profiles with the REST API. As presented in this blog, you can do this in two steps :
- Creation of a new profile with the Create Configuration Value endpoint on the
ogg:managedProcessSettingstype. - Creation of a new configuration
isDefaultset toFalseon theogg:configDataDescriptiontype.
- Creation of a new profile with the Create Configuration Value endpoint on the
This way, you can create or recreate your profiles until the bug is solved.
L’article Overcome GoldenGate 26ai Bug On Custom Profiles est apparu en premier sur dbi Blog.
Scaling SSRS Migrations: Multi-Threaded Automation for PBIRS 2025
Modernizing a reporting platform is a pivotal milestone for any BI infrastructure. Whether it’s a standard upgrade or a forced transition to Power BI Report Server (PBIRS) following the decommissioning of SSRS in SQL Server 2025, the operation is critical. For the purposes of our lab, we will use an SSRS 2017 source, but the logic remains universal: regardless of the original version, the goal is to ensure the continuity of your decision-making services without sacrificing your mental health in the process.
As my colleague Amine Haloui explained in a recent blog post, several strategies exist for migrating an instance. The “Lift and Shift” method (restoring the ReportServer database onto a new instance) is often the most attractive on paper. However, the reality on the ground can be more temperamental.
In some production environments, the target PBIRS instance already exists, hosts its own content, or follows specific configurations that prohibit simply overwriting its underlying ReportServer database. Therefore, we are proceeding here on the premise of a selective and granular migration: we must inject the SSRS catalog into an active PBIRS environment without burning everything to the ground in the process.
When faced with inventories exceeding hundreds or even thousands of reports (RDL), folders, and datasources, a manual approach via the web interface is not an option and automation becomes a necessity.
This article analyzes a systematic approach based on the ReportingServicesTools PowerShell module. The objective is to provide a robust methodology to extract your catalog and redeploy it intelligently, while managing the necessary reconfigurations along the way.
To migrate cleanly, objects must first be isolated. The idea is not to blindly vacuum everything, but to target the critical folders of your SSRS instance and transform them into flat files (.rdl and .rds) within a local staging area. If your SSRS instance contains specific object types, the scripts can easily be adapted to include them as well.
This is where the power of the SOAP Proxy comes into play. Rather than multiplying slow HTTP calls, we use the native service interface to list and extract our components:
$sourceUrl = "http://your-ssrs-server/ReportServer"
$exportRoot = "H:\Migration_Dump"
$proxySource = New-RsWebServiceProxy -ReportServerUri $sourceUrl
In a production environment, SSRS folders are often a messy mix of reports, data sources, images, and sometimes obsolete semantic models. To maintain total control over what we export, we isolate the filtering logic.
This Get-AllItemsByType function allows us to retrieve only what truly matters to us, based on the TypeName and file extension returned by the API.
function Get-AllItemsByType {
param(
[string]$CurrentPath,
$Proxy,
[string]$TypeName
)
try {
return $Proxy.ListChildren($CurrentPath, $true) | Where-Object { $_.TypeName -eq $TypeName }
} catch {
Write-Host " [!] Error on $CurrentPath : $($_.Exception.Message)" -ForegroundColor Red
return $null
}
}
This mapping between the file type and its extension must be defined upfront in a dictionary:
$extensionMap = @{
"Report" = ".rdl"
"DataSource" = ".rds"
}
A crucial point in extracting SSRS objects is preserving their context. To ensure a seamless import into PBIRS 2025, we must recreate the exact folder hierarchy of the source server locally.
The trick lies in transforming the SSRS path (formatted as /Folder/SubFolder/Report) into a valid Windows path, while simultaneously handling the extension mapping (.rdl for reports, .rds for DataSources).
function Export-SsrsItems {
param(
[string]$RootPath,
$Proxy,
[string]$TypeName,
[string]$ExportRoot
)
$items = Get-AllItemsByType -CurrentPath $RootPath -Proxy $Proxy -TypeName $TypeName
foreach ($item in $items) {
$relativeItemPath = $item.Path.TrimStart('/').Replace("/", "\")
$localFilePath = Join-Path $ExportRoot $relativeItemPath
$localDirectory = Split-Path -Path $localFilePath -Parent
if (-not (Test-Path $localDirectory)) {
New-Item -ItemType Directory -Path $localDirectory -Force | Out-Null
}
Out-RsCatalogItem -Path $item.Path -Destination $localDirectory -Proxy $Proxy
}
}
By doing this, your H:\Migration_Dump becomes the exact mirror of your SSRS portal. This structural rigor is what will allow us, in the next step, to remap our data sources without having to hunt down which report belongs to which department.
Finally, we define the folders we wish to export along with the document types they contain (since a migration is often the perfect time for a bit of spring cleaning):
$exportTasks = @(
@{ Path = "/Migration_Source_2"; Types = @("Report") },
@{ Path = "/Data Sources"; Types = @("DataSource") }
)
Write-Host "--- Selective Export Started ---" -ForegroundColor Cyan
foreach ($task in $exportTasks) {
foreach ($typeName in $task.Types) {
$ext = $extensionMap[$typeName]
Export-SsrsItems `
-RootPath $task.Path `
-Proxy $proxySource `
-TypeName $typeName `
-Extension $ext `
-ExportRoot $exportRoot
}
}
Once the extraction is complete, you have a local mirror of your source instance, but the data sources still point to the legacy infrastructure.
Instead of manually fixing each connection after the import (the best way to miss half of them), we will apply an automated transformation directly to our local XML files. This allows us to update connection strings in bulk before a single report even hits the target server.
The idea is simple: use PowerShell to inject the new SQL instance wherever necessary, ensuring a functional deployment from the very first second:
$allDataSources = Get-ChildItem -Path $exportRoot -Filter "*.rds" -Recurse
Write-Host "[>] Datasources updated in : $exportRoot" -ForegroundColor Yellow
foreach ($dsFile in $allDataSources) {
[xml]$xmlContent = Get-Content $dsFile.FullName
$node = $xmlContent.SelectSingleNode("//ConnectString")
if ($null -ne $node) {
$oldValue = $node."#text"
if ($null -eq $oldValue) { $oldValue = $node.InnerText }
$newValue = $oldValue -replace "OLD_REPORTING_INSTANCE", "NEW_REPORTING_INSTANCE"
if ($oldValue -ne $newValue) {
$node.InnerText = $newValue
$xmlContent.Save($dsFile.FullName)
Write-Host " [v] ConnectString updated in : $($dsFile.Name)" -ForegroundColor Green
}
} else {
Write-Host " [!] ConnectString not found in file $($dsFile.Name)" -ForegroundColor Red
}
}
Moreover, since we are interacting directly with the file’s XML structure, this logic isn’t limited to connection strings: you can apply the same principle to automate changes for any XML property, from timeouts to provider names.
Phase 3: Mass Deployment – Rebuilding the Reporting PortalAt this stage, the operation is purely mechanical. We once again leverage the ReportingServicesTools module to recreate the folder structure and upload the .rds and .rdl files. By following this specific order, PBIRS will automatically restore the links between your reports and their newly patched data sources.
It is worth noting that the script allows for importing into a specific root folder (defined by the $destroot variable). This is particularly useful if you want to isolate the migrated assets into a dedicated directory, such as SSRS_Folder to keep them distinct from the existing hierarchy. Furthermore, this script is designed with safety in mind: it cannot overwrite or delete anything. If a report with the same name already exists in the same location, the -Overwrite:$false argument prevents replacement, ensuring that the import process never destroys existing content.
Here is the final block to complete your migration:
$destUrl = "http://your-pbirs-server/ReportServer"
$localDump = "H:\Migration_Dump"
$destRoot = "/" #Start import in the root folder
$proxyDest = New-RsWebServiceProxy -ReportServerUri $destUrl
$extensionMap = @{
"Report" = ".rdl"
"DataSource" = ".rds"
}
function Ensure-RsFolderBruteForce {
param($fullFolderPath, $Proxy)
$parts = $fullFolderPath.Split('/') | Where-Object { $_ -ne '' }
$currentPath = ''
foreach ($part in $parts) {
$parent = if ($currentPath -eq '') { "/" } else { $currentPath }
$target = if ($currentPath -eq '') { "/$part" } else { "$currentPath/$part" }
try {
$Proxy.CreateFolder($part, $parent, $null) | Out-Null
Write-Host " [DIR] Created : $target" -ForegroundColor Cyan
} catch {
if ($_.Exception.Message -match "AlreadyExists") {
# Folder already exists but we continue
} else {
Write-Host " [!] Error for folder $target : $($_.Exception.Message)" -ForegroundColor Red
}
}
$currentPath = $target
}
}
function Import-SsrsItem {
param(
[System.IO.FileInfo]$File,
[string]$LocalDump,
[string]$DestRoot,
$Proxy
)
$relativeDir = $File.DirectoryName.Replace($LocalDump, '').Replace("\", "/")
$targetFolderPath = ($DestRoot + $relativeDir).Replace("//", "/")
$fullItemPath = ($targetFolderPath + "/" + $File.BaseName).Replace("//", "/")
Ensure-RsFolderBruteForce -fullFolderPath $targetFolderPath -Proxy $Proxy
try {
Write-RsCatalogItem -Path $File.FullName -Destination $targetFolderPath -Proxy $Proxy -Overwrite:$false
Write-Host " [DONE] Imported: $fullItemPath" -ForegroundColor Green
}
catch {
if ($_.Exception.Message -match "already exists") {
Write-Host " [SKIP] Already created : $fullItemPath" -ForegroundColor Gray
} else {
Write-Host " [FAIL] Error $fullItemPath : $($_.Exception.Message)" -ForegroundColor Red
}
}
}
$importOrder = @("DataSource", "Report")
foreach ($typeName in $importOrder) {
$extension = $extensionMap[$typeName]
Write-Host "`n[PASS] Import of object with type : $typeName ($extension)" -ForegroundColor Magenta
$filesToImport = Get-ChildItem -Path $localDump -Filter "*$extension" -Recurse
if ($filesToImport.Count -eq 0) {
Write-Host " [i] No file with $extension found." -ForegroundColor Gray
continue
}
foreach ($file in $filesToImport) {
Import-SsrsItem -File $file -LocalDump $localDump -DestRoot $destRoot -Proxy $proxyDest
}
}
Write-Host "`nImport done!" -ForegroundColor Green
Importing via SOAP is more resource-intensive than extraction, as the server must validate every piece of metadata and physically recreate the path for each report. On large volumes, this stage can become a bottleneck (averaging ~1 second per report).
To overcome this, we can parallelize the import by folder, creating multiple background jobs running on separate threads. Here is the general skeleton to implement this multi-threaded approach:
$maxJobs = 5
foreach ($file in $filesToImport) {
while ((Get-Job -State Running).Count -ge $maxJobs) {
Start-Sleep -Milliseconds 500
}
Start-Job -Name "Import_$($file.Name)" -ScriptBlock {
param($f, $url, $targetPath)
$Proxy = New-RsWebServiceProxy -ReportServerUri $url
try {
Write-RsCatalogItem -Path $f.FullName -Destination $targetPath -Proxy $Proxy -Overwrite:$false
return "SUCCESS: $($f.Name)"
} catch {
return "ERROR: $($f.Name) -> $($_.Exception.Message)"
}
} -ArgumentList $file, $destUrl, $targetFolderPath
}
Note : The -Parallel parameter is a feature of the ForEach-Object cmdlet introduced in PowerShell 7 to enable native multi-threading. While it allows for processing multiple objects simultaneously, it is not reliably supported by the ReportingServicesTools library as the underlying API is not thread-safe. To ensure stability and avoid session collisions, it is recommended to use the Start-Job method instead, as it provides better process isolation for each task.
Migrating to Power BI Report Server shouldn’t be a manual challenge. By adopting this PowerShell-driven ETL approach, you replace the uncertainty of manual intervention with industrial-grade rigor.
The primary advantage lies in consistency: regardless of the report volume or folder complexity, the script guarantees an identical and predictable result every single time. By isolating extraction, XML transformation, and ordered importation, you maintain total control over your data integrity.
Ultimately, automation is about securing your delivery and freeing up time for what truly matters: leveraging your data on your brand-new PBIRS 2025 platform.
Happy migrating!
L’article Scaling SSRS Migrations: Multi-Threaded Automation for PBIRS 2025 est apparu en premier sur dbi Blog.
Monitoring GoldenGate Certificates Expiration
Your GoldenGate certificates should be granted by a trusted Certificate Authority, which normally handles the monitoring of certificates. However, if you don’t want your future you, or a colleague, to spend too much time debugging which certificate should be renewed, you could monitor your certificates. In this blog, I will present a way to do this with the REST API.
Certificates types in GoldenGateThe first thing you need to know is that there are three types of certificates in a GoldenGate deployment. Each of them can be monitored separately in the REST API.
- Server Certificates, which belong to the
servertype - Client Certificates, which belong to the
clienttype - CA Certificates, which belong to the
truststoretype
Before presenting the REST API monitoring of certificates, let’s see what we are supposed to be looking at. From the web UI, when connected to the Service Manager, you can observe the details of your certificates in the Certificate Management tab. I give below an example for both the Service Manager and a deployment in a secure GoldenGate installation.
I create short-term certificates on purpose for the example, and we see that the UI is designed to tell the user when the certificates are close to expiration. But of course, it’s better to have some sort of monitoring do the job for us.
Certificate monitoring with the REST APIWith GoldenGate REST API, it is rather easy to monitor certificate expiration. Unfortunately, you cannot ask the API for a list of certificates close to expiration, so you will have to iterate over all certificates in your monitoring script.
I will use the Python client I presented in another blog, but you can of course rebuild each call manually. Here are the methods / endpoints you should use:
- List Deployments –
GET /services/{version}/deployments, to retrieve the list of deployments. The Service Manager is considered as a normal deployment here, so there is no need to separate it from the other deployments. - Retrieve Available Certificate Types –
GET /services/{version}/deployments/{deployment}/certificates, to retrieve the collection of certificate types. It should always be the list given earlier (client,serverandtruststore). - Retrieve Certificate Types –
GET /services/{version}/deployments/{deployment}/certificates/{type}, to get the list of certificates that belong to a specific type inside a deployment. - Retrieve Certificate Information –
GET /services/{version}/deployments/{deployment}/certificates/{type}/{certificate}/info, to retrieve the information on a specific certificate.
I share below a full monitoring script that you can use to monitor all certificates in a GoldenGate setup. Feel free to adapt it to your own monitoring tool.
#!/usr/bin/env python3
"""
Oracle GoldenGate Certificate Monitoring Script
"""
from datetime import datetime, timezone
import sys
from oggrestapi import OGGRestAPI
if __name__ == '__main__':
exit_code = 0 # 0=OK, 1=WARNING, 2=CRITICAL, 3=UNKNOWN
try:
# Initialize the OGG REST API client
ogg_client = OGGRestAPI(
url='https://vmogg:7809',
username='ogg',
password='ogg',
verify_ssl=False)
# Retrieve the list of deployments
deployments = ogg_client.list_deployments()
print(f"Deployments: {[d['name'] for d in deployments]}")
# For each deployment, retrieve the list of certificates and check their expiration dates
for deployment in deployments:
deployment_name = deployment['name']
print(f" Checking certificates for deployment: {deployment_name}")
certificate_types = ogg_client.retrieve_available_certificate_types_deployment(
deployment=deployment_name)
print(f" Certificate types for deployment {deployment_name}: {[ct['name'] for ct in certificate_types]}")
for cert_type in certificate_types:
cert_type_name = cert_type['name']
print(f" Checking certificates for type: {cert_type_name}")
certificates = ogg_client.retrieve_certificate_types(
deployment=deployment_name,
type=cert_type_name)
for cert in certificates:
cert_name = cert['name']
print(f" Certificate: {cert_name}")
certificate_information = ogg_client.retrieve_certificate_information_deployment(
deployment=deployment_name,
type=cert_type_name,
certificate=cert_name)
expiration_date = certificate_information['certificate']['validTo']
print(f" Certificate Expiry Date: {expiration_date}")
expire_in = datetime.strptime(expiration_date, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) - datetime.now(timezone.utc)
is_expired = expire_in.total_seconds() < 0
if is_expired:
print(f" WARNING: Certificate '{cert_name}' in deployment '{deployment_name}' of type '{cert_type_name}' has expired on {expiration_date}")
exit_code = max(exit_code, 2) # CRITICAL
else:
days_left = int(expire_in.total_seconds() / 86400)
print(f" Certificate '{cert_name}' in deployment '{deployment_name}' of type '{cert_type_name}' will expire in {days_left} days on {expiration_date}")
if days_left < 30:
exit_code = max(exit_code, 1) # WARNING
except Exception as e:
print(f"UNKNOWN: {e}")
sys.exit(3)
sys.exit(exit_code)
On an installation where certificates are close to expire, the output looks like this:
Deployments: ['ServiceManager', 'ogg_test_01']
Checking certificates for deployment: ServiceManager
Certificate types for deployment ServiceManager: ['client', 'server', 'truststore']
Checking certificates for type: client
Checking certificates for type: server
Certificate: default
Certificate Expiry Date: 2026-03-31T05:54:15Z
Certificate 'default' in deployment 'ServiceManager' of type 'server' will expire in 0 days on 2026-03-31T05:54:15Z
Checking certificates for type: truststore
Checking certificates for deployment: ogg_test_01
Certificate types for deployment ogg_test_01: ['client', 'server', 'truststore']
Checking certificates for type: client
Certificate: default
Certificate Expiry Date: 2026-03-31T05:54:15Z
Certificate 'default' in deployment 'ogg_test_01' of type 'client' will expire in 0 days on 2026-03-31T05:54:15Z
Checking certificates for type: server
Certificate: default
Certificate Expiry Date: 2026-03-31T05:54:15Z
Certificate 'default' in deployment 'ogg_test_01' of type 'server' will expire in 0 days on 2026-03-31T05:54:15Z
Checking certificates for type: truststore
Certificate: XCertUser-467fd0986deb
Certificate Expiry Date: 2026-03-31T05:54:15Z
Certificate 'XCertUser-467fd0986deb' in deployment 'ogg_test_01' of type 'truststore' will expire in 0 days on 2026-03-31T05:54:15Z
Certificate: installed_0
Certificate Expiry Date: 2026-03-31T05:54:15Z
Certificate 'installed_0' in deployment 'ogg_test_01' of type 'truststore' will expire in 0 days on 2026-03-31T05:54:15Z
L’article Monitoring GoldenGate Certificates Expiration est apparu en premier sur dbi Blog.
Change GoldenGate Default Extract Profile
As part of a GoldenGate setup automation, changing the Default profile of your extracts and replicats seems like a good start if you don’t want to deal with custom profiles or changing each individual configuration.
Unfortunately, there is no easy way to modify an existing profile from the adminclient (there is no alter profile command). The Default profile makes no exception, so you will have to use the REST API for this. In this blog, I will present two ways of doing it:
- Updating the
Defaultprofile directly. - Creating a custom profile, and setting it as
Default.
Using the Python client for GoldenGate I presented in another blog post, you can easily create a session connecting to your GoldenGate setup and retrieve the existing configuration. For this, we will use the following methods / endpoints:
retrieve_configuration_valueto get the current configuration (GET /services/{version}/config/types/{type}/values/{value})replace_configuration_valueto update the profile (PUT /services/{version}/config/types/{type}/values/{value})
from oggrestapi import OGGRestAPI
ogg_client=OGGRestAPI(
url="https://vmogg:7810",
username="ogg",
password="ogg"
)
ogg_client.retrieve_configuration_value(
value='ogg:managedProcessSettings:Default',
type='ogg:managedProcessSettings')
This gives us the basic configuration of all new extracts and replicats in GoldenGate. Let’s see the default values:
>>> ogg_client.retrieve_configuration_value(
value='ogg:managedProcessSettings:Default',
type='ogg:managedProcessSettings')
{'$schema': 'ogg:managedProcessSettings', 'autoStart': {'enabled': False, 'delay': 0}, 'autoRestart': {'enabled': False, 'onSuccess': False, 'delay': 0, 'retries': 9, 'window': 60, 'disableOnFailure': True}}
Let’s have a look at the different parameters here:
autoStart.enabled: whether the process will start automatically after the Administration Server starts.autoStart.delay: delay in seconds before starting the process.autoRestart.enabled: whether to restart the process after it fails.autoRestart.onSuccess: the process is only restarted if it fails.autoRestart.delay: waiting time (in seconds) before attempting to restart a process once it fails.autoRestart.retries: maximum number of retries before stopping restart attempts.autoRestart.window: timeframe before GoldenGate will attempt to restart the process again.autoRestart.disableOnFailure: if set to True, GoldenGate will disable the restart if it fails to restart within theretries/windowsetting. You will have to start the process manually if this happens.
Default profile directly
To update the Default profile, just create your own configuration and use the following example (based on the description given above) to push it to your GoldenGate deployment. Here, for instance, autoStart will be delayed by 30 seconds, and the process will try to restart every 5 minutes, and stop trying to restart after six retries. It will attempt to start again after two hours.
ogg_client.replace_configuration_value(
value='ogg:managedProcessSettings:Default',
type='ogg:managedProcessSettings',
data={
'$schema': 'ogg:managedProcessSettings',
'autoStart': {
'enabled': True,
'delay': 30
},
'autoRestart': {
'enabled': True,
'retries': 6,
'delay': 300,
'window': 7200,
'onSuccess': False,
'disableOnFailure': False
}
}
)
We can check by retrieving the configuration again.
# Checking new configuration with the REST API
ogg_client.retrieve_configuration_value(
value='ogg:managedProcessSettings:Default',
type='ogg:managedProcessSettings')
{'$schema': 'ogg:managedProcessSettings', 'autoStart': {'enabled': True, 'delay': 30}, 'autoRestart': {'enabled': True, 'retries': 6, 'delay': 300, 'window': 7200, 'onSuccess': False, 'disableOnFailure': False}}
Or you can check in the web UI in the Managed Process Profiles tab.
Default
If for some reason you would rather keep the Default profile and have a custom profile as default, you have to create a new profile first and set it as default. To do this, we use the create_configuration_value method / endpoint, which is the same endpoint as before but with the POST verb. If we keep the same profile as in the previous example, here is the script to run. Only the method changes, as well as the value, where Default is changed with the name of your profile (NewDefaultProfile, here).
ogg_client.create_configuration_value(
value='ogg:managedProcessSettings:NewDefaultProfile',
type='ogg:managedProcessSettings',
data={
'$schema': 'ogg:managedProcessSettings',
'autoStart': {
'enabled': True,
'delay': 30
},
'autoRestart': {
'enabled': True,
'retries': 6,
'delay': 300,
'window': 7200,
'onSuccess': False,
'disableOnFailure': False
}
}
)
After this, you need to do two things:
- Create the
isDefaultflag for your new profile, and set it toTrue. This is done with the samecreate_configuration_valuemethod, but on a different type calledogg:configDataDescription. - Update the
isDefaultflag for theDefaultprofile toFalse. Since the property already exists, we will use thereplace_configuration_valuemethod.
Here is how to do the creation of the flag:
ogg_client.create_configuration_value(
value='ogg:managedProcessSettings:NewDefaultProfile',
type='ogg:configDataDescription',
data={
'isDefault': True
}
)
And to update the Default profile:
ogg_client.replace_configuration_value(
value='ogg:managedProcessSettings:Default',
type='ogg:configDataDescription',
data={
'isDefault': False
}
)
From now on, any new extract or replicat will be assigned to this NewDefaultProfile !
L’article Change GoldenGate Default Extract Profile est apparu en premier sur dbi Blog.
MySQL Metadata Store service not able to start on an ODA
I was recently doing some consulting on some ODA at one of our customers, and I faced an issue with the DCS Agent and the MySQL Metadata Store. In this blog, I will show you the problem and how I could resolved it.
Problem descriptionOn an Oracle Database Appliance, the DCS agent is the software providing the full automation to manage the appliance. It will have no impact on the well working of the databases but is mandatory to manage the appliance (patching, create dbhome, create database and any appliance resources). The DCS agent will perform various operations on the appliance, such as provisioning, database lifecycle management, backup and recovery, and storage management. The DCS Agent will use a MySQL database to store the metadata. In order for the DCS Agent to work, the MySQL database needs to be up and running.
On the node 0 of an ODA X9-2-HA, where I was preparing the next database patching operation, I wanted to run some cleanup.
[root@ODA-HA_node0 ~]# odacli cleanup-patchrepo -comp DB,GI -v 19.20.0.0.0 DCS-10009:Failed to create new service job report.
For which I could get a DCS failure.
I checked the job and realised that an ODA cleaning job where still in running status for about 3 months…
[root@ODA-HA_node0 ~]# odacli list-jobs ID Description Created Status ---------------------------------------- --------------------------------------------------------------------------- ----------------------------------- ---------------- 80f2bfbc-a02e-47bb-b57c-754d3d6d3d3a Server Patching 2025-11-12 08:00:14 CET Success 2e36d676-f175-402d-a7e4-735b4eb965de Patch pre-checks for [STORAGE] 2025-11-12 08:58:19 CET Success ... c4611880-fdc8-44fd-90a5-c2dd27d12f7c Auto purge job data 2025-12-09 03:59:48 CET Success ed09f270-5eff-4141-9b7b-51a2e5d30e32 Auto purge job data 2025-12-10 03:59:50 CET Success aecd8ca5-44fb-4e2c-af2b-0e42e954381a Auto purge job data 2025-12-11 03:59:52 CET Running [root@ODA-HA_node0 ~]#Troubleshooting
I restarted the DCS agent.
[root@ODA-HA_node0 ~]# systemctl stop initdcsagent
[root@ODA-HA_node0 ~]# systemctl start initdcsagent
[root@ODA-HA_node0 ~]# systemctl status initdcsagent
● initdcsagent.service - Oracle dcs-agent startup
Loaded: loaded (/etc/systemd/system/initdcsagent.service; enabled; vendor preset: disabled)
Active: active (running) since Wed 2026-03-18 14:35:41 CET; 2s ago
Main PID: 92038 (sh)
Tasks: 96 (limit: 79998)
Memory: 331.7M
CGroup: /system.slice/initdcsagent.service
├─92038 /bin/sh -c . /opt/oracle/dcs/bin/setupJreAgent.sh;LD_LIBRARY_PATH=/opt/oracle/rhp/lib:$JRE_HOME/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib $JAVA -Xms128m -Xmx256m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:+DisableExplicitGC -XX:ParallelGCThreads=4 -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:/opt/oracle/dcs/log/gc-dcs-agent-%t-%p.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFi>
└─92039 /opt/oracle/dcs/java/1.8.0_441/bin/java -Xms128m -Xmx256m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:+DisableExplicitGC -XX:ParallelGCThreads=4 -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:/opt/oracle/dcs/log/gc-dcs-agent-%t-%p.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=10M -Doracle.security.jps.config=/opt/oracle/dcs/agent/jps-config.xml -Dio.nett>
Mar 18 14:35:41 ODA-HA_node0 systemd[1]: Started Oracle dcs-agent startup.
[root@ODA-HA_node0 ~]# odacli list-jobs | tail -n3
Which did not help. In the DCS Agent log I could find some errors with HAMI. Oracle HAMI is the Oracle High Availability Metadata Infrastructure service providing distributed services required by DCS including locking and synchronizing configuration details in the cluster.
[root@ODA-HA_node0 log]# view dcs-agent-infra.log
...
...
...
2026-03-18 14:59:37,776 DEBUG [DcsInfraProvisioningThread] [] c.o.d.i.h.c.CommandExecutor: Going to execute command '/opt/oracle/dcs/hami/bin/hamictl.sh ssh-equiv --ensemble ODA_DCS --hosts ODA-HA_node0-priv,ODA-HA_node1-priv --json'
2026-03-18 14:59:38,014 DEBUG [DcsInfraProvisioningThread] [] c.o.d.i.h.c.CommandExecutor: Got result from execution of '/opt/oracle/dcs/hami/bin/hamictl.sh ssh-equiv --ensemble ODA_DCS --hosts ODA-HA_node0-priv,ODA-HA_node1-priv --json':
- RC: 1
- OUTPUT: /opt/oracle/dcs/oda-python3/lib/python3.12/getpass.py:91: GetPassWarning: Can not control echo on the terminal.
Warning: Password input may be echoed.
hamictl_oda_dcs's password: *********
{"operation": "ssh-equiv", "result": "FAILED", "message": "Invalid password"}
2026-03-18 14:59:38,015 ERROR [DcsInfraProvisioningThread] [] c.o.d.a.i.DcsInfraProvisioningThread: Could not provision DCS infrastructure
com.oracle.dcs.infra.hams.ctl.HamiCtl$HamiCtlException: Invalid password
I decided to restart MySQL service.
[root@ODA-HA_node0 ~]# systemctl stop oda-mysql [root@ODA-HA_node0 ~]# systemctl start oda-mysql Job for oda-mysql.service failed because the control process exited with error code. See "systemctl status oda-mysql.service" and "journalctl -xe" for details.
Which failed…
Checking the logs:
[root@ODA-HA_node0 ~]# journalctl -xe ... -- Unit oda-mysql.service has begun starting up. Mar 31 09:23:10 ODA-HA_node0 systemd[90052]: oda-mysql.service: Failed to execute command: Permission denied Mar 31 09:23:10 ODA-HA_node0 systemd[90052]: oda-mysql.service: Failed at step EXEC spawning /opt/oracle/dcs/mysql/bin/mysqld: Permission denied
I could see that it was complaining about permissions.
I checked the permissions on the MySQL binary:
[root@ODA-HA_node0 ~]# ls -ltrh /opt/oracle/dcs/mysql/bin/mysqld -rwxr-x--- 1 odamysql odamysql 338M Mar 26 16:55 /opt/oracle/dcs/mysql/bin/mysqld [root@ODA-HA_node0 ~]#
And that one was ok. The linux user starting MySQL, odamysql, was having the right permissions.
I then checked all permissions including the directories:
[root@ODA-HA_node0 log]# ls -ld /opt /opt/oracle /opt/oracle/dcs /opt/oracle/dcs/mysql /opt/oracle/dcs/mysql/bin drwxrwx--- 11 root oinstall 4096 Nov 21 13:58 /opt drwxr-xr-x. 9 root root 4096 Nov 14 16:31 /opt/oracle drwxr-xr-x. 30 root root 4096 Nov 14 12:40 /opt/oracle/dcs drwxr-xr-x. 13 root root 4096 Nov 14 09:10 /opt/oracle/dcs/mysql drwxr-x---. 2 odamysql odamysql 4096 Mar 26 16:55 /opt/oracle/dcs/mysql/bin [root@ODA-HA_node0 log]#
And I could see that there were wrong on the /opt directory… The odamysql directory is not able to enter and read in the /opt directory, as it does not belong to the group and there is no permissions on the other users…
I checked on a valid ODA and as expected the permissions should be:
drwxr-xr-x. 11 root root 4096 Mar 18 10:02 /opt
So this is mainly due to an human mistake…
ResolutionI first checked and could confirm the issue only affect the /opt directory itself.
[root@ODA-HA_node0 opt]# pwd /opt [root@ODA-HA_node0 opt]# ls -ltrh total 48K drwxr-xr-x. 3 root root 4.0K Sep 17 2024 lsi drwxr-xr-x 2 root root 4.0K Jan 15 2025 oracle.ahf drwx------. 2 root root 16K Nov 11 15:42 lost+found drwxr-xr-x. 4 root root 4.0K Nov 11 16:32 MegaRAID drwxr-xr-x 3 root root 4.0K Nov 12 10:20 ORCLfmap drwxr-xr-x 5 root root 4.0K Nov 12 13:36 odabr drwxr-xr-x. 9 root root 4.0K Nov 14 16:31 oracle drwxrwx--- 16 root oinstall 4.0K Feb 16 11:19 commvault drwx------ 11 root root 4.0K Mar 30 02:08 splunkforwarder [root@ODA-HA_node0 opt]#
I change the permissions for other users.
[root@ODA-HA_node0 /]# chmod o+rx /opt [root@ODA-HA_node0 /]# ls -ld /opt /opt/oracle /opt/oracle/dcs /opt/oracle/dcs/mysql /opt/oracle/dcs/mysql/bin drwxrwxr-x 11 root oinstall 4096 Nov 21 13:58 /opt drwxr-xr-x. 9 root root 4096 Nov 14 16:31 /opt/oracle drwxr-xr-x. 30 root root 4096 Nov 14 12:40 /opt/oracle/dcs drwxr-xr-x. 13 root root 4096 Nov 14 09:10 /opt/oracle/dcs/mysql drwxr-x---. 2 odamysql odamysql 4096 Mar 26 16:55 /opt/oracle/dcs/mysql/bin [root@ODA-HA_node0 /]#
And I could successfully start the MySQL service.
[root@ODA-HA_node0 /]# systemctl start oda-mysql
[root@ODA-HA_node0 /]# systemctl status oda-mysql
● oda-mysql.service - MySQL Server for ODA
Loaded: loaded (/etc/systemd/system/oda-mysql.service; enabled; vendor preset: disabled)
Active: active (running) since Tue 2026-03-31 09:55:48 CEST; 6s ago
Docs: man:mysqld(8)
http://dev.mysql.com/doc/refman/en/using-systemd.html
Main PID: 55837 (mysqld)
Status: "Server is operational"
Tasks: 33 (limit: 79998)
Memory: 423.5M
CGroup: /system.slice/oda-mysql.service
└─55837 /opt/oracle/dcs/mysql/bin/mysqld --defaults-file=/opt/oracle/dcs/mysql/etc/mysqldb.cnf --skip-mysqlx
Mar 31 09:55:48 ODA-HA_node0 systemd[1]: Starting MySQL Server for ODA...
Mar 31 09:55:48 ODA-HA_node0 systemd[1]: Started MySQL Server for ODA.
[root@ODA-HA_node0 /]#
And I could successfully start the DCS Agent.
[root@ODA-HA_node0 /]# systemctl status initdcsagent
● initdcsagent.service - Oracle dcs-agent startup
Loaded: loaded (/etc/systemd/system/initdcsagent.service; enabled; vendor preset: disabled)
Active: inactive (dead) since Wed 2026-03-18 15:22:48 CET; 1 weeks 5 days ago
Main PID: 84546 (code=killed, signal=TERM)
Mar 19 16:18:04 ODA-HA_node0 systemd[1]: Dependency failed for Oracle dcs-agent startup.
Mar 19 16:18:04 ODA-HA_node0 systemd[1]: initdcsagent.service: Job initdcsagent.service/start failed with result 'dependency'.
Mar 24 13:38:38 ODA-HA_node0 systemd[1]: Dependency failed for Oracle dcs-agent startup.
Mar 24 13:38:38 ODA-HA_node0 systemd[1]: initdcsagent.service: Job initdcsagent.service/start failed with result 'dependency'.
Mar 25 09:46:35 ODA-HA_node0 systemd[1]: Dependency failed for Oracle dcs-agent startup.
Mar 25 09:46:35 ODA-HA_node0 systemd[1]: initdcsagent.service: Job initdcsagent.service/start failed with result 'dependency'.
Mar 26 11:06:50 ODA-HA_node0 systemd[1]: Dependency failed for Oracle dcs-agent startup.
Mar 26 11:06:50 ODA-HA_node0 systemd[1]: initdcsagent.service: Job initdcsagent.service/start failed with result 'dependency'.
Mar 30 16:07:15 ODA-HA_node0 systemd[1]: Dependency failed for Oracle dcs-agent startup.
Mar 30 16:07:15 ODA-HA_node0 systemd[1]: initdcsagent.service: Job initdcsagent.service/start failed with result 'dependency'.
[root@ODA-HA_node0 /]# systemctl start initdcsagent
[root@ODA-HA_node0 /]# systemctl status initdcsagent
● initdcsagent.service - Oracle dcs-agent startup
Loaded: loaded (/etc/systemd/system/initdcsagent.service; enabled; vendor preset: disabled)
Active: active (running) since Tue 2026-03-31 09:57:34 CEST; 1s ago
Main PID: 59253 (sh)
Tasks: 96 (limit: 79998)
Memory: 270.0M
CGroup: /system.slice/initdcsagent.service
├─59253 /bin/sh -c . /opt/oracle/dcs/bin/setupJreAgent.sh;LD_LIBRARY_PATH=/opt/oracle/rhp/lib:$JRE_HOME/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib $JAVA -Xms128m -Xmx256m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:+DisableExplicitGC -XX:ParallelGCThreads=4 -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:/opt/oracle/dcs/log/gc-dcs-agent-%t-%p.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFi>
└─59254 /opt/oracle/dcs/java/1.8.0_441/bin/java -Xms128m -Xmx256m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:+DisableExplicitGC -XX:ParallelGCThreads=4 -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:/opt/oracle/dcs/log/gc-dcs-agent-%t-%p.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=10M -Doracle.security.jps.config=/opt/oracle/dcs/agent/jps-config.xml -Dio.nett>
Mar 31 09:57:34 ODA-HA_node0 systemd[1]: Started Oracle dcs-agent startup.
[root@ODA-HA_node0 /]#
And I could successfully run the cleanup:
[root@ODA-HA_node0 ~]# odacli cleanup-patchrepo -comp DB,GI -v 19.20.0.0.0
{
"jobId" : "08b395be-ae5a-4d6f-8e5d-1c0fa232f672",
"status" : "Created",
"message" : "",
"reports" : [ ],
"createTimestamp" : "March 31, 2026 11:21:58 CEST",
"resourceList" : [ ],
"description" : "Cleanup patchrepos",
"updatedTime" : "March 31, 2026 11:21:58 CEST",
"jobType" : null,
"cpsMetadata" : null
}
[root@ODA-HA_node0 ~]# odacli describe-job -i 08b395be-ae5a-4d6f-8e5d-1c0fa232f672
Job details
----------------------------------------------------------------
ID: 08b395be-ae5a-4d6f-8e5d-1c0fa232f672
Description: Cleanup patchrepos
Status: Success
Created: March 31, 2026 11:21:58 CEST
Message:
Task Name Node Name Start Time End Time Status
---------------------------------------- ------------------------- ---------------------------------------- ---------------------------------------- ----------------
Cleanup Repository ODA-HA_node1 March 31, 2026 11:21:58 CEST March 31, 2026 11:21:59 CEST Success
Cleanup old ASR rpm ODA-HA_node0 March 31, 2026 11:21:59 CEST March 31, 2026 11:21:59 CEST Success
[root@ODA-HA_node0 ~]#
Finally, I have also changed the group for /opt accordingly to root group.
[root@ODA-HA_node0 /]# chgrp root /opt [root@ODA-HA_node0 /]# ls -ld /opt /opt/oracle /opt/oracle/dcs /opt/oracle/dcs/mysql /opt/oracle/dcs/mysql/bin drwxrwxr-x 11 root root 4096 Nov 21 13:58 /opt drwxr-xr-x. 9 root root 4096 Nov 14 16:31 /opt/oracle drwxr-xr-x. 30 root root 4096 Nov 14 12:40 /opt/oracle/dcs drwxr-xr-x. 13 root root 4096 Nov 14 09:10 /opt/oracle/dcs/mysql drwxr-x---. 2 odamysql odamysql 4096 Mar 26 16:55 /opt/oracle/dcs/mysql/bin [root@ODA-HA_node0 /]#To wrap up…
DCS Agent and MySQL database will have no effect on the well working of your databases, but if they are not working you are not able to manage your appliance any more. Here, wrong permissions (for sure a human mistake) had sad effects on appliance software. Solving the permissions issue resolved my problem.
L’article MySQL Metadata Store service not able to start on an ODA est apparu en premier sur dbi Blog.
The Undocumented GoldenGate ‘all’ Role You Should Know About
Let me tell you a story about how I discovered an undocumented role in GoldenGate that could break your automation. Recently, I wanted to do a massive update of the role of a GoldenGate user on multiple deployments. First of all, you should know that GoldenGate users are assigned to a role within the following list: User, Operator, Administrator and Security.
I will not dwell on the differences between them, but you just have to know something important here: you cannot change the role assigned to a GoldenGate user. If you try to edit a user, the only thing that you can change is the password.
That being said, the only possible option to change the role of a user in GoldenGate is to recreate it. In the web UI, it is rather simple. You just delete it, and recreate it with the same credentials, but a different role.
But as mentioned, I wanted to do it for multiple deployments and users, without having to do the operation manually.every time. That is why I decided to use the GoldenGate REST API to quickly iterate over users and deployments. And this is where the trouble began.
According to the GoldenGate documentation on the REST API, there are two endpoints that appear to replicate the web UI behavior :
- Delete User :
DELETE /services/{version}/authorizations/{role}/{user}
- Create User :
POST /services/{version}/authorizations/{role}/{user}
However, when calling the API with these endpoints and the Python client I presented in another blog, I had the following error after trying to recreate the user:
# Deletion works as intended
>>> ogg_client.delete_user('dbiblog','Operator')
{'$schema': 'api:standardResponse', 'links': [{'rel': 'canonical', 'href': 'https://vmogg/se/Operator', 'mediaType': 'application/json'}, {'rel': 'self', 'href': 'https://vmogg/se/Operator', 'mediaType': 'application/json'}], 'messages': []}
# Creation doesn't work
>>> ogg_client.create_user('dbiblog', 'User', data={'credential':'**'})
Exception: ERROR - https://vmogg/services/ogg_test_01/adminsrvr/v2/authorizations/Operator/dbiblog: The specified user already exists.
In the UI, the user was gone, but I couldn’t recreate it with the REST API. And even from the UI, the user couldn’t be recreated. Instead, I had the following error: OGG-12430 : The specified user already exists.
At first, I thought something was wrong with either my deployment or my client, but I found nothing wrong. So I checked the restapi.log files of my deployment to understand:
- What the REST API calls were doing
- What the web UI was doing.
restapi.log analysis
Let’s try to create the exact same user (dbiblog / Operator) from the UI, and see what passes through the API. For more insight about how to query the restapi.log file efficiently, you can read a blog I wrote on the topic. Otherwise, you can just search through the logs.
As shown below, the creation of a user from the UI only generated one call to the API (excluding GET calls) to the same endpoint used by my method.
{
"content": {
"credential": "** Masked **",
"type": "Basic"
},
"uri": "/services/v2/authorizations/Operator/dbiblog",
"uriTemplate": "/services/{version}/authorizations/{role}/{user}",
"verb": "POST",
"restapi_datetime": "2026-03-27 05:40:58.776+0000",
"restapi_reqno": 1840,
"restapi_service": "adminsrvr",
"restapi_status": "INFO"
}
Now, when deleting the user from the UI, two DELETE operations go through the API.
{
"content": null,
"uri": "/services/v2/authorizations/Operator/dbiblog",
"uriTemplate": "/services/{version}/authorizations/{role}/{user}",
"verb": "DELETE",
"restapi_datetime": "2026-03-27 05:42:20.994+0000",
"restapi_reqno": 1926,
"restapi_service": "adminsrvr",
"restapi_status": "INFO"
}
{
"content": null,
"uri": "/services/v2/authorizations/all/dbiblog",
"uriTemplate": "/services/{version}/authorizations/{role}/{user}",
"verb": "DELETE",
"restapi_datetime": "2026-03-27 05:42:21.012+0000",
"restapi_reqno": 1927,
"restapi_service": "adminsrvr",
"restapi_status": "INFO"
}
On top of making a DELETE call to the dbiblog / Operator pair, the UI generates another DELETE call on the dbiblog user with an undocumented all role !
This explains why you get an error when recreating a user with the REST API. If the user was deleted by a single REST API call, it still exists with the hidden all role.
To verify this hypothesis, let’s look at the list of users with the Operator and all roles before and after the user creation. Before, we only have the default ogg user (with the Security role, not shown here).
>>> ogg_client.list_users('Operator')
[]
>>> ogg_client.list_users('all')
[{'username': 'ogg'}]
But after, the newly created user also gets the all role.
>>> ogg_client.create_user(user='dbiblog', role='Operator', data={'credential':'**'})
{'$schema': 'api:standardResponse', 'links': [{'rel': 'canonical', 'href': 'https://vmogg/ser/Operator/dbiblog', 'mediaType': 'application/json'}, {'rel': 'self', 'href': 'https://vmogg/ser/Operator/dbiblog', 'mediaType': 'application/json'}], 'messages': []}
>>> ogg_client.list_users('Operator')
[{'username': 'dbiblog'}]
>>> ogg_client.list_users('all')
[{'username': 'ogg'}, {'username': 'dbiblog'}]
And of course, when deleting the user, I was only deleting it with the role I knew about.
How to recreate a GoldenGate user with the REST API ?The conclusion is simple: to recreate a GoldenGate user with the REST API, you need to delete the user on its current role, but also on the all role. After doing this, you can recreate the user:
# User deletion on the 'Operator' role
>>> ogg_client.delete_user(user='dbiblog', role='Operator')
{'$schema': 'api:standardResponse', 'links': [{'rel': 'canonical', 'href': 'https://vmogg/Operator', 'mediaType': 'application/json'}, {'rel': 'self', 'href': 'https://vmogg/ser/Operator', 'mediaType': 'application/json'}], 'messages': []}
# User deletion on the 'all' role
>>> ogg_client.delete_user(user='dbiblog', role='all')
{'$schema': 'api:standardResponse', 'links': [{'rel': 'canonical', 'href': 'https://vmogg/ser/All', 'mediaType': 'application/json'}, {'rel': 'self', 'href': 'https://vmogg/ser/All', 'mediaType': 'application/json'}], 'messages': []}
# User creation on the 'Operator' role
>>> ogg_client.create_user(user='dbiblog', role='Operator', data={'credential':'**'})
{'$schema': 'api:standardResponse', 'links': [{'rel': 'canonical', 'href': 'https://vmogg/ser/Operator/dbiblog', 'mediaType': 'application/json'}, {'rel': 'self', 'href': 'https://vmogg/ser/Operator/dbiblog', 'mediaType': 'application/json'}], 'messages': []}
L’article The Undocumented GoldenGate ‘all’ Role You Should Know About est apparu en premier sur dbi Blog.
OTDS – Installation of Replicas fail if the OT Admin password is too long?
For simplicity, in this blog, I will refer to the first OTDS instance as the “Primary” (the synchronization master host, installed with ISREPLICA_TOPOLOGY=0=FALSE) and any additional instances as “Replicas” (installed with ISREPLICA_TOPOLOGY=1=TRUE). Over the past few years, I have installed and worked on around 20–30 different OTDS environments, some with a single instance and others with multiple instances (HA). Overall, it is not a bad piece of software, even though it could use improvements in certain areas (e.g.: c.f. this blog). However, it was only after I started installing a few Replicas on recent OTDS versions (using a database backend instead of OpenDJ) that I encountered a rather unusual issue.
1. OTDS Replica installation failureSingle-instance installations using the silent properties file were always successful, and most multi-instance installations worked as well. However, I encountered a very specific issue twice: the Primary instance would install successfully, but the Replica installation would fail with an error stating “parameter JDBC_CONNECTION_STRING not defined“. Since everything runs in automated environments (Kubernetes or Ansible), I knew it was not a human error. When comparing the silent properties files, everything looked correct. The file used on the Primary was exactly the same as the one used on the Replica, except for “ISREPLICA_TOPOLOGY=0” and “ENCRYPTION_KEY=” on the Primary versus “ISREPLICA_TOPOLOGY=1” and “ENCRYPTION_KEY=XXXXXXX” on the Replica.
This is the expected configuration. A Replica needs to take the value of “directory.bootstrap.CryptSecret” from the “otds.properties” file of the Primary and use that value for “ENCRYPTION_KEY“. Therefore, when you install the Primary instance, the value remains empty because nothing is installed yet. During the Replica installation, the automation retrieves this value and populates the parameter accordingly. But then why would the Primary installation succeed while the Replica fails when using the exact same silent properties file? Quite strange, right? First of all, I tried running the installer manually (outside of Kubernetes or Ansible) to see whether additional details would appear in the console:
[tomcat@otds-1 workspace_otds]$ /app/scripts/workspace_otds/otds/setup -qbi -rf /app/scripts/workspace_otds/otds/silent.properties
OpenText Directory Services 24.4.0
Error, parameter JDBC_CONNECTION_STRING not defined.
[tomcat@otds-1 workspace_otds]$
The generated log file was not really helpful either:
[tomcat@otds-1 workspace_otds]$ cat otds.log
...
2025-08-08 6:38:40 chmod ran successfully on /etc/opentext/unixsetup
2025-08-08 6:38:40 Setting environment variable "ACTION" to "-1" : Success
2025-08-08 6:38:40 Setting environment variable "UPGRADE" to "0" : Success
2025-08-08 6:38:40 Setting environment variable "PATCH" to "0" : Success
2025-08-08 6:38:40 Setting environment variable "INSTALLED" to "0" : Success
2025-08-08 6:38:40 Setting environment variable "INSTALLEDVERSION" to "0.0.0" : Success
2025-08-08 6:38:40 Setting environment variable "PRODUCTINSTANCE" to "1" : Success
2025-08-08 6:38:40 Setting environment variable "PRODUCTVERSION" to "24.4.0.4503" : Success
2025-08-08 6:38:40 Setting environment variable "PRODUCTNAME" to "OpenText Directory Services" : Success
2025-08-08 6:38:40 Setting environment variable "PRODUCTID" to "OTDS" : Success
2025-08-08 6:38:40 Setting environment variable "PATCHVERSION" to "0" : Success
2025-08-08 6:38:40 Setting environment variable "ROOTUSER" to "0" : Success
2025-08-08 6:38:40 Setting environment variable "Main_INSTALLED" to "-1" : Success
2025-08-08 6:38:40 Setting environment variable "INST_GROUP" to "tomcat" : Success
2025-08-08 6:38:40 Setting environment variable "INST_USER" to "tomcat" : Success
2025-08-08 6:38:40 Setting environment variable "INSTALL_DIR" to "/app/tomcat/app_data/otds" : Success
2025-08-08 6:38:40 Setting environment variable "TOMCAT_DIR" to "/app/tomcat" : Success
2025-08-08 6:38:40 Setting environment variable "PRIMARY_FQDN" to "otds-1.otds.otdsdev.svc.cluster.local" : Success
2025-08-08 6:38:40 Setting environment variable "ISREPLICA_TOPOLOGY" to "1" : Success
2025-08-08 6:38:40 Setting environment variable "IMPORT_DATA" to "0" : Success
2025-08-08 6:38:40 Setting environment variable "OTDS_PASS" to "*****" : Success
2025-08-08 6:38:40 Setting environment variable "ENCRYPTION_KEY" to "mqLgucZ8UIUnNcLwjwmhNw==" : Success
2025-08-08 6:38:40 Setting environment variable "MIGRATION_OPENDJ_URL" to "" : Success
2025-08-08 6:38:40 Setting environment variable "MIGRATION_OPENDJ_PASSWORD" to "*****" : Success
2025-08-08 6:38:40 Setting environment variable "JDBC_CONNECTION_STRING" to "" : Success
2025-08-08 6:38:40 Setting environment variable "JDBC_USERNAME" to "" : Success
2025-08-08 6:38:40 Setting environment variable "JDBC_PASSWORD" to "*****" : Success
2025-08-08 6:38:40 Setting environment variable "ACTION" to "3" : Success
2025-08-08 6:38:40 Setting environment variable "Main_ACTION" to "3" : Success
2025-08-08 6:38:40 Adding Pre-req "TOMCAT7_HIGHER"
...
2025-08-08 6:38:40 Action #1 ended: OK
2025-08-08 6:38:40 Setting environment variable "PRIMARY_FQDN" to "otds-1.otds.otdsdev.svc.cluster.local" : Success
2025-08-08 6:38:40 Setting environment variable "ISREPLICA_TOPOLOGY" to "1" : Success
2025-08-08 6:38:40 Skipping IMPORT_DATA parameter (condition is false)
2025-08-08 6:38:40 Skipping OTDS_PASS parameter (condition is false)
2025-08-08 6:38:40 Setting environment variable "ENCRYPTION_KEY" to "mqLgucZ8UIUnNcLwjwmhNw==" : Success
2025-08-08 6:38:40 Skipping MIGRATION_OPENDJ_URL parameter (condition is false)
2025-08-08 6:38:40 Skipping MIGRATION_OPENDJ_PASSWORD parameter (condition is false)
2025-08-08 6:38:40 Error, parameter JDBC_CONNECTION_STRING not defined.
2025-08-08 6:38:40 Setup Ended: 1
2025-08-08 6:38:40 ============= Verbose logging Ended =============
[tomcat@otds-1 workspace_otds]$
For reference, here is the content of the “silent.properties” file that this Replica installation uses:
[tomcat@otds-1 workspace_otds]$ cat otds/silent.properties
[Setup]
Id=OTDS
Version=24.4.0.4503
Patch=0
Basedir=/app/scripts/workspace_otds/otds
Configfile=/app/scripts/workspace_otds/otds/setup.xml
Action=Install
Log=/app/scripts/workspace_otds/otds/otds.log
Instance=1
Feature=All
[Property]
INST_GROUP=tomcat
INST_USER=tomcat
INSTALL_DIR=/app/tomcat/app_data/otds
TOMCAT_DIR=/app/tomcat
PRIMARY_FQDN=otds-1.otds.otdsdev.svc.cluster.local
ISREPLICA_TOPOLOGY=1
IMPORT_DATA=0
OTDS_PASS=m1z6GX+HEX81DRpC
ENCRYPTION_KEY=mqLgucZ8UIUnNcLwjwmhNw==
MIGRATION_OPENDJ_URL=
MIGRATION_OPENDJ_PASSWORD=
JDBC_CONNECTION_STRING=jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=db_host.domain.com)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=db_svc.domain.com)))
JDBC_USERNAME=OTDS
JDBC_PASSWORD=Shu#Asd#Tgb;6799
[tomcat@otds-1 workspace_otds]$
(These are the real passwords from that environment. I have changed them since then, obviously, but I included them so you can understand the details below. The encryption key is altered, though – the system originally took the real one from the “Primary” instance.)
3. Status of the installer created/managed filesAfter the failure, I checked the parameter file that the OTDS installer populates during installation, but it was mostly empty and not yet filled:
[tomcat@otds-1 workspace_otds]$ cat /etc/opentext/unixsetup/OTDS_parameters_1.txt
#GROUP name that should be used to change file group ownership (group of USER)
INST_GROUP=tomcat
#USER name that should be used to change file ownership (user running processes)
INST_USER=tomcat
#Specify the installation directory for OpenText Directory Services
INSTALL_DIR=/usr/local/OTDS
#Specify the directory, where (64-bit) Apache Tomcat 10 or higher is installed
TOMCAT_DIR=/app/tomcat
#This hostname is used by other instances to connect to the synchronization master host.
PRIMARY_FQDN=
#Is this server a supplementary instance to an existing environment?
ISREPLICA_TOPOLOGY=0
#Specify OpenDJ connection for import.
IMPORT_DATA=0
#Specify the data encryption key from an existing instance.
ENCRYPTION_KEY=
#OpenDJ LDAP URL (example: ldap://localhost:1389)
MIGRATION_OPENDJ_URL=
#Specify JDBC connection String (example: jdbc:postgresql://localhost:5432/postgres). NOTE: Enter these values carefully since they cannot be validated here. Refer to the OTDS installation and administration guide for JDBC URL samples for supported databases.
JDBC_CONNECTION_STRING=
#Specify Database User Name
JDBC_USERNAME=
[tomcat@otds-1 workspace_otds]$
Finally, the “otds.properties” file (normally generated during the installation) was also not present yet:
[tomcat@otds-1 workspace_otds]$ ls -l $APP_DATA/otds/config/otds.properties
ls: cannot access '/app/tomcat/app_data/otds/config/otds.properties': No such file or directory
[tomcat@otds-1 workspace_otds]$
I tried launching the installer multiple times, on that OTDS Replica, while making small changes to the silent properties file to see if something specific would cause it to fail. Starting by modifying the “JDBC_CONNECTION_STRING” parameter, since that is what the installer complained about, but without success. I then suspected the password parameter. Because passwords are masked in the logs (“*“), it is impossible to see whether the value is parsed correctly or not.
Therefore, I replaced the OTDS Admin password in the silent properties file with “dummyPassword“, and the installer suddenly proceeded further… I cancelled the installation because this was not the real password of the “otadmin” account on the Primary instance, but in this case the “JDBC_CONNECTION_STRING” parameter was no longer empty and the installer continued normally.
Note: the OTDS documentation specifies that passwords must contain at least eight characters, including one lowercase letter, one uppercase letter, one number, and one special character. However, it appears that this rule may not be strictly validated during Replica installations (and possibly not for the Primary either?).
At that point it became clear that the password itself was involved in the issue, somehow. Looking at the script “tools/setup.sh“, you can see that the installer extracts the value of “OTDS_PASS“, applies a function called “AsctoHex“, and then encrypts it. My original “otadmin” password was 16 characters long and satisfied all complexity requirements. However, I noticed that the password contained the string “HEX“. Since the installer converts the password to hexadecimal before encryption, I wondered whether the presence of the string “HEX” might interfere with this process. That would be quite unbelievable, right?
5. A problem with the password length or content?To test this idea, I removed the “E” in the middle, transforming “HEX” into “HX” and effectively reducing the password length by one character:
[tomcat@otds-1 workspace_otds]$ grep OTDS_PASS otds/silent.properties | awk -F= '{print $2}' | wc -c
17
[tomcat@otds-1 workspace_otds]$ # 17 means 16 char since wc -c count the new line in this command
[tomcat@otds-1 workspace_otds]$
[tomcat@otds-1 workspace_otds]$ sed -i 's,HEX,HX,' otds/silent.properties
[tomcat@otds-1 workspace_otds]$
[tomcat@otds-1 workspace_otds]$ grep OTDS_PASS otds/silent.properties | awk -F= '{print $2}' | wc -c
16
[tomcat@otds-1 workspace_otds]$ # 16 means 15 char now
To re-execute the installer after a failure, you must remove the content of the “/etc/opentext” directory (which kind of caches the content from the “silent.properties” file) and also delete the “otds.properties” file if it exists (not in my case):
[tomcat@otds-1 workspace_otds]$ rm -rf /etc/opentext/*
[tomcat@otds-1 workspace_otds]$
In addition to modifying the “silent.properties” file, I also changed the “otadmin” password through the OTDS otds-admin UI (see the OTDS Install & Admin Guide, section 7.2.5 “Resetting a user password”). Then I started a new Replica installation to see whether changing “HEX” to “HX” from the password would resolve the issue:
[tomcat@otds-1 workspace_otds]$ /app/scripts/workspace_otds/otds/setup -qbi -rf /app/scripts/workspace_otds/otds/silent.properties
OpenText Directory Services 24.4.0
------------------------------------------------------------------------------
OpenText Directory Services
------------------------------------------------------------------------------
Installing OpenText Directory Services Component
Please wait .
Installation of OpenText Directory Services Component OK
Installation completed. Results:
OpenText Directory Services Component OK
Installation finished.
[tomcat@otds-1 workspace_otds]$
… It worked …?
If the issue was really caused by the presence of “HEX” in the password, then replacing it with “HXE” should also work, right? Unfortunately, when I tried that, the issue came back… This indicates that the real problem is not the literal “HEX” string but maybe something related to password length, complexity, or how the installer processes and encrypts the password internally?
6. ConclusionIn the end, I reverted to the shorter 15-character password that worked and prepared all higher environments at this customer to use 15-character passwords. This approach worked without issue for five additional environments until, of course, it failed again, in Production…
Since it failed in another environment even with a 15-character password, the length alone does not seem to be the root cause. When reviewing previously installed environments across multiple customers, I found a few instances running with “otadmin” passwords of up to 19 characters long (about 111/120 bits of entropy according to a password manager like KeePass). This is significantly stronger than the 15-character password (96 bits) used in the Production environment where the issue occurred.
Therefore, since I couldn’t find any logical reasons why the issue happened on some environments but not with others, I opened a ticket with OpenText. I described everything and we went through several weeks of exchanges to try to find an explanation but without success. As of today, I still don’t know why ~10% of the OTDS Replicas that I installed faced an issue with the OT Admin password, but the fix, was simply to change the password in the UI and start the silent installation again. I don’t have an environment to test / debug that issue anymore, since it’s not easily reproducible. Guess I will need to wait for next time to get more debug logs from the OTDS installer (“-debug” option). In the meantime, I can only assume something is probably wrong in the way OTDS manages the password or its hash.
L’article OTDS – Installation of Replicas fail if the OT Admin password is too long? est apparu en premier sur dbi Blog.
Protected: Upgrade RHEL from 9.6 to 10.1 (when running PostgreSQL/Patroni)
This content is password-protected. To view it, please enter the password below.
Password:
L’article Protected: Upgrade RHEL from 9.6 to 10.1 (when running PostgreSQL/Patroni) est apparu en premier sur dbi Blog.
Deployment Creation INS-85037 Error With GoldenGate 26ai for DB2 z/OS
Among all the automation I was doing around a GoldenGate installation for DB2, I recently ended up with an INS-85037 error when running the configuration assistant oggca.sh. And because this error is quite common and has many different possible root causes, I wanted to write about it.
If you’re wondering how to set up GoldenGate 26ai for DB2 z/OS, it is very similar to what you would do with GoldenGate for Oracle. For more information on standard GoldenGate setups, you can read my blog posts about both 26ai and 23ai installations.
For the binary installation, the main difference is that INSTALL_OPTION should be set to DB2ZOS. A complete oggcore.rsp response file would look like this:
oracle.install.responseFileVersion=/oracle/install/rspfmt_ogginstall_response_schema_v23_1_0
INSTALL_OPTION=DB2ZOS
SOFTWARE_LOCATION=/u01/app/oracle/product/oggzos
INVENTORY_LOCATION=/u01/app/oraInventory
UNIX_GROUP_NAME=oinstall
When running the configuration assistant, some options are not available, but the main difference is in the environment variables section of the response file. You should have an IBMCLIDRIVER variable set to your DB2 driver’s path.
ENV_LD_LIBRARY_PATH=${IBMCLIDRIVER}/1ib:${OGG_HOME}/1ib
IBMCLIDRIVER=/path/to/ibmclidriver
ENV_USER_VARS=
Possible Solution for INS-85037
That being said, here is the exact error I had when running the Configuration Assistant oggca.sh:
[FATAL] [INS-85037] Deployment creation failed.
ACTION: Check logs at /u01/app/oraInventory/logs/OGGCAConfigActions2026-03-22_15-19-15PM for more information.
*MORE DETAILS*
Return code 503 (Service Unavailable) does not match the expected code 201 (Created).
Verification failed for REST call to 'http://127.0.0.1:7810/services/v2/authorizations/security/ogguser'
Results for "Add a new deployment":
..."Verifying Service Manager deployment status.": SUCCEEDED
..."Adding 'zos_test' deployment.": SUCCEEDED
...."Configuring and starting the Administration Service.": SUCCEEDED
..."Verifying the initial Administration Service configuration.": SUCCEEDED
..."Adding user 'ogguser' to administer the deployment.": FAILED
Log of this session available at: /u01/app/oraInventory/logs/OGGCAConfigActions2026-03-22_15-19-15PM
The deployment creation failed and the associated files will be deleted from disk. Oracle recommends that if you want to keep the log files, you should move them to another location.
Log files will be copied to:
/u01/app/oraInventory/logs/OGGCAConfigActions2026-03-22_15-19-15PM/userdeploy_logs_2026-03-22_15-19-15PM
[WARNING] [INS-32090] Software installation was unsuccessful.
ACTION: Refer to the log files for details or contact Oracle Support Services.
Unfortunately, the installation logs did not show anything other than the following:
SEVERE: Deployment creation job failed.
INFO: Service Manager deployment that was created as part of the process needs to be removed.
INFO: Running clean-up job for Service Manager.
SEVERE: Removing Service Manager deployment.
The deployment and the service manager get deleted after the installation failure, but the logs are also copied to the oraInventory installation logs. Looking at the ServiceManager.log in the smdeploy folder, we don’t get much information.
ERROR| Configuration does not contain a 'config/network' specification. (ServiceManager.Topology)
The same applies to the restapi.log, where the logs start after the initial deployment creation error. Unfortunately, none of this was really helpful in my case. After quite some digging, I found that the response file I was using when running oggca.sh had an error. In the custom section for environment variables, I had the following settings:
# SECTION G - ENVIRONMENT VARIABLES
ENY_LD_LIBRARY_PATH-S{IBMCLIDRIVER}/1ib:${OGG_HOME}/11b
IBMCLIDRIVER=/u01/app/ibm/db2_odbc_cli_11_5
ENV_USER_VARS=
It looks like what I gave earlier, except that the path for the clidriver was incomplete.
oracle@vmogg:/home/oracle/ [ogg] ls -l /u01/app/ibm/db2_odbc_cli_11_5
drwxr-xr-x 3 oracle oinstall 23 Mar 22 2026 odbc_cli
oracle@vmogg:/home/oracle/ [ogg] ls -l /u01/app/ibm/db2_odbc_cli_11_5/odbc_cli/clidriver/
-r-xr-xr-x 1 oracle oinstall 4170 Mar 17 2021 Readme.txt
drwxr-xr-x 2 oracle oinstall 36 Mar 22 2026 adm
drwxr-xr-x 2 oracle oinstall 122 Mar 22 2026 bin
drwxr-xr-x 2 oracle oinstall 197 Mar 22 2026 bnd
drwxr-xr-x 2 oracle oinstall 157 Mar 22 09:16 cfg
drwxr-xr-x 2 oracle oinstall 24 Mar 22 2026 cfecache
drwxr-xr-x 4 oracle oinstall 27 Mar 22 2026 conv
drwxr-xr-x 3 oracle oinstall 49 Mar 22 09:26 db2dump
drwxr-xr-x 3 oracle oinstall 217 Mar 22 2026 lib
drwxr-xr-x 3 oracle oinstall 124 Mar 22 09:26 license
drwxr-xr-x 3 oracle oinstall 28 Mar 22 2026 msg
drwxr-xr-x 3 oracle oinstall 21 Mar 22 2026 properties
drwxr-xr-x 3 oracle oinstall 20 Mar 22 2026 security32
drwxr-xr-x 3 oracle oinstall 20 Mar 22 2026 security64
After correcting the oggca.rsp response file with the correct path, the configuration assistant ran successfully.
oracle@vmogg:/u01/app/oracle/product/ogg26/bin [ogg] oggca.sh -silent -responseFile /home/oracle/oggca.rsp
Successfully Setup Software.
Next time you encounter an error like this when setting up GoldenGate for DB2, make sure to check not only the variable value but also the actual content of the IBMCLIDRIVER directory !
NB: If you had this error for any other kind of setup, make sure to always check all the content of the response file you are using, as well as the prerequisites. (CLIDRIVER in this case, but it could be XAG, etc.)
L’article Deployment Creation INS-85037 Error With GoldenGate 26ai for DB2 z/OS est apparu en premier sur dbi Blog.
Discover refreshable clone PDB with Autoupgrade
AutoUpgrade with a refreshable clone is basically “zero‑panic upgrades with a live copy of your database.”.
What problem it solvesTraditionally you had to schedule a maintenance window, stop everything, take a backup, upgrade, and hope nothing went wrong.
With a refreshable clone PDB, AutoUpgrade builds and continuously syncs a copy of your database while production stays online. At cutover time, you just stop users, do a last refresh, convert/upgrade the clone, and switch them over. If something goes wrong, the original source is untouched and you can fall back quickly.
Core idea in simple termsThink of your non‑CDB or old‑version PDB as the “master” and the refreshable clone PDB as a “follow‑me” copy sitting in the target CDB.
AutoUpgrade:
- Creates a PDB in the target CDB via database link (initial clone of datafiles).
- Marks it as refreshable, so redo from the source is applied and it keeps rolling forward.
- Lets you test the clone (read‑only) while users are still working on the source.
- At a controlled start time, runs a last refresh, disconnects it from the source, converts it to a normal PDB, and upgrades it.
From your point of view: you prepare everything days in advance, and the real downtime shrinks to “final refresh + upgrade + app switch.”
High‑level lifecycleFor a non‑CDB to PDB migration or a PDB upgrade, the flow looks like this:
Preparation- You have a source: non‑CDB 12.2/19c or older PDB.
- You have a target: a higher‑version CDB (for example 23ai/26ai) with enough space and network.
- You configure AutoUpgrade with the source and target, plus the parameter telling it to use refreshable clone PDB.
- In deploy mode, AutoUpgrade creates the pluggable database in the target CDB via DB link, copies the datafiles, and defines it as refreshable.
- From now on, redo is shipped from source to target and applied, so the clone stays close to current.
- The source database stays fully online; business keeps running.
- The refreshable clone is read‑only, so you can query it, run app smoke tests, check performance characteristics, etc.
- AutoUpgrade keeps the job running in the background, doing periodic refreshes.
- When you reach the maintenance window, users leave the system and you quiesce activity on the source.
- AutoUpgrade performs a final refresh: last redo from source is applied on the clone so you don’t lose any committed data.
- The clone is then disconnected from the source, turned into a regular PDB, and AutoUpgrade moves into the upgrade and conversion steps (non‑CDB to PDB conversion if needed, then catalog/PSU/UTLRP, etc.).
- You point applications to the new PDB in the target CDB.
- The original source database still exists; if you hit a show‑stopper, you can redirect apps back to it and plan a new attempt.
In practice, the “scary” part is only the final refresh and the moment you switch your apps.
Why DBAs like this patternSome clear advantages:
- Minimal downtime: Most of the heavy lifting (copy + sync) happens while production is running; downtime is limited to final refresh and upgrade.
- Built‑in rollback: Because the source stays untouched, you always have a clean fallback without restore/recovery.
- Realistic testing: You test against a clone built from real production data that is almost up‑to‑date, not a weeks‑old backup.
- Automation: AutoUpgrade orchestrates the create‑clone, refresh, disconnect, convert, and upgrade steps; you mostly steer with parameters and commands instead of custom scripts.
Trade‑offs are mainly around resources: you need disk, CPU, and network to maintain the refreshable clone, and you have to ensure redo shipping is reliable (archivelog gaps or network glitches can break the refresh and need fixing).
Typical exampleImagine you need to move a 19c non‑CDB to a new 26ai CDB on a different host, with less than 30 minutes downtime:
- Monday: you configure AutoUpgrade with the refreshable clone option, start the job. The tool creates the PDB clone in the 26ai CDB and starts streaming redo. Users never notice.
- Next days: you let it refresh every few minutes, developers connect read‑only to the clone and test their application against 26ai. Everything looks good.
- Saturday night: you enter the maintenance window, let open transactions finish, stop app traffic, and tell AutoUpgrade to proceed to the final refresh. Once that’s done, it disconnects the clone, upgrades it, and runs post‑upgrade steps.
- After checks, you change the service names on the app side so they point to the new PDB. Your downtime is mostly spent waiting for the upgrade scripts, not copying terabytes of data.
L’article Discover refreshable clone PDB with Autoupgrade est apparu en premier sur dbi Blog.
How to Standardize SQL Server Disks on VMs using Ansible
Today, the benefits of automation no longer need much explanation: saving time, reducing human error, and ensuring every environment remains aligned with internal standards. What is less obvious, however, is how using an Ansible Playbook can provide advantages that more traditional scripting approaches — such as large PowerShell scripts — struggle to offer. That is exactly what I want to explore here.
When you complete an automated deployment of a SQL Server environment on Windows Server, there is a real sense of achievement. You have invested time and effort, and you expect that investment to pay off thanks to the reliability and repeatability of automation.
But everything changes when the next Windows Server upgrade or SQL Server version arrives… or when corporate standards evolve. Suddenly, you need to reopen a multi-thousand‑line PowerShell script and:
- Integrate the required changes while keeping execution stable,
- Avoid subtle but potentially critical regressions,
- Maintain clear and usable logging,
- Retest the entire automation workflow,
- Troubleshoot new issues introduced by the modifications.
This is precisely the type of situation where Ansible becomes a far better long‑term investment. Its architecture and philosophy offer several advantages:
- Native idempotence, ensuring the same result even after multiple runs,
- A declarative YAML approach, focusing on the desired end state rather than the execution steps,
- Windows Server and SQL Server modules, providing built‑in idempotence and saving significant time,
- Agentless connectivity, simplifying deployment on new machines,
- A modular structure (roles, modules, variables), making adaptation and reuse of your automation much easier.
In this article, I will give you a concrete overview by walking you through how to configure the disks required for SQL Server using Ansible.
1-Map iSCSI controllers to disk numbersWhen developing an Ansible Playbook, one fundamental principle is to design for idempotence from the very start—not just rely on idempotent modules.
On Windows, disk numbering is not guaranteed: it depends on several factors – how disks are detected at startup, the firmware, and so on.
As a result, disk numbers may change from one reboot to another.
To ensure consistent and reliable execution of your deployment, this behavior must be accounted for directly in the design of your Playbook.
Otherwise, it may introduce wrong behaviors, and lead to:
- formatting the wrong disk,
- mounting volumes on incorrect devices,
- completely breaking the SQL Server provisioning workflow.
In other words, idempotence is no longer guaranteed.
To ensure stable and predictable executions, you must determine dynamically the correct disk numbering at each execution.
You can use Get-Disk PowerShell command to achieve your goal, by searching iSCSI controller number and LUN position from Location property.
$adapter = {{ disk.adapter }}
$lun = {{ disk.lun}}
(Get-Disk | Where-Object {
$_.Location -match "Adapter $adapter\s+:.*\s+LUN $lun"
}).number
We have done our mapping between VM specifications and Windows disk numbers.
2-Loop SQL Server disksSince we often have several disks to configure — Data, Logs, TempDB — we need to perform the same actions repeatedly on each disk:
- dynamically determine the disk number,
- initialize it in GPT,
- create the partition and format the volume in NTFS with a 64 KB allocation unit size,
- assign an access path (drive letter or mountpoint),
- apply certain specific configuration settings, such as disabling indexing,
- verify the compliance of the disk configuration.
As these actions are identical for all disks, the best approach is to factorize the tasks.
The Ansible pattern, for such scenario, is to loop that call in a dedicated Task File.
---
- name: Manage all disk properties based on Location and Target numbers
ansible.builtin.include_tasks: disks_properties.yml
loop:
- name: data
location: "{{ disk_specs.data.location }}"
target: "{{ disk_specs.data.target }}"
label: "{{ disk_specs.data.label }}"
letter: "{{ disk_specs.data.letter }}"
- name: logs
location: "{{ disk_specs.logs.location }}"
target: "{{ disk_specs.logs.target }}"
label: "{{ disk_specs.logs.label }}"
letter: "{{ disk_specs.logs.letter }}"
- name: tempdb
location: "{{ disk_specs.tempdb.location }}"
target: "{{ disk_specs.tempdb.target }}"
label: "{{ disk_specs.tempdb.label }}"
letter: "{{ disk_specs.tempdb.letter }}"
loop_control:
loop_var: disk
...
Since we performed our loop in the previous section on the disks_properties.yml file, we can now implement the configuration actions inside this file.
First, we will retrieve the disk number and then begin configuring the disk according to best practices and our internal standards.
To guarantee idempotence, we will mark this step as not changed: this is only a Get action:
---
- name: Identify the {{ disk.name }} disk number
ansible.windows.win_shell: |
$adapter = {{ disk.target }}
$lun = {{ disk.location }}
(Get-Disk | Where-Object {
$_.Location -match "Adapter $adapter\s+:.*\s+LUN $lun"
}).number
register: disk_num
changed_when: false
Then, we will register the disk number as an Ansible Fact for all this task file execution call.
- name: Set fact for {{ disk.name }} disk number
ansible.builtin.set_fact:
"disk_number_{{ disk.name }}": "{{ disk_num.stdout | trim | int }}"
We can now initialize the disk using community.windows module. Of course, use Ansible module if possible.
The parameter disk_bps.partition_style is a variable of my Ansible Role, to guarantee GPT will be used.
- name: Initialize disks
community.windows.win_initialize_disk:
disk_number: "{{ lookup('vars', 'disk_number_' + disk.name) }}"
style: "{{ disk_bps.partition_style }}"
From there, we can create our partition:
- name: Create partition with letter {{ disk.letter }} for disk {{ disk.name }}
community.windows.win_partition:
drive_letter: "{{ disk.letter }}"
partition_size: "-1"
disk_number: "{{ lookup('vars', 'disk_number_' + disk.name) }}"
And now format our volume with allocation unit size 64KB:
- name: Create a partition letter {{ disk.letter }} on disk {{ disk.name }} with label {{ disk.label }}
community.windows.win_format:
drive_letter: "{{ disk.letter }}"
allocation_unit_size: "{{ disk_bps.allocation_unit_size_bytes }}"
new_label: "{{ disk.label }}"
...
As I mentioned earlier in previous section, we can also add tasks relative to some specific standards or a tasks to guarantee disk compliance.
4- Execute the PlaybookNow that our Ansible Role windows_disks is ready, we can call it through a Playbook.
Of course, we must adjust the reality of the iSCSI configuration of the Virtual Machine.
---
- name: Configure Disks by detecting Disk Number
hosts: Raynor
gather_facts: false
vars:
disk_specs:
data:
location: 0
target: 1
label: SQL_DATA
letter: E
logs:
location: 0
target: 2
label: SQL_TLOG
letter: L
tempdb:
location: 0
target: 3
label: SQL_TEMPDB
letter: T
tasks:
- name: gather facts
ansible.builtin.setup:
changed_when: false
tags: [always]
- name: Configure Disks
ansible.builtin.import_role:
name: windows_disks
tags: windows_disks
...
CONCLUSION
We have had an overview of how Ansible makes automation easier to maintain and to evolve, by focusing on the logic of our deployment and not on the code to achieve it.
Now, updating your standards or upgrading versions will no longer require rewriting scripts, but mainly adapting variables.
However, it is important to be aware that idempotence must also be maintained through design.
L’article How to Standardize SQL Server Disks on VMs using Ansible est apparu en premier sur dbi Blog.
Creating Path Connections with GoldenGate REST API
When automating your GoldenGate deployment management, you might want to create path connections with the GoldenGate REST API. This is an important aspect when connecting GoldenGate deployments with distribution paths. A first step towards this is to create a path connection on the same deployment as the distribution server where the distribution path will run.
In the GoldenGate web UI, you can easily create Path Connections. Just go to the Path Connections tab, add a path, and specify the following information:
- Credential Alias: Alias used to connect to the target deployment. It doesn’t have to match any name on the target deployment.
- User ID: Real username that must exist on the target deployment.
- Password: Password associated with the User ID given before.
restapi.log analysis
But what about the REST API ? When looking at the list of endpoints given by Oracle, no REST endpoint explicitly refers to path connections, so how to create path connections through the REST API ?
The key point to understand is that path connections are not independent GoldenGate objects. In fact, they exist as a subset of another object, which you should know by now : aliases. Aliases are created to store credentials and are organized in domains. The default domain is called OracleGoldenGate, and Oracle has a reserved name for a subtype of domains : Network.
We can see this easily when creating a path connection through the web UI, and then looking at the restapi.log file. Open the log file located in the var/log folder of your deployment, or read the blog I wrote about restapi.log analysis. Using this method, we see the endpoint and the content of the API call. Here, for instance, I created a path connection from the web UI, to connect to ogg_user with the ogg_target alias.
oracle@vmogg: jq -c 'select (.request.context.verb == "POST" and .request.context.uriTemplate == "/services/{version}/credentials/{domain}/{alias}")' restapi.ndjson
{"request":{"context":{"verb":"POST","uri":"/services/v2/credentials/Network/ogg_target","uriTemplate":"/services/{version}/credentials/{domain}/{alias}"}},"content":{"userid":"ogg_user","password":"** Masked **"},...}
Path connection creation with the REST API
To summarize, path connections are just aliases in the Network domain. This simplifies the creation of path connections. You just need to make a POST API call to the alias endpoint, specifying Network as the domain. The exact endpoint is then /services/{version}/credentials/Network/{alias}.
Quick example: using the GoldenGate Python client I presented in another blog, let’s create an alias in the Network domain :
>>> from oggrestapi import OGGRestAPI
>>> ogg_client = OGGRestAPI(
url="https://vmogg",
username="ogg",
password="ogg")
Connected to OGG REST API at https://vmogg
>>> ogg_client.create_alias(
alias='ogg_dbi_blog',
domain='Network',
data={
"userid": "ogg_user_on_target",
"password": "***"
}
)
{'$schema': 'api:standardResponse', 'links': [{'rel': 'canonical', 'href': 'https://vmogg/services/v2/credentials/Network/ogg_dbi_blog', 'mediaType': 'application/json'}, {'rel': 'self', 'href': 'https://vmogg/services/v2/credentials/Network/ogg_dbi_blog', 'mediaType': 'application/json'}], 'messages': [{'$schema': 'ogg:message', 'title': 'Credential store altered.', 'code': 'OGG-15114', 'severity': 'INFO', 'issued': '2026-03-22T10:14:01Z', 'type': 'https://docs.oracle.com/en/middleware/goldengate/core/23.26/error-messages/'}]}
After refreshing the web UI, the newly created path connection is visible.
L’article Creating Path Connections with GoldenGate REST API est apparu en premier sur dbi Blog.
Dctm – Another DM_LICENSE_E_INVALID_LICENSE error but caused by JMS this time
At the end of last year, I published a first blog about a DM_LICENSE_E_INVALID_LICENSE error in D2 SSO login through OTDS. The root cause in that previous post was a duplicate user with one lowercase and one uppercase user_login_name. However, I did mention that there can be several reasons for that error. In this blog, I will describe another such case.
1. Symptoms in D2 logsThe generated D2 logs associated with this new issue are almost exactly the same. The only difference is that the Repository returns “null” as the userid (user_name). See the message “Authentication failed for user null with docbase REPO_NAME“. This wasn’t the case in the other blog post:
[tomcat@d2-0 logs]$ cat D2.log
...
2025-12-08 12:21:14,784 UTC [INFO ] (https-jsse-nio-8080-exec-47) - c.emc.x3.portal.server.X3HttpSessionListener : Created http session 8531D373A3EA12A398B158AF656E7D20
2025-12-08 12:21:14,784 UTC [DEBUG] (https-jsse-nio-8080-exec-47) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : OTDS : No user name on the Http session yet
2025-12-08 12:21:14,785 UTC [DEBUG] (https-jsse-nio-8080-exec-47) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : OTDS : No access_token found in Http request or Cookie Redirecting to OTDS Server
2025-12-08 12:21:14,786 UTC [DEBUG] (https-jsse-nio-8080-exec-47) - c.e.x.p.s.f.a.X3TrustHttpAuthenticationFilter : Identified scheme : https
2025-12-08 12:21:14,786 UTC [DEBUG] (https-jsse-nio-8080-exec-47) - c.e.x.p.s.f.a.X3TrustHttpAuthenticationFilter : Identified server name : d2.domain.com
2025-12-08 12:21:14,787 UTC [DEBUG] (https-jsse-nio-8080-exec-47) - c.e.x.p.s.f.a.X3TrustHttpAuthenticationFilter : Identified server port : 443
2025-12-08 12:21:14,787 UTC [DEBUG] (https-jsse-nio-8080-exec-47) - c.e.x.p.s.f.a.X3TrustHttpAuthenticationFilter : Built server host is : https://d2.domain.com:443
2025-12-08 12:21:14,788 UTC [DEBUG] (https-jsse-nio-8080-exec-47) - o.o.e.logging.slf4j.Slf4JLogLevelHandlers$4 : [EVENT SUCCESS -> /D2/HTTPUtilities] header name=Host, value=d2.domain.com
2025-12-08 12:21:14,789 UTC [DEBUG] (https-jsse-nio-8080-exec-47) - o.o.e.logging.slf4j.Slf4JLogLevelHandlers$4 : [EVENT SUCCESS -> /D2/HTTPUtilities] MaxHeaderValueSize: 8192
2025-12-08 12:21:14,792 UTC [DEBUG] (https-jsse-nio-8080-exec-47) - o.o.e.logging.slf4j.Slf4JLogLevelHandlers$4 : [EVENT SUCCESS -> /D2/HTTPUtilities] validating the input valued2.domain.com
2025-12-08 12:21:14,793 UTC [DEBUG] (https-jsse-nio-8080-exec-47) - c.e.x.p.s.f.a.X3TrustHttpAuthenticationFilter : Identified host : d2.domain.com
2025-12-08 12:21:14,794 UTC [DEBUG] (https-jsse-nio-8080-exec-47) - c.e.x.p.s.f.a.X3TrustHttpAuthenticationFilter : Overall base URL built : https://d2.domain.com/D2
2025-12-08 12:21:14,795 UTC [DEBUG] (https-jsse-nio-8080-exec-47) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : Redirection url post encoding - https%3A%2F%2Fd2.domain.com%2FD2%2Fd2_otds.html%3ForigUrl%3D%2FD2%2F
2025-12-08 12:21:14,797 UTC [DEBUG] (https-jsse-nio-8080-exec-47) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : OTDS : OAUTH final login sendRedirect URL : https://otds-mfa.domain.com/otdsws/oauth2/auth?response_type=token&client_id=dctm-ns-d2&redirect_uri=https%3A%2F%2Fd2.domain.com%2FD2%2Fd2_otds.html%3ForigUrl%3D%2FD2%2F&logon_appname=Documentum+Client+CE+23.4
2025-12-08 12:21:14,798 UTC [DEBUG] (https-jsse-nio-8080-exec-47) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : OTDS : Sending redirection as it's not a rpc call : https://otds-mfa.domain.com/otdsws/oauth2/auth?response_type=token&client_id=dctm-ns-d2&redirect_uri=https%3A%2F%2Fd2.domain.com%2FD2%2Fd2_otds.html%3ForigUrl%3D%2FD2%2F&logon_appname=Documentum+Client+CE+23.4
2025-12-08 12:21:15,018 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - o.o.e.logging.slf4j.Slf4JLogLevelHandlers$4 : [EVENT SUCCESS -> /D2/HTTPUtilities] MaxHeaderKeySize: 256
2025-12-08 12:21:15,018 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - o.o.e.logging.slf4j.Slf4JLogLevelHandlers$4 : [EVENT SUCCESS -> /D2/HTTPUtilities] MaxHeaderValueSize: 8192
2025-12-08 12:21:15,020 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : OTDS : No user name on the Http session yet
2025-12-08 12:21:15,021 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : OTDS : Found access_token on Http Cookie, invalidating the cookie by setting maxAge 0
2025-12-08 12:21:15,022 UTC [INFO ] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : OTDS : setting the cookie as secure as its a https request
2025-12-08 12:21:15,024 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : OTDS : OTDS responded with a oauth token
2025-12-08 12:21:15,025 UTC [INFO ] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : ------ Begin getUntrustedJwtHeader : eyJraWQiOiI1YjM4...oSD8Xh3vVmkekcA
2025-12-08 12:21:15,026 UTC [INFO ] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : getUntrustedJwtHeader oauthTokenWithoutSignature : eyJraWQiOiI1YjM4...i1xYWN0LWQyIn0.
2025-12-08 12:21:15,614 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : ------ Begin validateOTDSTokenClaims : MYUSERID
2025-12-08 12:21:15,615 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : validateOTDSTokenClaims for user : MYUSERID , OTDS : currenttime: 1765196475615 expirationtime: 1765200074000
2025-12-08 12:21:15,615 UTC [INFO ] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : ------ End validateOTDSTokenClaims : MYUSERID
2025-12-08 12:21:15,615 UTC [INFO ] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : PublicKey for Key id : 5b38b...bf487 exists
2025-12-08 12:21:15,617 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : OTDS Deafault Repository from shiro configured : REPO_NAME
2025-12-08 12:21:15,617 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : OTDS : generating DM_Ticket for user : MYUSERID in Repository : REPO_NAME
2025-12-08 12:21:16,522 UTC [ERROR] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : OTDS : OAuth Token Error occurred while generating a DCTM MultiUse Ticket for user : MYUSERID
2025-12-08 12:21:16,522 UTC [ERROR] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : OTDS : OAuth Token Error please validate the OTDS Config of user exists in Repository
com.documentum.fc.client.DfAuthenticationException: [DM_SESSION_E_AUTH_FAIL]error: "Authentication failed for user null with docbase REPO_NAME."
at com.documentum.fc.client.impl.docbase.DocbaseExceptionMapper.newException(DocbaseExceptionMapper.java:52)
at com.documentum.fc.client.impl.connection.docbase.MessageEntry.getException(MessageEntry.java:39)
at com.documentum.fc.client.impl.connection.docbase.DocbaseMessageManager.getException(DocbaseMessageManager.java:137)
at com.documentum.fc.client.impl.connection.docbase.netwise.NetwiseDocbaseRpcClient.checkForMessages(NetwiseDocbaseRpcClient.java:332)
at com.documentum.fc.client.impl.connection.docbase.netwise.NetwiseDocbaseRpcClient.applyForObject(NetwiseDocbaseRpcClient.java:680)
at com.documentum.fc.client.impl.connection.docbase.DocbaseConnection$8.evaluate(DocbaseConnection.java:1572)
at com.documentum.fc.client.impl.connection.docbase.DocbaseConnection.evaluateRpc(DocbaseConnection.java:1272)
at com.documentum.fc.client.impl.connection.docbase.DocbaseConnection.applyForObject(DocbaseConnection.java:1564)
at com.documentum.fc.client.impl.docbase.DocbaseApi.authenticateUser(DocbaseApi.java:1894)
at com.documentum.fc.client.impl.connection.docbase.DocbaseConnection.authenticate(DocbaseConnection.java:460)
at com.documentum.fc.client.impl.connection.docbase.DocbaseConnection.open(DocbaseConnection.java:140)
at com.documentum.fc.client.impl.connection.docbase.DocbaseConnection.<init>(DocbaseConnection.java:109)
at com.documentum.fc.client.impl.connection.docbase.DocbaseConnection.<init>(DocbaseConnection.java:69)
at com.documentum.fc.client.impl.connection.docbase.DocbaseConnectionFactory.newDocbaseConnection(DocbaseConnectionFactory.java:32)
at com.documentum.fc.client.impl.connection.docbase.DocbaseConnectionManager.createNewConnection(DocbaseConnectionManager.java:202)
at com.documentum.fc.client.impl.connection.docbase.DocbaseConnectionManager.getDocbaseConnection(DocbaseConnectionManager.java:132)
at com.documentum.fc.client.impl.session.SessionFactory.newSession(SessionFactory.java:24)
...
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
at java.base/java.lang.Thread.run(Thread.java:840)
2025-12-08 12:21:16,524 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : redirectToErrorPage : Redirecting to Error Page as Login failed for user : null and exception : {}
com.emc.x3.portal.server.filters.authc.X3OTDSAuthenticationFilter$1: Authentication failed for user null with repository REPO_NAME.
at com.emc.x3.portal.server.filters.authc.X3OTDSAuthenticationFilter.validateTokenAndGetUserId(X3OTDSAuthenticationFilter.java:1167)
at com.emc.x3.portal.server.filters.authc.X3OTDSAuthenticationFilter.onAccessDenied(X3OTDSAuthenticationFilter.java:293)
at org.apache.shiro.web.filter.AccessControlFilter.onAccessDenied(AccessControlFilter.java:133)
at org.apache.shiro.web.filter.AccessControlFilter.onPreHandle(AccessControlFilter.java:162)
at org.apache.shiro.web.filter.PathMatchingFilter.isFilterChainContinued(PathMatchingFilter.java:223)
at org.apache.shiro.web.filter.PathMatchingFilter.preHandle(PathMatchingFilter.java:198)
...
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
at java.base/java.lang.Thread.run(Thread.java:840)
2025-12-08 12:21:16,524 UTC [INFO ] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : Adding the LicenseException to the Session : DM_SESSION_E_AUTH_FAIL
2025-12-08 12:21:16,526 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.a.X3TrustHttpAuthenticationFilter : Identified scheme : https
2025-12-08 12:21:16,526 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.a.X3TrustHttpAuthenticationFilter : Identified server name : d2.domain.com
2025-12-08 12:21:16,526 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.a.X3TrustHttpAuthenticationFilter : Identified server port : 443
2025-12-08 12:21:16,528 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.a.X3TrustHttpAuthenticationFilter : Built server host is : https://d2.domain.com:443
2025-12-08 12:21:16,529 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - o.o.e.logging.slf4j.Slf4JLogLevelHandlers$4 : [EVENT SUCCESS -> /D2/HTTPUtilities] header name=Host, value=d2.domain.com
2025-12-08 12:21:16,530 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - o.o.e.logging.slf4j.Slf4JLogLevelHandlers$4 : [EVENT SUCCESS -> /D2/HTTPUtilities] MaxHeaderValueSize: 8192
2025-12-08 12:21:16,531 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - o.o.e.logging.slf4j.Slf4JLogLevelHandlers$4 : [EVENT SUCCESS -> /D2/HTTPUtilities] validating the input valued2.domain.com
2025-12-08 12:21:16,532 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.a.X3TrustHttpAuthenticationFilter : Identified host : d2.domain.com
2025-12-08 12:21:16,533 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.a.X3TrustHttpAuthenticationFilter : Overall base URL built : https://d2.domain.com/D2
2025-12-08 12:21:16,534 UTC [DEBUG] (https-jsse-nio-8080-exec-5) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : D2 redirecting to errorPage JSP : https://d2.domain.com/D2/errors/authenticationError.jsp
2025-12-08 12:21:16,567 UTC [DEBUG] (https-jsse-nio-8080-exec-26) - o.o.e.logging.slf4j.Slf4JLogLevelHandlers$4 : [EVENT SUCCESS -> /D2/HTTPUtilities] MaxHeaderKeySize: 256
2025-12-08 12:21:16,567 UTC [DEBUG] (https-jsse-nio-8080-exec-26) - o.o.e.logging.slf4j.Slf4JLogLevelHandlers$4 : [EVENT SUCCESS -> /D2/HTTPUtilities] MaxHeaderValueSize: 8192
2025-12-08 12:21:16,568 UTC [DEBUG] (https-jsse-nio-8080-exec-26) - c.e.x.p.s.f.authc.X3OTDSAuthenticationFilter : No LicenseExcepton found on HttpSession hence not Redirectling to License ErrorPage
2025-12-08 12:21:16,571 UTC [DEBUG] (https-jsse-nio-8080-exec-26) - c.e.x.p.s.f.a.X3TrustHttpAuthenticationFilter : Selected Repository : REPO_NAME
2025-12-08 12:21:16,573 UTC [DEBUG] (https-jsse-nio-8080-exec-26) - o.o.e.logging.slf4j.Slf4JLogLevelHandlers$4 : [EVENT SUCCESS -> /D2/HTTPUtilities] MaxHeaderKeySize: 256
2025-12-08 12:21:16,574 UTC [DEBUG] (https-jsse-nio-8080-exec-26) - o.o.e.logging.slf4j.Slf4JLogLevelHandlers$4 : [EVENT SUCCESS -> /D2/HTTPUtilities] MaxHeaderValueSize: 8192
2025-12-08 12:21:16,578 UTC [INFO ] (https-jsse-nio-8080-exec-26) - c.emc.x3.portal.server.X3HttpSessionListener : Expired Http session id : 8531D373A3EA12A398B158AF656E7D20
2025-12-08 12:21:16,578 UTC [DEBUG] (https-jsse-nio-8080-exec-26) - com.emc.x3.server.context.ContextManager : Create a new context manager
...
[tomcat@d2-0 logs]$
As usual, the next step is to check the Repository logs with the authentication trace enabled:
[dmadmin@cs-0 ~]$ cat $DOCUMENTUM/dba/log/$DOCBASE_NAME.log
...
2025-12-08T12:21:16.235912 3567122[3567122] 0101234580c77e96 [AUTH] Entering RPC AUTHENTICATE_USER
2025-12-08T12:21:16.236052 3567122[3567122] 0101234580c77e96 [AUTH] Start Authentication : LOGON_NAME=MYUSERID, DOMAIN_NAME=, OS_LOGON_NAME=tomcat, OS_LOGON_DOMAIN=, ASSUME_USER=0, TRUSTED_LOGIN_ALLOWED=1, PRINCIPAL_AUTH=0, DO_SET_LOCALE=0, RECONNECT=0, CLIENT_TOKEN=[-36, 8, 66, 12, 89, 102, -85, -11, 6, -115, -34, -68, -123, 11, 100]
2025-12-08T12:21:16.236115 3567122[3567122] 0101234580c77e96 [AUTH] Start Authenticate Client Instance
2025-12-08T12:21:16.236215 3567122[3567122] 0101234580c77e96 [AUTH] Start Verify Signature, Client : dfc_327WHMY40Mglbp4taDgajZEM39Lc , Host : d2-0.d2.dctm-ns.svc.cluster.local
2025-12-08T12:21:16.244603 3567122[3567122] 0101234580c77e96 [AUTH] End Verify Signature, Client : dfc_327WHMY40Mglbp4taDgajZEM39Lc , Host : d2-0.d2.dctm-ns.svc.cluster.local
2025-12-08T12:21:16.244657 3567122[3567122] 0101234580c77e96 [AUTH] End Authenticate Client Instance
2025-12-08T12:21:16.303325 3567122[3567122] 0101234580c77e96 [AUTH] Start-AuthenticateUser: ClientHost(d2-0.d2.dctm-ns.svc.cluster.local), LogonName(null), LogonOSName(tomcat), LogonOSDomain(), UserExtraDomain(), ServerDomain()
2025-12-08T12:21:16.303410 3567122[3567122] 0101234580c77e96 [AUTH] Start-AuthenticateUserName:
2025-12-08T12:21:16.303442 3567122[3567122] 0101234580c77e96 [AUTH] dmResolveNamesForCredentials: auth_protocol()
2025-12-08T12:21:16.305698 3567122[3567122] 0101234580c77e96 [AUTH] [DM_USER_E_NOT_DOCUMENTUM_USER]error: "User null does not exist in the docbase"
2025-12-08T12:21:16.305720 3567122[3567122] 0101234580c77e96 [AUTH] End-AuthenticateUserName: dm_user.user_login_domain(), Result: 0
2025-12-08T12:21:16.305730 3567122[3567122] 0101234580c77e96 [AUTH] Not Found dm_user.user_login_name(null), dm_user.user_login_domain()
2025-12-08T12:21:16.519331 3567122[3567122] 0101234580c77e96 [AUTH] Final Auth Result=F, LOGON_NAME=null, AUTHENTICATION_LEVEL=1, OS_LOGON_NAME=tomcat, OS_LOGON_DOMAIN=, CLIENT_HOST_NAME=d2-0.d2.dctm-ns.svc.cluster.local, CLIENT_HOST_ADDR=172.1.1.1, USER_LOGON_NAME_RESOLVED=1, AUTHENTICATION_ONLY=0, USER_NAME=, USER_OS_NAME=null, USER_LOGIN_NAME=null, USER_LOGIN_DOMAIN=, USER_EXTRA_CREDENTIAL[0]=, USER_EXTRA_CREDENTIAL[1]=, USER_EXTRA_CREDENTIAL[2]=e2, USER_EXTRA_CREDENTIAL[3]=, USER_EXTRA_CREDENTIAL[4]=, USER_EXTRA_CREDENTIAL[5]=, SERVER_SESSION_ID=0101234580c77e96, AUTH_BEGIN_TIME=Mon Dec 8 12:21:16 2025, AUTH_END_TIME=Mon Dec 8 12:21:16 2025, Total elapsed time=0 seconds
2025-12-08T12:21:16.519359 3567122[3567122] 0101234580c77e96 [AUTH] Exiting RPC AUTHENTICATE_USER
...
[dmadmin@cs-0 ~]$
There is one thing that is quite strange in these logs. If you look at the beginning, it traces the authentication for “MYUSERID“. But then, in the middle of the process, that user_name becomes “null“. I do not recall seeing that behavior before, so I started investigating what might have caused it.
The account “MYUSERID” existed in the Repository. This issue occurred on the same application as in the previous blog post, but this time in the TEST/QA environment (instead of DEV). The same OTDS and users were present, so my account was definitely there (without duplicates in TEST/QA).
3. Investigating OTDS authentication logsSince the dm_user object had a “user_source” of OTDS, I then checked the OTDS Authentication log file from the JMS. For this Documentum 23.4 version, the log file was “$JMS_HOME/logs/otdsauth.log“. Starting from version 25.4, this log file is located inside “$DOCUMENTUM/dba/log” instead:
[dmadmin@cs-0 ~]$ cat $JMS_HOME/logs/otdsauth.log
...
2025-12-08 11:49:46,106 UTC ERROR [] (https-jsse-nio-9082-exec-36) Thread[https-jsse-nio-9082-exec-36,5,main] java.io.IOException: Unable to tunnel through proxy. Proxy returns "HTTP/1.1 502 Bad Gateway"
at java.base/sun.net.www.protocol.http.HttpURLConnection.doTunneling0(HttpURLConnection.java:2311)
at java.base/sun.net.www.protocol.http.HttpURLConnection.doTunneling(HttpURLConnection.java:2181)
at java.base/sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
at java.base/sun.net.www.protocol.http.HttpURLConnection.getOutputStream0(HttpURLConnection.java:1465)
at java.base/sun.net.www.protocol.http.HttpURLConnection.getOutputStream(HttpURLConnection.java:1436)
at java.base/sun.net.www.protocol.https.HttpsURLConnectionImpl.getOutputStream(HttpsURLConnectionImpl.java:220)
at com.documentum.cs.otds.OTDSAuthenticationServlet.validatePassword(OTDSAuthenticationServlet.java:275)
at com.documentum.cs.otds.OTDSAuthenticationServlet.doPost(OTDSAuthenticationServlet.java:175)
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590)
...
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1740)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.base/java.lang.Thread.run(Thread.java:840)
2025-12-08 12:21:16,302 UTC ERROR [] (https-jsse-nio-9082-exec-50) Exception while fetching certificates from jwks url
[dmadmin@cs-0 ~]$
The first error message (11:49) occurred about 30 minutes before the authentication attempt. On the other hand, the last line (12:21) is directly linked to the problem according to its timestamp. This indicates that the Documentum Server was trying to fetch the JWKS certificate. This happens when the OTDS Authentication Servlet is configured with the “auto_cert_refresh=true” parameter (see the “otdsauth.properties” file).
This forces the Documentum Server to contact the OTDS Server in order to retrieve the correct or current SSL certificate to use. However, that request failed. Even though it is not explicitly written, it is easy to deduce that the first error, related to a proxy communication issue, is the root cause.
4. Checking newly added proxy and correcting itAs far as I knew, there should not have been any proxy configured on Documentum, since all components are internal to the customer and located within the same network. However, when checking the startup logs of the JMS, I noticed that a new proxy configuration had recently been added when the Tomcat process restarted less than two hours earlier:
[dmadmin@cs-0 ~]$ grep proxy $JMS_HOME/logs/catalina.out
...
2025-12-08 10:54:56,385 UTC INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dhttp.proxyHost=proxy.domain.com
2025-12-08 10:54:56,385 UTC INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dhttp.proxyPort=2010
2025-12-08 10:54:56,385 UTC INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dhttps.proxyHost=proxy.domain.com
2025-12-08 10:54:56,385 UTC INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dhttps.proxyPort=2011
...
[dmadmin@cs-0 ~]$
After checking with the relevant teams, it turned out that this issue was not really related to Documentum itself. Someone had simply restarted the JMS after adding proxy settings as new JVM parameters while testing an external service that required internet access. Yes, directly in TEST/QA without validating in DEV first – it happens apparently.
However, since no exceptions were configured through the no_proxy setting (“-Dhttp.nonProxyHosts” JVM parameter), it meant that 100% of the requests initiated by the JVM were forwarded to the proxy. That proxy had no knowledge of the OTDS server (which is expected), so the communication simply failed.
After correcting the proxy configuration (either by removing it or by adding all internal domains to the no_proxy setting), the JVM was able to communicate with OTDS again. As a consequence, the D2 SSO started working successfully and the environment was back “online” for all testers. These two blog posts clearly demonstrate that just because D2 displays an error, it doesn’t mean that the real root cause is obvious. Careful investigation and analysis of the log files is always essential.
L’article Dctm – Another DM_LICENSE_E_INVALID_LICENSE error but caused by JMS this time est apparu en premier sur dbi Blog.
Credential Errors (OGG-15409) with GoldenGate Migration Utility
The GoldenGate migration utility provided by Oracle allows you to quickly upgrade your classic architecture into GoldenGate 26ai with Microservices Architecture. But even after some updates, it still has a few bugs, as I explained in a previous blog post.
One of them can lead to an OGG-15409 error during the migration. This error will not appear when running the migration tool in dryrun mode. You might then be faced with this issue only when doing the real migration. Here is the exact error:
ERROR: Unable to patch EXTRACT EXT, response is HTTP Status-Code 400: Bad Request..
[ERROR] OGG-15409 - Alias 'ggadmin_alias' not found in credential store domain 'OracleGoldenGate'.
Extract EXT Process Definitions patched.
Where does the error come from ?
The first step is to understand what is causing the issue. For this, you need to understand how the GoldenGate migration utility works.
When migrating extracts (or replicats), GoldenGate will make API calls to the new Microservices Architecture administration service to register the extract (or replicat). Once created, it will alter it with a PATCH request to update the credentials used.
We can see it in the restapi.log:
{"context":{"verb":"PATCH","uri":"/services/v2/extract/EXT",...},"content":{"credentials":{"alias":"ggadmin_alias","domain":"OracleGoldenGate"}},...}
Unfortunately, once the migration is done, you cannot re-run the migration. You will need to fix this manually.
But since this is the only post-migration task made on extracts and replicats, it is rather easy to do. You can just create the aliases first, and call the REST API to alter all extracts and replicats. In Python, using the client I presented in a previous blog post, it would look like the following. First, create the client connection.
from oggrestapi import OGGRestAPI
ogg_client = OGGRestAPI(url='https://vmogg:7810', username='ogg', password='***')
Then, check the content of the extract (or replicat) using the retrieve_extract (or retrieve_replicat) method. For the moment, we don’t see any credentials key.
# This retrieves all the configuration of an extract, except for the configuration file
>>> {k:v for k,v in ogg_client.retrieve_extract('EXT').items() if k != 'config'}
{'$schema': 'ogg:extract', 'targets': [{'name': 'aa', 'path': 'source', 'sizeMB': 500, ...}], 'description': 'dbi blog migration', 'source': 'tranlogs', 'type': 'Integrated'}
Then, create the alias(es) with the create_alias method.
ogg_client.create_alias(
alias='ggadmin_alias',
domain='OracleGoldenGate',
data={
"userid":"ggadmin@vmora:1521/DB",
"password": "***"
}
)
And finally, alter the extracts with the update_extract method.
ogg_client.update_extract(
extract='EXT',
data={
"alias": "ggadmin_alias",
"domain": "OracleGoldenGate"
}
)
If you had the issue with a replicat, the syntax is exactly the same, with the update_replicat method.
ogg_client.update_replicat(
replicat='REP',
data={
"alias": "ggadmin_alias",
"domain": "OracleGoldenGate"
}
)
You can check that the credentials are there by reusing the retrieve_extract (or retrieve_replicat) method. This time, we see the credentials key !
>>> {k:v for k,v in ogg_client.retrieve_extract('EXT').items() if k != 'config'}
{'$schema': 'ogg:extract', 'credentials': {'alias': 'ggadmin_alias', 'domain': 'OracleGoldenGate'}, 'targets': [{'name': 'aa', 'path': 'source', 'sizeMB': 500, ...}], 'description': 'dbi blog migration', 'source': 'tranlogs', 'type': 'Integrated', ...}
How to avoid this error ?
For some reason, the credentials of the source setup will not always be migrated. If you don’t have too many aliases, I would suggest creating the aliases in the target environment. This way, you know they are working even before attempting the migration. This should definitely be part of your new deployment tests.
L’article Credential Errors (OGG-15409) with GoldenGate Migration Utility est apparu en premier sur dbi Blog.


