Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[collect] Handle custom node and inherited configs with collections #3852

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions sos/collector/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class SoSCollector(SoSComponent):
'encrypt_pass': '',
'group': None,
'image': '',
'inherit_config_file': None,
'force_pull_image': True,
'skip_cleaning_files': [],
'jobs': 4,
Expand All @@ -102,6 +103,7 @@ class SoSCollector(SoSComponent):
'map_file': '/etc/sos/cleaner/default_mapping',
'primary': '',
'namespaces': None,
'node_config_file': None,
'nodes': [],
'no_env_vars': False,
'no_local': False,
Expand Down Expand Up @@ -463,6 +465,15 @@ def add_parser_options(cls, parser):
choices=['auto', 'https', 'ftp', 'sftp',
's3'],
help="Manually specify the upload protocol")
collect_grp.add_argument('--inherit-config-file', type=str,
default=None,
help='Path to a local config file to'
' copy to the remote node and use with'
' sos report')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial thought was that this would be a toggle, whereby if True we implicitly take the config-file used locally, so sos.conf by default or whatever gets set.

Is there a use case you have where a user would have config_file_A locally that is used for the collect and local report collections, but would have config_file_B also local to the system that is running collect but should be used only for remote node collections?

Copy link
Member Author

@TrevorBenson TrevorBenson Nov 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect any difference is mostly my misinterpretation of the discussion

  • A new --inherit-config-file (or whatever) option causes collect to transfer the supplied config file to remote nodes and implicitly sets the per-node sos command to use that transferred config file

Not intentional change to your thoughts on implementation.

Is there a use case you have where [...]

Possibly, but not one of my requirements. I plan to use the same config for the collector and the local/remote nodes.


I'll attempt to layout the resulting behavior from this change.

Overview

Three arguments

  1. --config-file: str
  2. --node-config-file: str
  3. --inherit-config-file: bool

Logic

  • --inherit-config-file is False
    • --config-file & --node-config-file unset
      • Current behavior for both collector and report.
    • --config-file set to/etc/sos/custom1.conf
      • collector uses custom1.conf
      • report uses sos.conf
    • --node-config set to /etc/sos/custom2.conf
      • collector uses default /etc/sos/sos.conf
      • report uses (pre-existing) /etc/sos/custom2.conf for the local and remote nodes .
  • --inherit-config-file is True
    • --config-file & --node-config-file unset
      • collector uses /etc/sos/sos.conf
      • /etc/sos/sos.conf is copied to all nodes in a temp path.
      • report uses /tmp/sos.XXXXX.conf (local could use default path)
    • --config-file set to/etc/sos/custom3.conf
      • collector uses /etc/sos/custom3.conf
      • /etc/sos/custom3.conf is copied to all nodes in a temp path.
      • report uses /tmp/sos.XXXXX.conf (local ...)

--node-config-file & --inherit-config-file are an incompatible argument pair. Potentially also --config-file & --node-config-file.

Does this correctly describe what you were thinking?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've got this logic and additional comments worked out. I will add the new commit for review, but can be dropped/reverted if this wasn't what you were thinking of.

collect_grp.add_argument('--node-config-file', type=str,
default=None,
help='Path to a config file on the'
' remote node to use with sos report')

# Group the cleaner options together
cleaner_grp = parser.add_argument_group(
Expand Down
25 changes: 25 additions & 0 deletions sos/collector/sosnode.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def __init__(self, address, commons, password=None, local_sudo=None,
self.hostlen = commons['hostlen']
self.need_sudo = commons['need_sudo']
self.sos_options = commons['sos_options']
self.node_config_file = self.opts.node_config_file
self.inherit_config_file = self.opts.inherit_config_file
self.local = False
self.host = None
self.cluster = None
Expand Down Expand Up @@ -762,12 +764,35 @@ def execute_sos_command(self):
try:
path = False
checksum = False
config_file_arg = ''
if self.opts.node_config_file:
config_file_arg = f'--config-file={self.opts.node_config_file}'
elif self.opts.inherit_config_file:
if not self.local:
remote_config = os.path.join(self.tmpdir,
'remote_sos.conf')
self.run_command(f"mkdir -pv {self.tmpdir}",
timeout=10)
self._transport.copy_file_to_remote(
self.opts.inherit_config_file,
remote_config)
TrevorBenson marked this conversation as resolved.
Show resolved Hide resolved
config_file_arg = f'--config-file={remote_config}'
else:
config_file_arg = (
f'--config-file={self.opts.inherit_config_file}')
self.sos_cmd = (
f"{self.sos_cmd} {config_file_arg}"
if config_file_arg else self.sos_cmd)
Comment on lines +780 to +782
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it easier just to have

if config_file_arg:
    self.sos_cmd = f"{self.sos_cmd} {config_file_arg}"

? But I am ok with the current code.

res = self.run_command(self.sos_cmd,
timeout=self.opts.timeout,
use_shell=True,
need_root=True,
use_container=True,
env=self.sos_env_vars)
if self.opts.inherit_config_file and not self.local:
self.run_command(f"rm {remote_config} ; rmdir {self.tmpdir}",
use_shell=True,
need_root=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, same here. Very much not a fan of doing rms like this - the local sos installation on the remote node should take care of any cleanup. If we place the inherited config files in /tmp, then they would be both out of the way and cleaned up separately by long-standing behavior.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll presume you mean leaving the inherited conf in the tmp directory and letting systemd-tmpfiles-clean.timer RHEL, or tmpfiles.d for Ubuntu, handle the cleanup on schedule?

It works for my use case, as long as we are comfortable that the file remains might (although probably uncommon) store plugin_options in the file for things like mysql.dbpass, postgresql.password, skydive.password?

if res['status'] == 0:
for line in res['output'].splitlines():
if fnmatch.fnmatch(line, '*sosreport-*tar*'):
Expand Down
31 changes: 31 additions & 0 deletions sos/collector/transports/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,37 @@ def _get_hostname(self):
self.log_info(f"Hostname set to {self._hostname}")
return self._hostname

def copy_file_to_remote(self, fname, dest):
"""Copy a local file, fname, to dest on the remote node

:param fname: The name of the file to copy
:type fname: ``str``

:param dest: Where to save the file to remotely
:type dest: ``str``

:returns: True if file was successfully copied to remote, or False
:rtype: ``bool``
"""
attempts = 0
try:
while attempts < 5:
attempts += 1
ret = self._copy_file_to_remote(fname, dest)
if ret:
return True
self.log_info(f"File copy attempt {attempts} failed")
self.log_info("File copy failed after 5 attempts")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while attempts < 3 vs. failed after 5 attempts :)

return False
except Exception as err:
self.log_error("Exception encountered during config copy attempt "
f"{attempts} for {fname}: {err}")
raise err

def _copy_file_to_remote(self, fname, dest):
TrevorBenson marked this conversation as resolved.
Show resolved Hide resolved
raise NotImplementedError(
f"Transport {self.name} does not support file copying")

def retrieve_file(self, fname, dest):
"""Copy a remote file, fname, to dest on the local node

Expand Down
6 changes: 6 additions & 0 deletions sos/collector/transports/control_persist.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,12 @@ def remote_exec(self):
f"{self.opts.ssh_user}@{self.address}")
return self.ssh_cmd

def _copy_file_to_remote(self, fname, dest):
cmd = (f"/usr/bin/scp -oControlPath={self.control_path} "
f"{fname} {self.opts.ssh_user}@{self.address}:{dest}")
res = sos_get_command_output(cmd)
TrevorBenson marked this conversation as resolved.
Show resolved Hide resolved
return res['status'] == 0

def _retrieve_file(self, fname, dest):
cmd = (f"/usr/bin/scp -oControlPath={self.control_path} "
f"{self.opts.ssh_user}@{self.address}:{fname} {dest}")
Expand Down
7 changes: 7 additions & 0 deletions sos/collector/transports/juju.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ def remote_exec(self):
option = f"{model_option} {target_option}"
return f"juju ssh {option}"

def _copy_file_to_remote(self, fname, dest):
model, unit = self.address.split(":")
model_option = f"-m {model}" if model else ""
cmd = f"juju scp {model_option} -- {fname} {unit}:{dest}"
res = sos_get_command_output(cmd)
return res["status"] == 0

def _retrieve_file(self, fname, dest):
self._chmod(fname) # juju scp needs the archive to be world-readable
model, unit = self.address.split(":")
Expand Down
3 changes: 3 additions & 0 deletions sos/collector/transports/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ def _retrieve_file(self, fname, dest):
def _format_cmd_for_exec(self, cmd):
return cmd

def _copy_file_to_remote(self, fname, dest):
pass
Copy link
Member

@TurboTurtle TurboTurtle Nov 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I was initially going to say this should return True, but that might not track given that we may not be writing the the same location (and in this first implementation we already aren't).

This might need some attention later on depending on how much use this functionality sees, but for now I think it's fine.


def _read_file(self, fname):
if os.path.exists(fname):
with open(fname, 'r', encoding='utf-8') as rfile:
Expand Down
6 changes: 6 additions & 0 deletions sos/collector/transports/oc.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,12 @@ def remote_exec(self):
return (f"oc -n {self.project} exec --request-timeout=0 "
f"{self.pod_name} -- /bin/bash -c")

def _copy_file_to_remote(self, fname, dest):
result = self.run_oc("cp --retries", stderr=True)
flags = '' if "unknown flag" in result["output"] else '--retries=5'
cmd = self.run_oc(f"cp {flags} {fname} {self.pod_name}:{dest}")
return cmd['status'] == 0

def _retrieve_file(self, fname, dest):
# check if --retries flag is available for given version of oc
result = self.run_oc("cp --retries", stderr=True)
Expand Down
28 changes: 26 additions & 2 deletions sos/collector/transports/saltstack.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ def run_command(self, cmd, timeout=180, need_root=False, env=None,
ret['output'] = self._convert_output_json(ret['output'])
return ret

def _salt_copy_file(self, node, fname, dest):
"""
Execute cp.get_file on the remote host using SaltStack Master
"""
cmd = f"salt-cp {node} {fname} {dest}"
res = sos_get_command_output(cmd)
return res['status'] == 0

def _salt_retrieve_file(self, node, fname, dest):
"""
Execute cp.push on the remote host using SaltStack Master
Expand Down Expand Up @@ -119,12 +127,28 @@ def remote_exec(self):
salt_args = "--out json --static --no-color"
return f"salt {salt_args} {self.address} cmd.shell "

def _copy_file_to_remote(self, fname, dest):
"""Copy a file to the remote host using SaltStack Master

Parameters
fname The path to the file on the master
dest The path to the destination directory on the remote host

Returns
True if the file was copied, else False
"""
return (
self._salt_copy_file(self.address, fname, dest)
if self.connected
else False
)

def _retrieve_file(self, fname, dest):
"""Retrieve a file from the remote host using saltstack

Parameters
fname The path to the file on the remote host
dest The path to the destination directory on the master
fname The path to the file on the remote host
dest The path to the destination directory on the master

Returns
True if the file was retrieved, else False
Expand Down
Loading