Skip to content

Commit e44aa98

Browse files
RemoteOperations::exec_command explicitly transfers LANG, LANGUAGE and LC_* envvars to the server side (#187)
* RemoteOperations::exec_command updated - Exact enumeration of supported 'cmd' types - Refactoring * RemoteOperations::exec_command explicitly transfers LANG, LANGUAGE and LC_* envvars to the server side It should help resolve a problem with replacing a LANG variable by ssh-server. History. On our internal tests we got a problem on the Debian 11 and PostgresPro STD-13. One test returned the error from initdb: initdb: error: collations with different collate and ctype values ("en_US.UTF-8" and "C.UTF-8" accordingly) are not supported by ICU - TestRunner set variable LANG="C" - Python set variable LC_CTYPE="C.UTF-8" - Test call inidb through command "ssh test@localhost inidb -D ...." - SSH-server replaces LANG with value "en_US.UTF-8" (from etc/default/locale) - initdb calculate collate through this value of LANG variable and get en_US.UTF-8 So we have that: - ctype is C.UTF-8 - collate is en_US.UTF-8 ICU on the Debuan-11 (uconv v2.1 ICU 67.1) does not suppot this combination and inidb rturns the error. This patch generates a new command line for ssh: ssh test@localhost "LANG=\"...\";LC_xxx=\"...\";<command>" It resolves this problem with initdb and should help resolve other problems with execution of command through SSH. Amen. * New tests in TestgresRemoteTests are added New tests: - test_init__LANG_С - test_init__unk_LANG_and_LC_CTYPE * TestgresRemoteTests.test_init__unk_LANG_and_LC_CTYPE is updated Let's test bad data with '\' and '"' symbols. * Static methods are marked with @staticmethod [thanks to Victoria Shepard] The following methods of RemoteOperations were corrected: - _make_exec_env_list - _does_put_envvar_into_exec_cmd - _quote_envvar * TestRemoteOperations::_quote_envvar is updated (typification)
1 parent 7fd2f07 commit e44aa98

File tree

2 files changed

+124
-1
lines changed

2 files changed

+124
-1
lines changed

testgres/operations/remote_ops.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,12 @@ def exec_command(self, cmd, wait_exit=False, verbose=False, expect_error=False,
8787

8888
assert type(cmd_s) == str # noqa: E721
8989

90-
ssh_cmd = ['ssh', self.ssh_dest] + self.ssh_args + [cmd_s]
90+
cmd_items = __class__._make_exec_env_list()
91+
cmd_items.append(cmd_s)
92+
93+
env_cmd_s = ';'.join(cmd_items)
94+
95+
ssh_cmd = ['ssh', self.ssh_dest] + self.ssh_args + [env_cmd_s]
9196

9297
process = subprocess.Popen(ssh_cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
9398
assert not (process is None)
@@ -510,6 +515,45 @@ def db_connect(self, dbname, user, password=None, host="localhost", port=5432):
510515
)
511516
return conn
512517

518+
@staticmethod
519+
def _make_exec_env_list() -> list[str]:
520+
result = list[str]()
521+
for envvar in os.environ.items():
522+
if not __class__._does_put_envvar_into_exec_cmd(envvar[0]):
523+
continue
524+
qvalue = __class__._quote_envvar(envvar[1])
525+
assert type(qvalue) == str # noqa: E721
526+
result.append(envvar[0] + "=" + qvalue)
527+
continue
528+
529+
return result
530+
531+
sm_envs_for_exec_cmd = ["LANG", "LANGUAGE"]
532+
533+
@staticmethod
534+
def _does_put_envvar_into_exec_cmd(name: str) -> bool:
535+
assert type(name) == str # noqa: E721
536+
name = name.upper()
537+
if name.startswith("LC_"):
538+
return True
539+
if name in __class__.sm_envs_for_exec_cmd:
540+
return True
541+
return False
542+
543+
@staticmethod
544+
def _quote_envvar(value: str) -> str:
545+
assert type(value) == str # noqa: E721
546+
result = "\""
547+
for ch in value:
548+
if ch == "\"":
549+
result += "\\\""
550+
elif ch == "\\":
551+
result += "\\\\"
552+
else:
553+
result += ch
554+
result += "\""
555+
return result
556+
513557

514558
def normalize_error(error):
515559
if isinstance(error, bytes):

tests/test_simple_remote.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,79 @@ def test_custom_init(self):
119119
# there should be no trust entries at all
120120
self.assertFalse(any('trust' in s for s in lines))
121121

122+
def test_init__LANG_С(self):
123+
# PBCKP-1744
124+
prev_LANG = os.environ.get("LANG")
125+
126+
try:
127+
os.environ["LANG"] = "C"
128+
129+
with get_remote_node(conn_params=conn_params) as node:
130+
node.init().start()
131+
finally:
132+
__class__.helper__restore_envvar("LANG", prev_LANG)
133+
134+
def test_init__unk_LANG_and_LC_CTYPE(self):
135+
# PBCKP-1744
136+
prev_LANG = os.environ.get("LANG")
137+
prev_LANGUAGE = os.environ.get("LANGUAGE")
138+
prev_LC_CTYPE = os.environ.get("LC_CTYPE")
139+
prev_LC_COLLATE = os.environ.get("LC_COLLATE")
140+
141+
try:
142+
# TODO: Pass unkData through test parameter.
143+
unkDatas = [
144+
("UNKNOWN_LANG", "UNKNOWN_CTYPE"),
145+
("\"UNKNOWN_LANG\"", "\"UNKNOWN_CTYPE\""),
146+
("\\UNKNOWN_LANG\\", "\\UNKNOWN_CTYPE\\"),
147+
("\"UNKNOWN_LANG", "UNKNOWN_CTYPE\""),
148+
("\\UNKNOWN_LANG", "UNKNOWN_CTYPE\\"),
149+
("\\", "\\"),
150+
("\"", "\""),
151+
]
152+
153+
for unkData in unkDatas:
154+
logging.info("----------------------")
155+
logging.info("Unk LANG is [{0}]".format(unkData[0]))
156+
logging.info("Unk LC_CTYPE is [{0}]".format(unkData[1]))
157+
158+
os.environ["LANG"] = unkData[0]
159+
os.environ.pop("LANGUAGE", None)
160+
os.environ["LC_CTYPE"] = unkData[1]
161+
os.environ.pop("LC_COLLATE", None)
162+
163+
assert os.environ.get("LANG") == unkData[0]
164+
assert not ("LANGUAGE" in os.environ.keys())
165+
assert os.environ.get("LC_CTYPE") == unkData[1]
166+
assert not ("LC_COLLATE" in os.environ.keys())
167+
168+
while True:
169+
try:
170+
with get_remote_node(conn_params=conn_params):
171+
pass
172+
except testgres.exceptions.ExecUtilException as e:
173+
#
174+
# Example of an error message:
175+
#
176+
# warning: setlocale: LC_CTYPE: cannot change locale (UNKNOWN_CTYPE): No such file or directory
177+
# postgres (PostgreSQL) 14.12
178+
#
179+
errMsg = str(e)
180+
181+
logging.info("Error message is: {0}".format(errMsg))
182+
183+
assert "LC_CTYPE" in errMsg
184+
assert unkData[1] in errMsg
185+
assert "warning: setlocale: LC_CTYPE: cannot change locale (" + unkData[1] + "): No such file or directory" in errMsg
186+
assert "postgres" in errMsg
187+
break
188+
raise Exception("We expected an error!")
189+
finally:
190+
__class__.helper__restore_envvar("LANG", prev_LANG)
191+
__class__.helper__restore_envvar("LANGUAGE", prev_LANGUAGE)
192+
__class__.helper__restore_envvar("LC_CTYPE", prev_LC_CTYPE)
193+
__class__.helper__restore_envvar("LC_COLLATE", prev_LC_COLLATE)
194+
122195
def test_double_init(self):
123196
with get_remote_node(conn_params=conn_params).init() as node:
124197
# can't initialize node more than once
@@ -994,6 +1067,12 @@ def test_child_process_dies(self):
9941067
# try to handle children list -- missing processes will have ptype "ProcessType.Unknown"
9951068
[ProcessProxy(p) for p in children]
9961069

1070+
def helper__restore_envvar(name, prev_value):
1071+
if prev_value is None:
1072+
os.environ.pop(name, None)
1073+
else:
1074+
os.environ[name] = prev_value
1075+
9971076

9981077
if __name__ == '__main__':
9991078
if os_ops.environ('ALT_CONFIG'):

0 commit comments

Comments
 (0)