mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
291 lines
9.3 KiB
Python
291 lines
9.3 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
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 python libs
|
|
from __future__ import absolute_import, print_function, unicode_literals
|
|
|
|
import base64
|
|
import errno
|
|
import logging
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
# Import salt libs
|
|
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
|
|
|
|
# Import 3rd party libs
|
|
from salt.ext import six
|
|
|
|
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()
|
|
|
|
# Setup file logging!
|
|
self.setup_logfile_logger()
|
|
salt.utils.verify.verify_log(self.config)
|
|
|
|
cp_ = SaltCP(self.config)
|
|
cp_.run()
|
|
|
|
|
|
class SaltCP(object):
|
|
"""
|
|
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("{0} does not exist\n".format(path))
|
|
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 = "The referenced file, {0} is not available.".format(fn_)
|
|
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(
|
|
fn_ + " is a directory, only files are supported "
|
|
'in non-chunked mode. Use "--chunked" command '
|
|
"line argument."
|
|
)
|
|
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"]]
|
|
local = salt.client.get_local_client(self.opts["conf_file"])
|
|
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)
|
|
|
|
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"]
|
|
|
|
local = salt.client.get_local_client(self.opts["conf_file"])
|
|
|
|
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
|
|
for fn_, mode in six.iteritems(files):
|
|
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_,
|
|
"{0} ".format(selected_target_option)
|
|
if selected_target_option
|
|
else "",
|
|
tgt,
|
|
remote_path,
|
|
" (chunk #{0})".format(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.{0} It may be necessary to "
|
|
"decrease salt_cp_chunk_size (current value: "
|
|
"{1})".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 six.iteritems(result):
|
|
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 six.iteritems(failed):
|
|
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,
|
|
"{0} ".format(
|
|
selected_target_option
|
|
) # pylint: disable=str-format-in-logging
|
|
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 six.iteritems(local.cmd(*args)):
|
|
ret.setdefault(minion_id, {})[remote_path] = minion_ret
|
|
|
|
return ret
|