Mini Shell
"""
The cp module is used to execute the logic used by the salt-cp command
line application, salt-cp is NOT intended to broadcast large files, it is
intended to handle text files.
Salt-cp can be used to distribute configuration files
"""
import base64
import errno
import logging
import os
import re
import sys
import salt.client
import salt.output
import salt.utils.files
import salt.utils.gzip_util
import salt.utils.itertools
import salt.utils.minions
import salt.utils.parsers
import salt.utils.platform
import salt.utils.stringutils
import salt.utils.verify
log = logging.getLogger(__name__)
class SaltCPCli(salt.utils.parsers.SaltCPOptionParser):
"""
Run the salt-cp command line client
"""
def run(self):
"""
Execute salt-cp
"""
self.parse_args()
cp_ = SaltCP(self.config)
cp_.run()
class SaltCP:
"""
Create a salt cp object, used to distribute simple files with salt
"""
def __init__(self, opts):
self.opts = opts
self.is_windows = salt.utils.platform.is_windows()
def _mode(self, path):
if self.is_windows:
return None
try:
return int(oct(os.stat(path).st_mode)[-4:], 8)
except (TypeError, IndexError, ValueError):
return None
def _recurse(self, path):
"""
Get a list of all specified files
"""
files = {}
empty_dirs = []
try:
sub_paths = os.listdir(path)
except OSError as exc:
if exc.errno == errno.ENOENT:
# Path does not exist
sys.stderr.write(f"{path} does not exist\n")
sys.exit(42)
elif exc.errno in (errno.EINVAL, errno.ENOTDIR):
# Path is a file (EINVAL on Windows, ENOTDIR otherwise)
files[path] = self._mode(path)
else:
if not sub_paths:
empty_dirs.append(path)
for fn_ in sub_paths:
files_, empty_dirs_ = self._recurse(os.path.join(path, fn_))
files.update(files_)
empty_dirs.extend(empty_dirs_)
return files, empty_dirs
def _list_files(self):
files = {}
empty_dirs = set()
for fn_ in self.opts["src"]:
files_, empty_dirs_ = self._recurse(fn_)
files.update(files_)
empty_dirs.update(empty_dirs_)
return files, sorted(empty_dirs)
def _file_dict(self, fn_):
"""
Take a path and return the contents of the file as a string
"""
if not os.path.isfile(fn_):
err = f"The referenced file, {fn_} is not available."
sys.stderr.write(err + "\n")
sys.exit(42)
with salt.utils.files.fopen(fn_, "r") as fp_:
data = fp_.read()
return {fn_: data}
def _load_files(self):
"""
Parse the files indicated in opts['src'] and load them into a python
object for transport
"""
files = {}
for fn_ in self.opts["src"]:
if os.path.isfile(fn_):
files.update(self._file_dict(fn_))
elif os.path.isdir(fn_):
salt.utils.stringutils.print_cli(
"{} is a directory, only files are supported "
'in non-chunked mode. Use "--chunked" command '
"line argument.".format(fn_)
)
sys.exit(1)
return files
def run(self):
"""
Make the salt client call
"""
if self.opts["chunked"]:
ret = self.run_chunked()
else:
ret = self.run_oldstyle()
salt.output.display_output(ret, self.opts.get("output", "nested"), self.opts)
def run_oldstyle(self):
"""
Make the salt client call in old-style all-in-one call method
"""
arg = [self._load_files(), self.opts["dest"]]
args = [
self.opts["tgt"],
"cp.recv",
arg,
self.opts["timeout"],
]
selected_target_option = self.opts.get("selected_target_option", None)
if selected_target_option is not None:
args.append(selected_target_option)
with salt.client.get_local_client(self.opts["conf_file"]) as local:
return local.cmd(*args)
def run_chunked(self):
"""
Make the salt client call in the new fasion chunked multi-call way
"""
files, empty_dirs = self._list_files()
dest = self.opts["dest"]
gzip = self.opts["gzip"]
tgt = self.opts["tgt"]
timeout = self.opts["timeout"]
selected_target_option = self.opts.get("selected_target_option")
dest_is_dir = (
bool(empty_dirs) or len(files) > 1 or bool(re.search(r"[\\/]$", dest))
)
reader = (
salt.utils.gzip_util.compress_file
if gzip
else salt.utils.itertools.read_file
)
_res = salt.utils.minions.CkMinions(self.opts).check_minions(
tgt, tgt_type=selected_target_option or "glob"
)
minions = _res["minions"]
def _get_remote_path(fn_):
if fn_ in self.opts["src"]:
# This was a filename explicitly passed on the CLI
return (
os.path.join(dest, os.path.basename(fn_)) if dest_is_dir else dest
)
else:
for path in self.opts["src"]:
relpath = os.path.relpath(fn_, path + os.sep)
if relpath.startswith(parent):
# File is not within this dir
continue
return os.path.join(dest, os.path.basename(path), relpath)
else: # pylint: disable=useless-else-on-loop
# Should not happen
log.error("Failed to find remote path for %s", fn_)
return None
ret = {}
parent = ".." + os.sep
with salt.client.get_local_client(self.opts["conf_file"]) as local:
for fn_, mode in files.items():
remote_path = _get_remote_path(fn_)
index = 1
failed = {}
for chunk in reader(fn_, chunk_size=self.opts["salt_cp_chunk_size"]):
chunk = base64.b64encode(salt.utils.stringutils.to_bytes(chunk))
append = index > 1
log.debug(
"Copying %s to %starget '%s' as %s%s",
fn_,
f"{selected_target_option} " if selected_target_option else "",
tgt,
remote_path,
f" (chunk #{index})" if append else "",
)
args = [
tgt,
"cp.recv_chunked",
[remote_path, chunk, append, gzip, mode],
timeout,
]
if selected_target_option is not None:
args.append(selected_target_option)
result = local.cmd(*args)
if not result:
# Publish failed
msg = (
"Publish failed.{} It may be necessary to "
"decrease salt_cp_chunk_size (current value: "
"{})".format(
" File partially transferred." if index > 1 else "",
self.opts["salt_cp_chunk_size"],
)
)
for minion in minions:
ret.setdefault(minion, {})[remote_path] = msg
break
for minion_id, minion_ret in result.items():
ret.setdefault(minion_id, {})[remote_path] = minion_ret
# Catch first error message for a given minion, we will
# rewrite the results after we're done iterating through
# the chunks.
if minion_ret is not True and minion_id not in failed:
failed[minion_id] = minion_ret
index += 1
for minion_id, msg in failed.items():
ret[minion_id][remote_path] = msg
for dirname in empty_dirs:
remote_path = _get_remote_path(dirname)
log.debug(
"Creating empty dir %s on %starget '%s'",
dirname,
f"{selected_target_option} " if selected_target_option else "",
tgt,
)
args = [tgt, "cp.recv_chunked", [remote_path, None], timeout]
if selected_target_option is not None:
args.append(selected_target_option)
for minion_id, minion_ret in local.cmd(*args).items():
ret.setdefault(minion_id, {})[remote_path] = minion_ret
return ret
Zerion Mini Shell 1.0