Skip to content

Commit 2c360cb

Browse files
committed
mk-oracle: fix monitoring of Oracle 12.1 and 12.2
Neither release was monitored at all. The version was determined by VERSION_FULL, with fallback to V$VERSION.BANNER_FULL. However, both were unavailable before 18c. - version probe: VERSION_FULL where it exists, else VERSION. Retried on ORA-00904 alone, so a permissions or connection error still surfaces instead of reading as an ancient release. - version extraction no longer takes the last whitespace-separated token, which is "Production" in a one-line banner. Releases below the supported floor of 12.1.0.2 are not accommodated: they fail on the absent V$DATABASE.CDB column, with the version the probe established already in the log. TESTS: Manual and automated. Confirmed against real Oracle DBs in Docker. Tested DB versions: 12.1 and 12.2 now emit their sections where they emitted none, 19.3 and 23.26 are unchanged, and 11.2 fails on the absent column. CMK-37730 Change-Id: Ifb0484f201ca1a93ceb36f04dd9f3e44ce48d82b
1 parent b8b1211 commit 2c360cb

5 files changed

Lines changed: 243 additions & 60 deletions

File tree

packages/mk-oracle/src/ora_sql/backend.rs

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,11 @@ pub fn obtain_config_credentials(auth: &Authentication) -> Option<Credentials> {
479479
pub(crate) mod test_support {
480480
//! Fake [`OraDbEngine`] and helpers for tests.
481481
use super::*;
482-
use crate::ora_sql::sqls::query::internal::{INSTANCE_INFO_SQL_TEXT_NEW, PDB_DISCOVERY_SQL};
482+
use crate::ora_sql::sqls::query::internal::{
483+
INSTANCE_INFO_SQL_TEXT_NEW, INSTANCE_INFO_SQL_TEXT_OLD, INSTANCE_VERSION,
484+
INSTANCE_VERSION_FULL, PDB_DISCOVERY_SQL,
485+
};
486+
use std::sync::{Arc, Mutex};
483487

484488
/// A `v$instance` row; parsing reads only columns 0 (name), 2 (version), 4 (cdb).
485489
pub fn instance_row(name: &str, version: &str, cdb: &str) -> Vec<String> {
@@ -498,6 +502,21 @@ pub(crate) mod test_support {
498502
pub instance_rows: Vec<Vec<String>>,
499503
pub pdb_rows: Vec<Vec<String>>,
500504
pub default_rows: Vec<Vec<String>>,
505+
/// A query naming any of these is answered with ORA-00904.
506+
pub absent_columns: Vec<String>,
507+
/// Shared with every clone from `clone_box`, so one run collects here.
508+
pub asked: Arc<Mutex<Vec<String>>>,
509+
pub version_rows: Vec<Vec<String>>,
510+
}
511+
512+
impl MiniOra {
513+
fn missing_column_in(&self, query: &str) -> Option<&str> {
514+
let upper = query.to_uppercase();
515+
self.absent_columns
516+
.iter()
517+
.find(|c| upper.contains(&c.to_uppercase()))
518+
.map(String::as_str)
519+
}
501520
}
502521

503522
impl OraDbEngine for MiniOra {
@@ -511,8 +530,15 @@ pub(crate) mod test_support {
511530
Ok(())
512531
}
513532
fn query_table(&self, query: &SqlQuery) -> QueryResult {
533+
self.asked.lock().unwrap().push(query.as_str().to_owned());
534+
if let Some(column) = self.missing_column_in(query.as_str()) {
535+
return QueryResult(Err(anyhow::anyhow!(
536+
"ORA-00904: \"{column}\": invalid identifier"
537+
)));
538+
}
514539
let rows = match query.as_str() {
515-
INSTANCE_INFO_SQL_TEXT_NEW => &self.instance_rows,
540+
INSTANCE_INFO_SQL_TEXT_NEW | INSTANCE_INFO_SQL_TEXT_OLD => &self.instance_rows,
541+
INSTANCE_VERSION_FULL | INSTANCE_VERSION => &self.version_rows,
516542
PDB_DISCOVERY_SQL => &self.pdb_rows,
517543
_ => &self.default_rows,
518544
};
@@ -523,16 +549,40 @@ pub(crate) mod test_support {
523549
instance_rows: self.instance_rows.clone(),
524550
pdb_rows: self.pdb_rows.clone(),
525551
default_rows: self.default_rows.clone(),
552+
absent_columns: self.absent_columns.clone(),
553+
asked: Arc::clone(&self.asked),
554+
version_rows: self.version_rows.clone(),
526555
})
527556
}
528557
}
529558

530559
impl MiniOra {
531560
/// One non-CDB instance `name`; custom queries return `details:ok`.
532561
pub fn single(name: &str) -> Self {
562+
Self::at_version(name, "19.1.0.0", "NO")
563+
}
564+
565+
/// Derives `absent_columns` from `version`.
566+
pub fn at_version(name: &str, version: &str, cdb: &str) -> Self {
567+
let numeric: Vec<u32> = version
568+
.split('.')
569+
.filter_map(|p| p.parse::<u32>().ok())
570+
.collect();
571+
let major = numeric.first().copied().unwrap_or(0);
572+
let minor = numeric.get(1).copied().unwrap_or(0);
573+
let mut absent_columns = Vec::new();
574+
if major < 18 {
575+
absent_columns.push("VERSION_FULL".to_string());
576+
}
577+
if major < 12 || (major == 12 && minor < 1) {
578+
absent_columns.push("CON_ID".to_string());
579+
absent_columns.push("d.cdb".to_string());
580+
}
533581
Self {
534-
instance_rows: vec![instance_row(name, "19.1.0.0", "NO")],
582+
instance_rows: vec![instance_row(name, version, cdb)],
535583
default_rows: vec![vec!["details:ok".to_string()]],
584+
version_rows: vec![vec![version.to_string()]],
585+
absent_columns,
536586
..Default::default()
537587
}
538588
}

packages/mk-oracle/src/ora_sql/instance.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,13 +872,15 @@ oracle:
872872
);
873873
let db = MiniOra {
874874
instance_rows: vec![instance_row("ORCL", "19.1.0.0", "NO")],
875+
version_rows: vec![vec!["19.1.0.0".to_string()]],
875876
pdb_rows: vec![],
876877
default_rows: vec![
877878
vec!["details:price=5; still ok".to_string()],
878879
vec!["perfdata:price=5;10;20;;".to_string()],
879880
vec!["long:extended detail".to_string()],
880881
vec!["exit:0".to_string()],
881882
],
883+
..Default::default()
882884
};
883885

884886
let out = emit(
@@ -963,12 +965,14 @@ oracle:
963965
);
964966
let db = MiniOra {
965967
instance_rows: vec![instance_row("ORCL", "19.1.0.0", "YES")],
968+
version_rows: vec![vec!["19.1.0.0".to_string()]],
966969
pdb_rows: vec![
967970
vec!["CDB$ROOT".to_string()],
968971
vec!["PDB$SEED".to_string()],
969972
vec!["PDB1".to_string()],
970973
],
971974
default_rows: vec![vec!["details:ok".to_string()]],
975+
..Default::default()
972976
};
973977

974978
let out = emit(

packages/mk-oracle/src/ora_sql/sqls.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,8 @@ SELECT
275275
FROM v$instance i
276276
join v$database d
277277
on i.con_id = d.con_id";
278+
pub const INSTANCE_VERSION_FULL: &str = r"SELECT VERSION_FULL FROM v$instance";
279+
pub const INSTANCE_VERSION: &str = r"SELECT VERSION FROM v$instance";
278280
/// ASM instances have no mounted `v$database`.
279281
pub const ASM_INSTANCE_INFO_SQL_TEXT_NEW: &str = r"
280282
SELECT
@@ -292,8 +294,6 @@ SELECT
292294
INSTANCE_NAME,
293295
'NO'
294296
FROM v$instance";
295-
pub const INSTANCE_APPROXIMATE_VERSION: &str =
296-
r"SELECT BANNER_FULL FROM v$version WHERE banner LIKE 'Oracle%'";
297297
/// Lists PDBs of the connected CDB. The root container (CDB$ROOT) is
298298
/// not in V$PDBS, but we exclude it defensively. PDB$SEED is the
299299
/// read-only template and must always be filtered out.

packages/mk-oracle/src/ora_sql/system.rs

Lines changed: 171 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -75,49 +75,87 @@ impl WorkInstances {
7575
}
7676
}
7777

78+
/// The only error that means "this release lacks the column"; anything else is
79+
/// a real failure and must not be retried away.
80+
fn is_unknown_column_error(e: &anyhow::Error) -> bool {
81+
e.to_string().contains("ORA-00904")
82+
}
83+
84+
/// Oracle changed the versioning information with release 18c, introducing
85+
/// `VERSION_FULL`. Where that is unavailable, `VERSION` carries the full
86+
/// versioning information instead.
87+
#[derive(Debug)]
88+
enum DetectedVersion {
89+
Since18c(InstanceVersion),
90+
Before18c(InstanceVersion),
91+
}
92+
93+
impl DetectedVersion {
94+
fn version(&self) -> &InstanceVersion {
95+
match self {
96+
Self::Since18c(v) | Self::Before18c(v) => v,
97+
}
98+
}
99+
}
100+
101+
fn _detect_version(spot: &OpenedSpot) -> Result<DetectedVersion> {
102+
let ask = |sql: &str| -> Result<InstanceVersion> {
103+
let rows = spot.query_table(&SqlQuery::new(sql, &Vec::new())).0?;
104+
_extract_version(rows.into_iter().map(|row| row.join("")).collect())
105+
.map(InstanceVersion::from)
106+
.ok_or_else(|| anyhow::anyhow!("No usable version in v$instance"))
107+
};
108+
match ask(sqls::query::internal::INSTANCE_VERSION_FULL) {
109+
Ok(version) => Ok(DetectedVersion::Since18c(version)),
110+
Err(e) if is_unknown_column_error(&e) => {
111+
log::info!("No VERSION_FULL in v$instance ({e}), asking for VERSION instead");
112+
ask(sqls::query::internal::INSTANCE_VERSION).map(DetectedVersion::Before18c)
113+
}
114+
Err(e) => Err(e),
115+
}
116+
}
117+
78118
fn _get_instances(spot: &OpenedSpot, custom_query: Option<&str>) -> Result<_InstanceEntries> {
79-
let (new_sql, old_sql) = if spot.target().is_asm() {
80-
(
81-
sqls::query::internal::ASM_INSTANCE_INFO_SQL_TEXT_NEW,
82-
sqls::query::internal::ASM_INSTANCE_INFO_SQL_TEXT_OLD,
83-
)
84-
} else {
85-
(
86-
sqls::query::internal::INSTANCE_INFO_SQL_TEXT_NEW,
87-
sqls::query::internal::INSTANCE_INFO_SQL_TEXT_OLD,
88-
)
119+
if let Some(query) = custom_query {
120+
// Replaces the probe entirely; the caller owns the column set.
121+
return Ok(_to_instance_entries(
122+
spot.query_table(&SqlQuery::new(query, &Vec::new())).0?,
123+
));
124+
}
125+
126+
let detected = _detect_version(spot)?;
127+
log::info!("Instance reports {detected:?}");
128+
129+
let sql = match (spot.target().is_asm(), &detected) {
130+
(true, DetectedVersion::Since18c(_)) => {
131+
sqls::query::internal::ASM_INSTANCE_INFO_SQL_TEXT_NEW
132+
}
133+
(true, DetectedVersion::Before18c(_)) => {
134+
sqls::query::internal::ASM_INSTANCE_INFO_SQL_TEXT_OLD
135+
}
136+
(false, DetectedVersion::Since18c(_)) => sqls::query::internal::INSTANCE_INFO_SQL_TEXT_NEW,
137+
(false, DetectedVersion::Before18c(_)) => sqls::query::internal::INSTANCE_INFO_SQL_TEXT_OLD,
89138
};
90-
if let Ok(result) = spot
91-
.query_table(&SqlQuery::new(custom_query.unwrap_or(new_sql), &Vec::new()))
92-
.0
93-
{
94-
Ok(_to_instance_entries(result))
95-
} else {
96-
let mut result = spot.query_table(&SqlQuery::new(old_sql, &Vec::new())).0?;
97-
let result_with_version = spot
98-
.query_table(&SqlQuery::new(
99-
sqls::query::internal::INSTANCE_APPROXIMATE_VERSION,
100-
&Vec::new(),
101-
))
102-
.format("")?;
103-
if let Some(version) = _extract_version(result_with_version) {
104-
log::info!("Extracted version: {version}");
105-
for r in result.iter_mut() {
106-
r[2] = version.clone(); // Update the version column
107-
}
139+
140+
let mut result = spot.query_table(&SqlQuery::new(sql, &Vec::new())).0?;
141+
for r in result.iter_mut() {
142+
if let Some(version_column) = r.get_mut(2) {
143+
*version_column = detected.version().clone().into();
108144
}
109-
Ok(_to_instance_entries(result))
110145
}
146+
Ok(_to_instance_entries(result))
111147
}
112148

113149
fn _extract_version(result: Vec<String>) -> Option<String> {
114150
if result.is_empty() {
115151
log::warn!("No version information found in v$instance");
116152
return None;
117153
}
118-
result[0].split(' ').next_back().and_then(|s| {
119-
convert_to_num_version(&InstanceVersion::from(s.to_owned())).map(|_| s.to_string())
120-
})
154+
result
155+
.iter()
156+
.flat_map(|line| line.split_whitespace())
157+
.find(|token| convert_to_num_version(&InstanceVersion::from((*token).to_owned())).is_some())
158+
.map(str::to_string)
121159
}
122160

123161
fn _to_instance_entries(result: Vec<Vec<String>>) -> _InstanceEntries {
@@ -160,18 +198,115 @@ pub fn convert_to_num_version(version: &InstanceVersion) -> Option<InstanceNumVe
160198
mod tests {
161199
use super::*;
162200
use crate::config::ora_sql::Endpoint;
163-
use crate::ora_sql::backend::test_support::{instance_row, MiniOra};
201+
use crate::ora_sql::backend::test_support::MiniOra;
164202
use crate::ora_sql::backend::SpotBuilder;
165203

204+
/// Returns the derived (version, tenant) plus every query the run issued.
205+
fn discover_at(version: &str, cdb: &str) -> (InstanceNumVersion, Tenant, Vec<String>) {
206+
let db = MiniOra::at_version("ORCL", version, cdb);
207+
let asked = std::sync::Arc::clone(&db.asked);
208+
let spot = SpotBuilder::new()
209+
.endpoint_target(&Endpoint::default())
210+
.custom_engine(Box::new(db))
211+
.build()
212+
.unwrap()
213+
.connect(None)
214+
.unwrap();
215+
let works = WorkInstances::new(&spot, None).expect("instance must be discovered");
216+
let (v, t) = works
217+
.get_info(&InstanceName::from("ORCL"))
218+
.expect("ORCL must be present");
219+
let queries = asked.lock().unwrap().clone();
220+
(v, t, queries)
221+
}
222+
166223
#[test]
167-
fn test_get_version() {
168-
let simulated_spot = SpotBuilder::new()
224+
fn test_18c_and_later_use_the_precise_version_and_ask_for_cdb() {
225+
let (version, tenant, asked) = discover_at("19.28.0.0.0", "YES");
226+
assert_eq!(version, InstanceNumVersion::from(19_28_00_00));
227+
assert_eq!(tenant, Tenant::Cdb);
228+
assert!(
229+
asked.iter().any(|q| q.contains("VERSION_FULL")),
230+
"{asked:?}"
231+
);
232+
assert!(asked.iter().any(|q| q.contains("d.cdb")), "{asked:?}");
233+
}
234+
235+
#[test]
236+
fn test_12c_falls_back_to_version_but_still_asks_for_cdb() {
237+
// No VERSION_FULL, but CON_ID and CDB are there: the probe degrades
238+
// while the instance query still reads the real tenancy.
239+
let (version, tenant, asked) = discover_at("12.2.0.1.0", "YES");
240+
assert_eq!(version, InstanceNumVersion::from(12_02_00_01));
241+
assert_eq!(tenant, Tenant::Cdb);
242+
assert!(
243+
asked
244+
.iter()
245+
.any(|q| q.trim() == "SELECT VERSION FROM v$instance"),
246+
"the probe must fall back to VERSION: {asked:?}"
247+
);
248+
assert!(asked.iter().any(|q| q.contains("d.cdb")), "{asked:?}");
249+
}
250+
251+
/// The supported floor is 12.1.0.2, so a release without `V$DATABASE.CDB`
252+
/// fails rather than being accommodated. Its version reaches the log first,
253+
/// which is what tells an operator why.
254+
#[test]
255+
fn test_before_12c_fails_on_the_absent_cdb_column() {
256+
let db = MiniOra::at_version("ORCL", "11.2.0.4.0", "irrelevant");
257+
let spot = SpotBuilder::new()
258+
.endpoint_target(&Endpoint::default())
259+
.custom_engine(Box::new(db))
260+
.build()
261+
.unwrap()
262+
.connect(None)
263+
.unwrap();
264+
let err = WorkInstances::new(&spot, None).expect_err("must not be accommodated");
265+
assert!(err.to_string().contains("ORA-00904"), "{err}");
266+
}
267+
268+
#[test]
269+
fn test_a_real_error_is_not_treated_as_a_missing_column() {
270+
// Anything but ORA-00904 must surface, or a permissions problem reads
271+
// as an ancient Oracle.
272+
let spot = SpotBuilder::new()
169273
.endpoint_target(&Endpoint::default())
170274
.custom_engine(Box::new(MiniOra {
171-
instance_rows: vec![instance_row("free", "22.1.1.6.0", "YES")],
275+
absent_columns: vec!["v$instance".to_string()],
172276
..Default::default()
173277
}))
174278
.build()
279+
.unwrap()
280+
.connect(None)
281+
.unwrap();
282+
let err = WorkInstances::new(&spot, None).expect_err("must not be swallowed");
283+
assert!(err.to_string().contains("ORA-00904"), "{err}");
284+
}
285+
286+
#[test]
287+
fn test_extract_version_is_not_positional() {
288+
assert_eq!(
289+
_extract_version(vec!["23.26.0.24.03".to_string()]).as_deref(),
290+
Some("23.26.0.24.03")
291+
);
292+
assert_eq!(
293+
_extract_version(vec![
294+
"Oracle Database 12c Enterprise Edition Release 12.1.0.2.0 - 64bit Production"
295+
.to_string()
296+
])
297+
.as_deref(),
298+
Some("12.1.0.2.0")
299+
);
300+
assert_eq!(_extract_version(vec![]), None);
301+
assert_eq!(_extract_version(vec!["no version here".to_string()]), None);
302+
}
303+
304+
#[test]
305+
fn test_get_version() {
306+
let simulated_spot = SpotBuilder::new()
307+
.endpoint_target(&Endpoint::default())
308+
.custom_engine(Box::new(MiniOra::at_version("free", "22.1.1.6.0", "YES")))
309+
.build()
175310
.unwrap();
176311
let conn = simulated_spot.connect(None).unwrap();
177312
assert_eq!(

0 commit comments

Comments
 (0)