Skip to content

Commit b981e15

Browse files
Added support for processing tnsnames.ora files containing IFILE
directives (#311).
1 parent 3a9c365 commit b981e15

File tree

6 files changed

+607
-166
lines changed

6 files changed

+607
-166
lines changed

doc/src/release_notes.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,19 @@ Common Changes
3030
is represented in Python by instances of the new
3131
:ref:`oracledb.IntervalYM <interval_ym>` class
3232
(`issue 310 <https://github.com/oracle/python-oracledb/issues/310>`__).
33+
#) Added support for processing :ref:`tnsnames.ora files <optnetfiles>`
34+
containing ``IFILE`` directives
35+
(`issue 311 <https://github.com/oracle/python-oracledb/issues/311>`__).
3336
#) Added support for getting a list of the network service names found in a
3437
:ref:`tnsnames.ora <optnetfiles>` file by adding the method
3538
:meth:`ConnectParams.get_network_service_names()`
3639
(`issue 313 <https://github.com/oracle/python-oracledb/issues/313>`__).
3740
#) Added support for iterating over :ref:`DbObject <dbobject>` instances that
3841
are collections
3942
(`issue 314 <https://github.com/oracle/python-oracledb/issues/314>`__).
43+
#) Error ``DPY-4032: invalid network service definition detected at line
44+
{line_no} of file '{file_name}'`` is now raised when an invalid network
45+
service definition is found in a :ref:`tnsnames.ora <optnetfiles>` file.
4046
#) Error ``ORA-24545: invalid value of POOL_BOUNDARY specified in connect
4147
string`` is now raised consistently for both Thick and Thin modes.
4248
Previously, Thin mode was raising the error

src/oracledb/base_impl.pxd

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -384,13 +384,6 @@ cdef class DescriptionList(ConnectParamsNode):
384384
cdef str build_connect_string(self)
385385

386386

387-
cdef class TnsnamesFile:
388-
cdef:
389-
str file_name
390-
int mtime
391-
dict entries
392-
393-
394387
cdef class ConnectParamsImpl:
395388
cdef:
396389
public str config_dir
@@ -432,7 +425,6 @@ cdef class ConnectParamsImpl:
432425
cdef bytearray _get_obfuscator(self, str secret_value)
433426
cdef bytes _get_password(self)
434427
cdef str _get_private_key(self)
435-
cdef TnsnamesFile _get_tnsnames_file(self)
436428
cdef str _get_token(self)
437429
cdef object _get_token_expires(self, str token)
438430
cdef str _get_wallet_password(self)

src/oracledb/errors.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,10 +294,13 @@ def _raise_err(
294294
ERR_INVALID_POOL_PURITY = 4022
295295
ERR_CALL_TIMEOUT_EXCEEDED = 4024
296296
ERR_INVALID_REF_CURSOR = 4025
297-
ERR_TNS_NAMES_FILE_MISSING = 4026
297+
ERR_MISSING_FILE = 4026
298298
ERR_NO_CONFIG_DIR = 4027
299299
ERR_INVALID_SERVER_TYPE = 4028
300300
ERR_TOO_MANY_BATCH_ERRORS = 4029
301+
ERR_IFILE_CYCLE_DETECTED = 4030
302+
ERR_NETWORK_SERVICE_NAME_DIFFERS = 4031
303+
ERR_NETWORK_SERVICE_NAME_INVALID = 4032
301304

302305
# error numbers that result in InternalError
303306
ERR_MESSAGE_TYPE_UNKNOWN = 5000
@@ -489,6 +492,10 @@ def _raise_err(
489492
ERR_HTTPS_PROXY_REQUIRES_TCPS: (
490493
"https_proxy requires use of the tcps protocol"
491494
),
495+
ERR_IFILE_CYCLE_DETECTED: (
496+
"file '{including_file_name}' includes file '{included_file_name}', "
497+
"which forms a cycle"
498+
),
492499
ERR_INCONSISTENT_DATATYPES: (
493500
"cannot convert from data type {input_type} to {output_type}"
494501
),
@@ -572,6 +579,7 @@ def _raise_err(
572579
'a bind variable replacement value for placeholder ":{name}" was '
573580
"not provided"
574581
),
582+
ERR_MISSING_FILE: "file '{file_name}' is missing or unreadable",
575583
ERR_MISSING_QUOTE_IN_IDENTIFIER: 'missing ending quote (") in identifier',
576584
ERR_MISSING_QUOTE_IN_STRING: "missing ending quote (') in string",
577585
ERR_MISSING_TYPE_NAME_FOR_OBJECT_VAR: (
@@ -590,6 +598,15 @@ def _raise_err(
590598
"national character set id {charset_id} is not supported by "
591599
"python-oracledb in thin mode"
592600
),
601+
ERR_NETWORK_SERVICE_NAME_DIFFERS: (
602+
"connect string for network service name '{network_service_name}' "
603+
"found in file '{new_file_name}' differs from the same entry in "
604+
"'{orig_file_name}'"
605+
),
606+
ERR_NETWORK_SERVICE_NAME_INVALID: (
607+
"invalid network service definition detected at line {line_no} of "
608+
"file '{file_name}'"
609+
),
593610
ERR_NO_CONFIG_DIR: "no configuration directory to search for tnsnames.ora",
594611
ERR_NO_CREDENTIALS: "no credentials specified",
595612
ERR_NO_CRYPTOGRAPHY_PACKAGE: (
@@ -647,7 +664,6 @@ def _raise_err(
647664
"Oracle Database does not support time only variables"
648665
),
649666
ERR_TNS_ENTRY_NOT_FOUND: 'unable to find "{name}" in {file_name}',
650-
ERR_TNS_NAMES_FILE_MISSING: "file tnsnames.ora not found in {config_dir}",
651667
ERR_TOO_MANY_CURSORS_TO_CLOSE: (
652668
"internal error: attempt to close more than {num_cursors} cursors"
653669
),

src/oracledb/impl/base/connect_params.pyx

Lines changed: 137 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -250,32 +250,6 @@ cdef class ConnectParamsImpl:
250250
return self._xor_bytes(self._private_key,
251251
self._private_key_obfuscator).decode()
252252

253-
cdef TnsnamesFile _get_tnsnames_file(self):
254-
"""
255-
Return a tnsnames file, if one is present, or None if one is not. If
256-
the file was previously loaded, the modification time is checked and,
257-
if unchanged, the previous file is returned.
258-
"""
259-
cdef TnsnamesFile tnsnames_file
260-
if self.config_dir is None:
261-
errors._raise_err(errors.ERR_NO_CONFIG_DIR)
262-
file_name = os.path.join(self.config_dir, "tnsnames.ora")
263-
tnsnames_file = _tnsnames_files.get(self.config_dir)
264-
try:
265-
stat_info = os.stat(file_name)
266-
except:
267-
if tnsnames_file is not None:
268-
del _tnsnames_files[self.config_dir]
269-
errors._raise_err(errors.ERR_TNS_NAMES_FILE_MISSING,
270-
config_dir=self.config_dir)
271-
if tnsnames_file is not None \
272-
and tnsnames_file.mtime == stat_info.st_mtime:
273-
return tnsnames_file
274-
tnsnames_file = TnsnamesFile(file_name, stat_info.st_mtime)
275-
tnsnames_file.read()
276-
_tnsnames_files[self.config_dir] = tnsnames_file
277-
return tnsnames_file
278-
279253
cdef str _get_token(self):
280254
"""
281255
Returns the token, after removing the obfuscation.
@@ -336,6 +310,7 @@ cdef class ConnectParamsImpl:
336310
"""
337311
cdef:
338312
TnsnamesFile tnsnames_file
313+
TnsnamesFileReader reader
339314
Description description
340315
Address address
341316
dict args = {}
@@ -355,7 +330,8 @@ cdef class ConnectParamsImpl:
355330
# otherwise, see if the name is a connect alias in a tnsnames.ora
356331
# configuration file
357332
else:
358-
tnsnames_file = self._get_tnsnames_file()
333+
reader = TnsnamesFileReader()
334+
tnsnames_file = reader.read_tnsnames(self.config_dir)
359335
name = connect_string
360336
connect_string = tnsnames_file.entries.get(name.upper())
361337
if connect_string is None:
@@ -567,8 +543,10 @@ cdef class ConnectParamsImpl:
567543
file found in the configuration directory associated with the
568544
parameters. If no such file exists, an error is raised.
569545
"""
570-
cdef TnsnamesFile tnsnames_file
571-
tnsnames_file = self._get_tnsnames_file()
546+
cdef:
547+
TnsnamesFileReader reader = TnsnamesFileReader()
548+
TnsnamesFile tnsnames_file
549+
tnsnames_file = reader.read_tnsnames(self.config_dir)
572550
return list(tnsnames_file.entries.keys())
573551

574552
def parse_connect_string(self, str connect_string):
@@ -1015,22 +993,118 @@ cdef class DescriptionList(ConnectParamsNode):
1015993
cdef class TnsnamesFile:
1016994
"""
1017995
Internal class used to parse and retain connect descriptor entries found in
1018-
a tnsnames.ora file.
996+
a tnsnames.ora file or any included file.
1019997
"""
998+
cdef:
999+
str file_name
1000+
int mtime
1001+
dict entries
1002+
set included_files
10201003

1021-
def __init__(self, str file_name, int mtime):
1004+
def __init__(self, str file_name):
10221005
self.file_name = file_name
1023-
self.mtime = mtime
1006+
self.clear()
1007+
self._get_mtime(&self.mtime)
1008+
1009+
cdef int _get_mtime(self, int* mtime) except -1:
1010+
"""
1011+
Returns the modification time of the file or throws an exception if the
1012+
file cannot be found.
1013+
"""
1014+
try:
1015+
mtime[0] = os.stat(self.file_name).st_mtime
1016+
except Exception as e:
1017+
errors._raise_err(errors.ERR_MISSING_FILE, str(e),
1018+
file_name=self.file_name)
1019+
1020+
cdef int clear(self) except -1:
1021+
"""
1022+
Clear all entries in the file.
1023+
"""
10241024
self.entries = {}
1025+
self.included_files = set()
1026+
1027+
def is_current(self):
1028+
"""
1029+
Returns a boolean indicating if the contents are current or not.
1030+
"""
1031+
cdef:
1032+
TnsnamesFile included_file
1033+
int mtime
1034+
self._get_mtime(&mtime)
1035+
if mtime != self.mtime:
1036+
return False
1037+
for included_file in self.included_files:
1038+
if not included_file.is_current():
1039+
return False
1040+
return True
1041+
1042+
1043+
1044+
cdef class TnsnamesFileReader:
1045+
"""
1046+
Internal class used to read a tnsnames.ora file and all of its included
1047+
files.
1048+
"""
1049+
cdef:
1050+
TnsnamesFile primary_file
1051+
list files_in_progress
1052+
dict entries
10251053

1026-
def read(self):
1054+
cdef int _add_entry(self, TnsnamesFile tnsnames_file, str name,
1055+
str value) except -1:
10271056
"""
1028-
Read and parse the file and retain the connect descriptors found inside
1029-
the file.
1057+
Adds an entry to the file, verifying that the name has not been
1058+
duplicated. An entry is always made in the primary file as well.
10301059
"""
1031-
with open(self.file_name) as f:
1060+
cdef TnsnamesFile orig_file
1061+
if name in self.entries and value != self.primary_file.entries[name]:
1062+
orig_file = self.entries[name]
1063+
errors._raise_err(errors.ERR_NETWORK_SERVICE_NAME_DIFFERS,
1064+
network_service_name=name,
1065+
new_file_name=tnsnames_file.file_name,
1066+
orig_file_name=orig_file.file_name)
1067+
self.entries[name] = tnsnames_file
1068+
self.primary_file.entries[name] = value
1069+
if tnsnames_file is not self.primary_file:
1070+
tnsnames_file.entries[name] = value
1071+
1072+
cdef TnsnamesFile _get_file(self, file_name):
1073+
"""
1074+
Get the file from the cache or read it from the file system.
1075+
"""
1076+
cdef TnsnamesFile tnsnames_file
1077+
if file_name in self.files_in_progress:
1078+
errors._raise_err(errors.ERR_IFILE_CYCLE_DETECTED,
1079+
including_file_name=self.files_in_progress[-1],
1080+
included_file_name=file_name)
1081+
tnsnames_file = _tnsnames_files.get(file_name)
1082+
if tnsnames_file is None:
1083+
tnsnames_file = TnsnamesFile(file_name)
1084+
else:
1085+
if tnsnames_file.is_current():
1086+
return tnsnames_file
1087+
del _tnsnames_files[file_name]
1088+
if self.primary_file is None:
1089+
self.primary_file = tnsnames_file
1090+
self.files_in_progress.append(file_name)
1091+
self._read_file(tnsnames_file)
1092+
_tnsnames_files[file_name] = tnsnames_file
1093+
self.files_in_progress.pop()
1094+
return tnsnames_file
1095+
1096+
cdef int _read_file(self, TnsnamesFile tnsnames_file) except -1:
1097+
"""
1098+
Reads the file and parses the contents.
1099+
"""
1100+
cdef:
1101+
TnsnamesFile included_file
1102+
int line_no = 0
1103+
tnsnames_file.clear()
1104+
with open(tnsnames_file.file_name) as f:
10321105
entry_names = None
10331106
for line in f:
1107+
line_no += 1
10341108
line = line.strip()
10351109
pos = line.find("#")
10361110
if pos >= 0:
@@ -1040,8 +1114,20 @@ cdef class TnsnamesFile:
10401114
if entry_names is None:
10411115
pos = line.find("=")
10421116
if pos < 0:
1117+
errors._raise_err(
1118+
errors.ERR_NETWORK_SERVICE_NAME_INVALID,
1119+
line_no=line_no,
1120+
file_name=tnsnames_file.file_name)
1121+
name = line[:pos].strip().upper()
1122+
if name == "IFILE":
1123+
file_name = line[pos + 1:].strip()
1124+
if not os.path.isabs(file_name):
1125+
dir_name = os.path.dirname(tnsnames_file.file_name)
1126+
file_name = os.path.join(dir_name, file_name)
1127+
included_file = self._get_file(file_name)
1128+
tnsnames_file.included_files.add(included_file)
10431129
continue
1044-
entry_names = [s.strip() for s in line[:pos].split(",")]
1130+
entry_names = [s.strip() for s in name.split(",")]
10451131
entry_lines = []
10461132
num_parens = 0
10471133
line = line[pos+1:].strip()
@@ -1051,5 +1137,18 @@ cdef class TnsnamesFile:
10511137
if entry_lines and num_parens <= 0:
10521138
descriptor = "".join(entry_lines)
10531139
for name in entry_names:
1054-
self.entries[name.upper()] = descriptor
1140+
self._add_entry(tnsnames_file, name, descriptor)
10551141
entry_names = None
1142+
1143+
cdef TnsnamesFile read_tnsnames(self, str dir_name):
1144+
"""
1145+
Read the tnsnames.ora file found in the given directory or raise an
1146+
exception if no such file can be found.
1147+
"""
1148+
self.primary_file = None
1149+
self.files_in_progress = []
1150+
self.entries = {}
1151+
if dir_name is None:
1152+
errors._raise_err(errors.ERR_NO_CONFIG_DIR)
1153+
file_name = os.path.join(dir_name, "tnsnames.ora")
1154+
return self._get_file(file_name)

0 commit comments

Comments
 (0)