fileclient.py 52 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430
  1. # -*- coding: utf-8 -*-
  2. '''
  3. Classes that manage file clients
  4. '''
  5. from __future__ import absolute_import, print_function, unicode_literals
  6. # Import python libs
  7. import contextlib
  8. import errno
  9. import logging
  10. import os
  11. import string
  12. import shutil
  13. import ftplib
  14. from tornado.httputil import parse_response_start_line, HTTPHeaders, HTTPInputError
  15. import salt.utils.atomicfile
  16. # Import salt libs
  17. from salt.exceptions import (
  18. CommandExecutionError, MinionError
  19. )
  20. import salt.client
  21. import salt.loader
  22. import salt.payload
  23. import salt.transport.client
  24. import salt.fileserver
  25. import salt.utils.data
  26. import salt.utils.files
  27. import salt.utils.gzip_util
  28. import salt.utils.hashutils
  29. import salt.utils.http
  30. import salt.utils.path
  31. import salt.utils.platform
  32. import salt.utils.stringutils
  33. import salt.utils.templates
  34. import salt.utils.url
  35. import salt.utils.versions
  36. from salt.utils.openstack.swift import SaltSwift
  37. # pylint: disable=no-name-in-module,import-error
  38. from salt.ext import six
  39. import salt.ext.six.moves.BaseHTTPServer as BaseHTTPServer
  40. from salt.ext.six.moves.urllib.error import HTTPError, URLError
  41. from salt.ext.six.moves.urllib.parse import urlparse, urlunparse
  42. # pylint: enable=no-name-in-module,import-error
  43. log = logging.getLogger(__name__)
  44. MAX_FILENAME_LENGTH = 255
  45. def get_file_client(opts, pillar=False):
  46. '''
  47. Read in the ``file_client`` option and return the correct type of file
  48. server
  49. '''
  50. client = opts.get('file_client', 'remote')
  51. if pillar and client == 'local':
  52. client = 'pillar'
  53. return {
  54. 'remote': RemoteClient,
  55. 'local': FSClient,
  56. 'pillar': PillarClient,
  57. }.get(client, RemoteClient)(opts)
  58. def decode_dict_keys_to_str(src):
  59. '''
  60. Convert top level keys from bytes to strings if possible.
  61. This is necessary because Python 3 makes a distinction
  62. between these types.
  63. '''
  64. if not six.PY3 or not isinstance(src, dict):
  65. return src
  66. output = {}
  67. for key, val in six.iteritems(src):
  68. if isinstance(key, bytes):
  69. try:
  70. key = key.decode()
  71. except UnicodeError:
  72. pass
  73. output[key] = val
  74. return output
  75. class Client(object):
  76. '''
  77. Base class for Salt file interactions
  78. '''
  79. def __init__(self, opts):
  80. self.opts = opts
  81. self.utils = salt.loader.utils(self.opts)
  82. self.serial = salt.payload.Serial(self.opts)
  83. # Add __setstate__ and __getstate__ so that the object may be
  84. # deep copied. It normally can't be deep copied because its
  85. # constructor requires an 'opts' parameter.
  86. # The TCP transport needs to be able to deep copy this class
  87. # due to 'salt.utils.context.ContextDict.clone'.
  88. def __setstate__(self, state):
  89. # This will polymorphically call __init__
  90. # in the derived class.
  91. self.__init__(state['opts'])
  92. def __getstate__(self):
  93. return {'opts': self.opts}
  94. def _check_proto(self, path):
  95. '''
  96. Make sure that this path is intended for the salt master and trim it
  97. '''
  98. if not path.startswith('salt://'):
  99. raise MinionError('Unsupported path: {0}'.format(path))
  100. file_path, saltenv = salt.utils.url.parse(path)
  101. return file_path
  102. def _file_local_list(self, dest):
  103. '''
  104. Helper util to return a list of files in a directory
  105. '''
  106. if os.path.isdir(dest):
  107. destdir = dest
  108. else:
  109. destdir = os.path.dirname(dest)
  110. filelist = set()
  111. for root, dirs, files in salt.utils.path.os_walk(destdir, followlinks=True):
  112. for name in files:
  113. path = os.path.join(root, name)
  114. filelist.add(path)
  115. return filelist
  116. @contextlib.contextmanager
  117. def _cache_loc(self, path, saltenv='base', cachedir=None):
  118. '''
  119. Return the local location to cache the file, cache dirs will be made
  120. '''
  121. cachedir = self.get_cachedir(cachedir)
  122. dest = salt.utils.path.join(cachedir,
  123. 'files',
  124. saltenv,
  125. path)
  126. destdir = os.path.dirname(dest)
  127. with salt.utils.files.set_umask(0o077):
  128. # remove destdir if it is a regular file to avoid an OSError when
  129. # running os.makedirs below
  130. if os.path.isfile(destdir):
  131. os.remove(destdir)
  132. # ensure destdir exists
  133. try:
  134. os.makedirs(destdir)
  135. except OSError as exc:
  136. if exc.errno != errno.EEXIST: # ignore if it was there already
  137. raise
  138. yield dest
  139. def get_cachedir(self, cachedir=None):
  140. if cachedir is None:
  141. cachedir = self.opts['cachedir']
  142. elif not os.path.isabs(cachedir):
  143. cachedir = os.path.join(self.opts['cachedir'], cachedir)
  144. return cachedir
  145. def get_file(self,
  146. path,
  147. dest='',
  148. makedirs=False,
  149. saltenv='base',
  150. gzip=None,
  151. cachedir=None):
  152. '''
  153. Copies a file from the local files or master depending on
  154. implementation
  155. '''
  156. raise NotImplementedError
  157. def file_list_emptydirs(self, saltenv='base', prefix=''):
  158. '''
  159. List the empty dirs
  160. '''
  161. raise NotImplementedError
  162. def cache_file(self, path, saltenv='base', cachedir=None, source_hash=None):
  163. '''
  164. Pull a file down from the file server and store it in the minion
  165. file cache
  166. '''
  167. return self.get_url(
  168. path, '', True, saltenv, cachedir=cachedir, source_hash=source_hash)
  169. def cache_files(self, paths, saltenv='base', cachedir=None):
  170. '''
  171. Download a list of files stored on the master and put them in the
  172. minion file cache
  173. '''
  174. ret = []
  175. if isinstance(paths, six.string_types):
  176. paths = paths.split(',')
  177. for path in paths:
  178. ret.append(self.cache_file(path, saltenv, cachedir=cachedir))
  179. return ret
  180. def cache_master(self, saltenv='base', cachedir=None):
  181. '''
  182. Download and cache all files on a master in a specified environment
  183. '''
  184. ret = []
  185. for path in self.file_list(saltenv):
  186. ret.append(
  187. self.cache_file(
  188. salt.utils.url.create(path), saltenv, cachedir=cachedir)
  189. )
  190. return ret
  191. def cache_dir(self, path, saltenv='base', include_empty=False,
  192. include_pat=None, exclude_pat=None, cachedir=None):
  193. '''
  194. Download all of the files in a subdir of the master
  195. '''
  196. ret = []
  197. path = self._check_proto(salt.utils.data.decode(path))
  198. # We want to make sure files start with this *directory*, use
  199. # '/' explicitly because the master (that's generating the
  200. # list of files) only runs on POSIX
  201. if not path.endswith('/'):
  202. path = path + '/'
  203. log.info(
  204. 'Caching directory \'%s\' for environment \'%s\'', path, saltenv
  205. )
  206. # go through the list of all files finding ones that are in
  207. # the target directory and caching them
  208. for fn_ in self.file_list(saltenv):
  209. fn_ = salt.utils.data.decode(fn_)
  210. if fn_.strip() and fn_.startswith(path):
  211. if salt.utils.stringutils.check_include_exclude(
  212. fn_, include_pat, exclude_pat):
  213. fn_ = self.cache_file(
  214. salt.utils.url.create(fn_), saltenv, cachedir=cachedir)
  215. if fn_:
  216. ret.append(fn_)
  217. if include_empty:
  218. # Break up the path into a list containing the bottom-level
  219. # directory (the one being recursively copied) and the directories
  220. # preceding it
  221. # separated = string.rsplit(path, '/', 1)
  222. # if len(separated) != 2:
  223. # # No slashes in path. (So all files in saltenv will be copied)
  224. # prefix = ''
  225. # else:
  226. # prefix = separated[0]
  227. cachedir = self.get_cachedir(cachedir)
  228. dest = salt.utils.path.join(cachedir, 'files', saltenv)
  229. for fn_ in self.file_list_emptydirs(saltenv):
  230. fn_ = salt.utils.data.decode(fn_)
  231. if fn_.startswith(path):
  232. minion_dir = '{0}/{1}'.format(dest, fn_)
  233. if not os.path.isdir(minion_dir):
  234. os.makedirs(minion_dir)
  235. ret.append(minion_dir)
  236. return ret
  237. def cache_local_file(self, path, **kwargs):
  238. '''
  239. Cache a local file on the minion in the localfiles cache
  240. '''
  241. dest = os.path.join(self.opts['cachedir'], 'localfiles',
  242. path.lstrip('/'))
  243. destdir = os.path.dirname(dest)
  244. if not os.path.isdir(destdir):
  245. os.makedirs(destdir)
  246. shutil.copyfile(path, dest)
  247. return dest
  248. def file_local_list(self, saltenv='base'):
  249. '''
  250. List files in the local minion files and localfiles caches
  251. '''
  252. filesdest = os.path.join(self.opts['cachedir'], 'files', saltenv)
  253. localfilesdest = os.path.join(self.opts['cachedir'], 'localfiles')
  254. fdest = self._file_local_list(filesdest)
  255. ldest = self._file_local_list(localfilesdest)
  256. return sorted(fdest.union(ldest))
  257. def file_list(self, saltenv='base', prefix=''):
  258. '''
  259. This function must be overwritten
  260. '''
  261. return []
  262. def dir_list(self, saltenv='base', prefix=''):
  263. '''
  264. This function must be overwritten
  265. '''
  266. return []
  267. def symlink_list(self, saltenv='base', prefix=''):
  268. '''
  269. This function must be overwritten
  270. '''
  271. return {}
  272. def is_cached(self, path, saltenv='base', cachedir=None):
  273. '''
  274. Returns the full path to a file if it is cached locally on the minion
  275. otherwise returns a blank string
  276. '''
  277. if path.startswith('salt://'):
  278. path, senv = salt.utils.url.parse(path)
  279. if senv:
  280. saltenv = senv
  281. escaped = True if salt.utils.url.is_escaped(path) else False
  282. # also strip escape character '|'
  283. localsfilesdest = os.path.join(
  284. self.opts['cachedir'], 'localfiles', path.lstrip('|/'))
  285. filesdest = os.path.join(
  286. self.opts['cachedir'], 'files', saltenv, path.lstrip('|/'))
  287. extrndest = self._extrn_path(path, saltenv, cachedir=cachedir)
  288. if os.path.exists(filesdest):
  289. return salt.utils.url.escape(filesdest) if escaped else filesdest
  290. elif os.path.exists(localsfilesdest):
  291. return salt.utils.url.escape(localsfilesdest) \
  292. if escaped \
  293. else localsfilesdest
  294. elif os.path.exists(extrndest):
  295. return extrndest
  296. return ''
  297. def cache_dest(self, url, saltenv='base', cachedir=None):
  298. '''
  299. Return the expected cache location for the specified URL and
  300. environment.
  301. '''
  302. proto = urlparse(url).scheme
  303. if proto == '':
  304. # Local file path
  305. return url
  306. if proto == 'salt':
  307. url, senv = salt.utils.url.parse(url)
  308. if senv:
  309. saltenv = senv
  310. return salt.utils.path.join(
  311. self.opts['cachedir'],
  312. 'files',
  313. saltenv,
  314. url.lstrip('|/'))
  315. return self._extrn_path(url, saltenv, cachedir=cachedir)
  316. def list_states(self, saltenv):
  317. '''
  318. Return a list of all available sls modules on the master for a given
  319. environment
  320. '''
  321. states = set()
  322. for path in self.file_list(saltenv):
  323. if salt.utils.platform.is_windows():
  324. path = path.replace('\\', '/')
  325. if path.endswith('.sls'):
  326. # is an sls module!
  327. if path.endswith('/init.sls'):
  328. states.add(path.replace('/', '.')[:-9])
  329. else:
  330. states.add(path.replace('/', '.')[:-4])
  331. return sorted(states)
  332. def get_state(self, sls, saltenv, cachedir=None):
  333. '''
  334. Get a state file from the master and store it in the local minion
  335. cache; return the location of the file
  336. '''
  337. if '.' in sls:
  338. sls = sls.replace('.', '/')
  339. sls_url = salt.utils.url.create(sls + '.sls')
  340. init_url = salt.utils.url.create(sls + '/init.sls')
  341. for path in [sls_url, init_url]:
  342. dest = self.cache_file(path, saltenv, cachedir=cachedir)
  343. if dest:
  344. return {'source': path, 'dest': dest}
  345. return {}
  346. def get_dir(self, path, dest='', saltenv='base', gzip=None,
  347. cachedir=None):
  348. '''
  349. Get a directory recursively from the salt-master
  350. '''
  351. ret = []
  352. # Strip trailing slash
  353. path = self._check_proto(path).rstrip('/')
  354. # Break up the path into a list containing the bottom-level directory
  355. # (the one being recursively copied) and the directories preceding it
  356. separated = path.rsplit('/', 1)
  357. if len(separated) != 2:
  358. # No slashes in path. (This means all files in saltenv will be
  359. # copied)
  360. prefix = ''
  361. else:
  362. prefix = separated[0]
  363. # Copy files from master
  364. for fn_ in self.file_list(saltenv, prefix=path):
  365. # Prevent files in "salt://foobar/" (or salt://foo.sh) from
  366. # matching a path of "salt://foo"
  367. try:
  368. if fn_[len(path)] != '/':
  369. continue
  370. except IndexError:
  371. continue
  372. # Remove the leading directories from path to derive
  373. # the relative path on the minion.
  374. minion_relpath = fn_[len(prefix):].lstrip('/')
  375. ret.append(
  376. self.get_file(
  377. salt.utils.url.create(fn_),
  378. '{0}/{1}'.format(dest, minion_relpath),
  379. True, saltenv, gzip
  380. )
  381. )
  382. # Replicate empty dirs from master
  383. try:
  384. for fn_ in self.file_list_emptydirs(saltenv, prefix=path):
  385. # Prevent an empty dir "salt://foobar/" from matching a path of
  386. # "salt://foo"
  387. try:
  388. if fn_[len(path)] != '/':
  389. continue
  390. except IndexError:
  391. continue
  392. # Remove the leading directories from path to derive
  393. # the relative path on the minion.
  394. minion_relpath = fn_[len(prefix):].lstrip('/')
  395. minion_mkdir = '{0}/{1}'.format(dest, minion_relpath)
  396. if not os.path.isdir(minion_mkdir):
  397. os.makedirs(minion_mkdir)
  398. ret.append(minion_mkdir)
  399. except TypeError:
  400. pass
  401. ret.sort()
  402. return ret
  403. def get_url(self, url, dest, makedirs=False, saltenv='base',
  404. no_cache=False, cachedir=None, source_hash=None):
  405. '''
  406. Get a single file from a URL.
  407. '''
  408. url_data = urlparse(url)
  409. url_scheme = url_data.scheme
  410. url_path = os.path.join(
  411. url_data.netloc, url_data.path).rstrip(os.sep)
  412. # If dest is a directory, rewrite dest with filename
  413. if dest is not None \
  414. and (os.path.isdir(dest) or dest.endswith(('/', '\\'))):
  415. if url_data.query or len(url_data.path) > 1 and not url_data.path.endswith('/'):
  416. strpath = url.split('/')[-1]
  417. else:
  418. strpath = 'index.html'
  419. if salt.utils.platform.is_windows():
  420. strpath = salt.utils.path.sanitize_win_path(strpath)
  421. dest = os.path.join(dest, strpath)
  422. if url_scheme and url_scheme.lower() in string.ascii_lowercase:
  423. url_path = ':'.join((url_scheme, url_path))
  424. url_scheme = 'file'
  425. if url_scheme in ('file', ''):
  426. # Local filesystem
  427. if not os.path.isabs(url_path):
  428. raise CommandExecutionError(
  429. 'Path \'{0}\' is not absolute'.format(url_path)
  430. )
  431. if dest is None:
  432. with salt.utils.files.fopen(url_path, 'rb') as fp_:
  433. data = fp_.read()
  434. return data
  435. return url_path
  436. if url_scheme == 'salt':
  437. result = self.get_file(url, dest, makedirs, saltenv, cachedir=cachedir)
  438. if result and dest is None:
  439. with salt.utils.files.fopen(result, 'rb') as fp_:
  440. data = fp_.read()
  441. return data
  442. return result
  443. if dest:
  444. destdir = os.path.dirname(dest)
  445. if not os.path.isdir(destdir):
  446. if makedirs:
  447. os.makedirs(destdir)
  448. else:
  449. return ''
  450. elif not no_cache:
  451. dest = self._extrn_path(url, saltenv, cachedir=cachedir)
  452. if source_hash is not None:
  453. try:
  454. source_hash = source_hash.split('=')[-1]
  455. form = salt.utils.files.HASHES_REVMAP[len(source_hash)]
  456. if salt.utils.hashutils.get_hash(dest, form) == source_hash:
  457. log.debug(
  458. 'Cached copy of %s (%s) matches source_hash %s, '
  459. 'skipping download', url, dest, source_hash
  460. )
  461. return dest
  462. except (AttributeError, KeyError, IOError, OSError):
  463. pass
  464. destdir = os.path.dirname(dest)
  465. if not os.path.isdir(destdir):
  466. os.makedirs(destdir)
  467. if url_data.scheme == 's3':
  468. try:
  469. def s3_opt(key, default=None):
  470. '''
  471. Get value of s3.<key> from Minion config or from Pillar
  472. '''
  473. if 's3.' + key in self.opts:
  474. return self.opts['s3.' + key]
  475. try:
  476. return self.opts['pillar']['s3'][key]
  477. except (KeyError, TypeError):
  478. return default
  479. self.utils['s3.query'](method='GET',
  480. bucket=url_data.netloc,
  481. path=url_data.path[1:],
  482. return_bin=False,
  483. local_file=dest,
  484. action=None,
  485. key=s3_opt('key'),
  486. keyid=s3_opt('keyid'),
  487. service_url=s3_opt('service_url'),
  488. verify_ssl=s3_opt('verify_ssl', True),
  489. location=s3_opt('location'),
  490. path_style=s3_opt('path_style', False),
  491. https_enable=s3_opt('https_enable', True))
  492. return dest
  493. except Exception as exc:
  494. raise MinionError(
  495. 'Could not fetch from {0}. Exception: {1}'.format(url, exc)
  496. )
  497. if url_data.scheme == 'ftp':
  498. try:
  499. ftp = ftplib.FTP()
  500. ftp.connect(url_data.hostname, url_data.port)
  501. ftp.login(url_data.username, url_data.password)
  502. remote_file_path = url_data.path.lstrip('/')
  503. with salt.utils.files.fopen(dest, 'wb') as fp_:
  504. ftp.retrbinary('RETR {0}'.format(remote_file_path), fp_.write)
  505. ftp.quit()
  506. return dest
  507. except Exception as exc:
  508. raise MinionError('Could not retrieve {0} from FTP server. Exception: {1}'.format(url, exc))
  509. if url_data.scheme == 'swift':
  510. try:
  511. def swift_opt(key, default):
  512. '''
  513. Get value of <key> from Minion config or from Pillar
  514. '''
  515. if key in self.opts:
  516. return self.opts[key]
  517. try:
  518. return self.opts['pillar'][key]
  519. except (KeyError, TypeError):
  520. return default
  521. swift_conn = SaltSwift(swift_opt('keystone.user', None),
  522. swift_opt('keystone.tenant', None),
  523. swift_opt('keystone.auth_url', None),
  524. swift_opt('keystone.password', None))
  525. swift_conn.get_object(url_data.netloc,
  526. url_data.path[1:],
  527. dest)
  528. return dest
  529. except Exception:
  530. raise MinionError('Could not fetch from {0}'.format(url))
  531. get_kwargs = {}
  532. if url_data.username is not None \
  533. and url_data.scheme in ('http', 'https'):
  534. netloc = url_data.netloc
  535. at_sign_pos = netloc.rfind('@')
  536. if at_sign_pos != -1:
  537. netloc = netloc[at_sign_pos + 1:]
  538. fixed_url = urlunparse(
  539. (url_data.scheme, netloc, url_data.path,
  540. url_data.params, url_data.query, url_data.fragment))
  541. get_kwargs['auth'] = (url_data.username, url_data.password)
  542. else:
  543. fixed_url = url
  544. destfp = None
  545. try:
  546. # Tornado calls streaming_callback on redirect response bodies.
  547. # But we need streaming to support fetching large files (> RAM
  548. # avail). Here we are working around this by disabling recording
  549. # the body for redirections. The issue is fixed in Tornado 4.3.0
  550. # so on_header callback could be removed when we'll deprecate
  551. # Tornado<4.3.0. See #27093 and #30431 for details.
  552. # Use list here to make it writable inside the on_header callback.
  553. # Simple bool doesn't work here: on_header creates a new local
  554. # variable instead. This could be avoided in Py3 with 'nonlocal'
  555. # statement. There is no Py2 alternative for this.
  556. #
  557. # write_body[0] is used by the on_chunk callback to tell it whether
  558. # or not we need to write the body of the request to disk. For
  559. # 30x redirects we set this to False because we don't want to
  560. # write the contents to disk, as we will need to wait until we
  561. # get to the redirected URL.
  562. #
  563. # write_body[1] will contain a tornado.httputil.HTTPHeaders
  564. # instance that we will use to parse each header line. We
  565. # initialize this to False, and after we parse the status line we
  566. # will replace it with the HTTPHeaders instance. If/when we have
  567. # found the encoding used in the request, we set this value to
  568. # False to signify that we are done parsing.
  569. #
  570. # write_body[2] is where the encoding will be stored
  571. write_body = [None, False, None]
  572. def on_header(hdr):
  573. if write_body[1] is not False and write_body[2] is None:
  574. if not hdr.strip() and 'Content-Type' not in write_body[1]:
  575. # If write_body[0] is True, then we are not following a
  576. # redirect (initial response was a 200 OK). So there is
  577. # no need to reset write_body[0].
  578. if write_body[0] is not True:
  579. # We are following a redirect, so we need to reset
  580. # write_body[0] so that we properly follow it.
  581. write_body[0] = None
  582. # We don't need the HTTPHeaders object anymore
  583. write_body[1] = False
  584. return
  585. # Try to find out what content type encoding is used if
  586. # this is a text file
  587. write_body[1].parse_line(hdr) # pylint: disable=no-member
  588. if 'Content-Type' in write_body[1]:
  589. content_type = write_body[1].get('Content-Type') # pylint: disable=no-member
  590. if not content_type.startswith('text'):
  591. write_body[1] = write_body[2] = False
  592. else:
  593. encoding = 'utf-8'
  594. fields = content_type.split(';')
  595. for field in fields:
  596. if 'encoding' in field:
  597. encoding = field.split('encoding=')[-1]
  598. write_body[2] = encoding
  599. # We have found our encoding. Stop processing headers.
  600. write_body[1] = False
  601. # If write_body[0] is False, this means that this
  602. # header is a 30x redirect, so we need to reset
  603. # write_body[0] to None so that we parse the HTTP
  604. # status code from the redirect target. Additionally,
  605. # we need to reset write_body[2] so that we inspect the
  606. # headers for the Content-Type of the URL we're
  607. # following.
  608. if write_body[0] is write_body[1] is False:
  609. write_body[0] = write_body[2] = None
  610. # Check the status line of the HTTP request
  611. if write_body[0] is None:
  612. try:
  613. hdr = parse_response_start_line(hdr)
  614. except HTTPInputError:
  615. # Not the first line, do nothing
  616. return
  617. write_body[0] = hdr.code not in [301, 302, 303, 307]
  618. write_body[1] = HTTPHeaders()
  619. if no_cache:
  620. result = []
  621. def on_chunk(chunk):
  622. if write_body[0]:
  623. if write_body[2]:
  624. chunk = chunk.decode(write_body[2])
  625. result.append(chunk)
  626. else:
  627. dest_tmp = u"{0}.part".format(dest)
  628. # We need an open filehandle to use in the on_chunk callback,
  629. # that's why we're not using a with clause here.
  630. destfp = salt.utils.files.fopen(dest_tmp, 'wb') # pylint: disable=resource-leakage
  631. def on_chunk(chunk):
  632. if write_body[0]:
  633. destfp.write(chunk)
  634. query = salt.utils.http.query(
  635. fixed_url,
  636. stream=True,
  637. streaming_callback=on_chunk,
  638. header_callback=on_header,
  639. username=url_data.username,
  640. password=url_data.password,
  641. opts=self.opts,
  642. **get_kwargs
  643. )
  644. if 'handle' not in query:
  645. raise MinionError('Error: {0} reading {1}'.format(query['error'], url))
  646. if no_cache:
  647. if write_body[2]:
  648. return ''.join(result)
  649. return b''.join(result)
  650. else:
  651. destfp.close()
  652. destfp = None
  653. salt.utils.files.rename(dest_tmp, dest)
  654. return dest
  655. except HTTPError as exc:
  656. raise MinionError('HTTP error {0} reading {1}: {3}'.format(
  657. exc.code,
  658. url,
  659. *BaseHTTPServer.BaseHTTPRequestHandler.responses[exc.code]))
  660. except URLError as exc:
  661. raise MinionError('Error reading {0}: {1}'.format(url, exc.reason))
  662. finally:
  663. if destfp is not None:
  664. destfp.close()
  665. def get_template(
  666. self,
  667. url,
  668. dest,
  669. template='jinja',
  670. makedirs=False,
  671. saltenv='base',
  672. cachedir=None,
  673. **kwargs):
  674. '''
  675. Cache a file then process it as a template
  676. '''
  677. if 'env' in kwargs:
  678. # "env" is not supported; Use "saltenv".
  679. kwargs.pop('env')
  680. kwargs['saltenv'] = saltenv
  681. url_data = urlparse(url)
  682. sfn = self.cache_file(url, saltenv, cachedir=cachedir)
  683. if not sfn or not os.path.exists(sfn):
  684. return ''
  685. if template in salt.utils.templates.TEMPLATE_REGISTRY:
  686. data = salt.utils.templates.TEMPLATE_REGISTRY[template](
  687. sfn,
  688. **kwargs
  689. )
  690. else:
  691. log.error(
  692. 'Attempted to render template with unavailable engine %s',
  693. template
  694. )
  695. return ''
  696. if not data['result']:
  697. # Failed to render the template
  698. log.error('Failed to render template with error: %s', data['data'])
  699. return ''
  700. if not dest:
  701. # No destination passed, set the dest as an extrn_files cache
  702. dest = self._extrn_path(url, saltenv, cachedir=cachedir)
  703. # If Salt generated the dest name, create any required dirs
  704. makedirs = True
  705. destdir = os.path.dirname(dest)
  706. if not os.path.isdir(destdir):
  707. if makedirs:
  708. os.makedirs(destdir)
  709. else:
  710. salt.utils.files.safe_rm(data['data'])
  711. return ''
  712. shutil.move(data['data'], dest)
  713. return dest
  714. def _extrn_path(self, url, saltenv, cachedir=None):
  715. '''
  716. Return the extrn_filepath for a given url
  717. '''
  718. url_data = urlparse(url)
  719. if salt.utils.platform.is_windows():
  720. netloc = salt.utils.path.sanitize_win_path(url_data.netloc)
  721. else:
  722. netloc = url_data.netloc
  723. # Strip user:pass from URLs
  724. netloc = netloc.split('@')[-1]
  725. if cachedir is None:
  726. cachedir = self.opts['cachedir']
  727. elif not os.path.isabs(cachedir):
  728. cachedir = os.path.join(self.opts['cachedir'], cachedir)
  729. if url_data.query:
  730. file_name = '-'.join([url_data.path, url_data.query])
  731. else:
  732. file_name = url_data.path
  733. if len(file_name) > MAX_FILENAME_LENGTH:
  734. file_name = salt.utils.hashutils.sha256_digest(file_name)
  735. return salt.utils.path.join(
  736. cachedir,
  737. 'extrn_files',
  738. saltenv,
  739. netloc,
  740. file_name
  741. )
  742. class PillarClient(Client):
  743. '''
  744. Used by pillar to handle fileclient requests
  745. '''
  746. def _find_file(self, path, saltenv='base'):
  747. '''
  748. Locate the file path
  749. '''
  750. fnd = {'path': '',
  751. 'rel': ''}
  752. if salt.utils.url.is_escaped(path):
  753. # The path arguments are escaped
  754. path = salt.utils.url.unescape(path)
  755. for root in self.opts['pillar_roots'].get(saltenv, []):
  756. full = os.path.join(root, path)
  757. if os.path.isfile(full):
  758. fnd['path'] = full
  759. fnd['rel'] = path
  760. return fnd
  761. return fnd
  762. def get_file(self,
  763. path,
  764. dest='',
  765. makedirs=False,
  766. saltenv='base',
  767. gzip=None,
  768. cachedir=None):
  769. '''
  770. Copies a file from the local files directory into :param:`dest`
  771. gzip compression settings are ignored for local files
  772. '''
  773. path = self._check_proto(path)
  774. fnd = self._find_file(path, saltenv)
  775. fnd_path = fnd.get('path')
  776. if not fnd_path:
  777. return ''
  778. return fnd_path
  779. def file_list(self, saltenv='base', prefix=''):
  780. '''
  781. Return a list of files in the given environment
  782. with optional relative prefix path to limit directory traversal
  783. '''
  784. ret = []
  785. prefix = prefix.strip('/')
  786. for path in self.opts['pillar_roots'].get(saltenv, []):
  787. for root, dirs, files in salt.utils.path.os_walk(
  788. os.path.join(path, prefix), followlinks=True
  789. ):
  790. # Don't walk any directories that match file_ignore_regex or glob
  791. dirs[:] = [d for d in dirs if not salt.fileserver.is_file_ignored(self.opts, d)]
  792. for fname in files:
  793. relpath = os.path.relpath(os.path.join(root, fname), path)
  794. ret.append(salt.utils.data.decode(relpath))
  795. return ret
  796. def file_list_emptydirs(self, saltenv='base', prefix=''):
  797. '''
  798. List the empty dirs in the pillar_roots
  799. with optional relative prefix path to limit directory traversal
  800. '''
  801. ret = []
  802. prefix = prefix.strip('/')
  803. for path in self.opts['pillar_roots'].get(saltenv, []):
  804. for root, dirs, files in salt.utils.path.os_walk(
  805. os.path.join(path, prefix), followlinks=True
  806. ):
  807. # Don't walk any directories that match file_ignore_regex or glob
  808. dirs[:] = [d for d in dirs if not salt.fileserver.is_file_ignored(self.opts, d)]
  809. if not dirs and not files:
  810. ret.append(salt.utils.data.decode(os.path.relpath(root, path)))
  811. return ret
  812. def dir_list(self, saltenv='base', prefix=''):
  813. '''
  814. List the dirs in the pillar_roots
  815. with optional relative prefix path to limit directory traversal
  816. '''
  817. ret = []
  818. prefix = prefix.strip('/')
  819. for path in self.opts['pillar_roots'].get(saltenv, []):
  820. for root, dirs, files in salt.utils.path.os_walk(
  821. os.path.join(path, prefix), followlinks=True
  822. ):
  823. ret.append(salt.utils.data.decode(os.path.relpath(root, path)))
  824. return ret
  825. def __get_file_path(self, path, saltenv='base'):
  826. '''
  827. Return either a file path or the result of a remote find_file call.
  828. '''
  829. try:
  830. path = self._check_proto(path)
  831. except MinionError as err:
  832. # Local file path
  833. if not os.path.isfile(path):
  834. log.warning(
  835. 'specified file %s is not present to generate hash: %s',
  836. path, err
  837. )
  838. return None
  839. else:
  840. return path
  841. return self._find_file(path, saltenv)
  842. def hash_file(self, path, saltenv='base'):
  843. '''
  844. Return the hash of a file, to get the hash of a file in the pillar_roots
  845. prepend the path with salt://<file on server> otherwise, prepend the
  846. file with / for a local file.
  847. '''
  848. ret = {}
  849. fnd = self.__get_file_path(path, saltenv)
  850. if fnd is None:
  851. return ret
  852. try:
  853. # Remote file path (self._find_file() invoked)
  854. fnd_path = fnd['path']
  855. except TypeError:
  856. # Local file path
  857. fnd_path = fnd
  858. hash_type = self.opts.get('hash_type', 'md5')
  859. ret['hsum'] = salt.utils.hashutils.get_hash(fnd_path, form=hash_type)
  860. ret['hash_type'] = hash_type
  861. return ret
  862. def hash_and_stat_file(self, path, saltenv='base'):
  863. '''
  864. Return the hash of a file, to get the hash of a file in the pillar_roots
  865. prepend the path with salt://<file on server> otherwise, prepend the
  866. file with / for a local file.
  867. Additionally, return the stat result of the file, or None if no stat
  868. results were found.
  869. '''
  870. ret = {}
  871. fnd = self.__get_file_path(path, saltenv)
  872. if fnd is None:
  873. return ret, None
  874. try:
  875. # Remote file path (self._find_file() invoked)
  876. fnd_path = fnd['path']
  877. fnd_stat = fnd.get('stat')
  878. except TypeError:
  879. # Local file path
  880. fnd_path = fnd
  881. try:
  882. fnd_stat = list(os.stat(fnd_path))
  883. except Exception:
  884. fnd_stat = None
  885. hash_type = self.opts.get('hash_type', 'md5')
  886. ret['hsum'] = salt.utils.hashutils.get_hash(fnd_path, form=hash_type)
  887. ret['hash_type'] = hash_type
  888. return ret, fnd_stat
  889. def list_env(self, saltenv='base'):
  890. '''
  891. Return a list of the files in the file server's specified environment
  892. '''
  893. return self.file_list(saltenv)
  894. def master_opts(self):
  895. '''
  896. Return the master opts data
  897. '''
  898. return self.opts
  899. def envs(self):
  900. '''
  901. Return the available environments
  902. '''
  903. ret = []
  904. for saltenv in self.opts['pillar_roots']:
  905. ret.append(saltenv)
  906. return ret
  907. def master_tops(self):
  908. '''
  909. Originally returned information via the external_nodes subsystem.
  910. External_nodes was deprecated and removed in
  911. 2014.1.6 in favor of master_tops (which had been around since pre-0.17).
  912. salt-call --local state.show_top
  913. ends up here, but master_tops has not been extended to support
  914. show_top in a completely local environment yet. It's worth noting
  915. that originally this fn started with
  916. if 'external_nodes' not in opts: return {}
  917. So since external_nodes is gone now, we are just returning the
  918. empty dict.
  919. '''
  920. return {}
  921. class RemoteClient(Client):
  922. '''
  923. Interact with the salt master file server.
  924. '''
  925. def __init__(self, opts):
  926. Client.__init__(self, opts)
  927. self._closing = False
  928. self.channel = salt.transport.client.ReqChannel.factory(self.opts)
  929. if hasattr(self.channel, 'auth'):
  930. self.auth = self.channel.auth
  931. else:
  932. self.auth = ''
  933. def _refresh_channel(self):
  934. '''
  935. Reset the channel, in the event of an interruption
  936. '''
  937. self.channel = salt.transport.client.ReqChannel.factory(self.opts)
  938. return self.channel
  939. def __del__(self):
  940. self.destroy()
  941. def destroy(self):
  942. if self._closing:
  943. return
  944. self._closing = True
  945. channel = None
  946. try:
  947. channel = self.channel
  948. except AttributeError:
  949. pass
  950. if channel is not None:
  951. channel.close()
  952. def get_file(self,
  953. path,
  954. dest='',
  955. makedirs=False,
  956. saltenv='base',
  957. gzip=None,
  958. cachedir=None):
  959. '''
  960. Get a single file from the salt-master
  961. path must be a salt server location, aka, salt://path/to/file, if
  962. dest is omitted, then the downloaded file will be placed in the minion
  963. cache
  964. '''
  965. path, senv = salt.utils.url.split_env(path)
  966. if senv:
  967. saltenv = senv
  968. if not salt.utils.platform.is_windows():
  969. hash_server, stat_server = self.hash_and_stat_file(path, saltenv)
  970. try:
  971. mode_server = stat_server[0]
  972. except (IndexError, TypeError):
  973. mode_server = None
  974. else:
  975. hash_server = self.hash_file(path, saltenv)
  976. mode_server = None
  977. # Check if file exists on server, before creating files and
  978. # directories
  979. if hash_server == '':
  980. log.debug(
  981. 'Could not find file \'%s\' in saltenv \'%s\'',
  982. path, saltenv
  983. )
  984. return False
  985. # If dest is a directory, rewrite dest with filename
  986. if dest is not None \
  987. and (os.path.isdir(dest) or dest.endswith(('/', '\\'))):
  988. dest = os.path.join(dest, os.path.basename(path))
  989. log.debug(
  990. 'In saltenv \'%s\', \'%s\' is a directory. Changing dest to '
  991. '\'%s\'', saltenv, os.path.dirname(dest), dest
  992. )
  993. # Hash compare local copy with master and skip download
  994. # if no difference found.
  995. dest2check = dest
  996. if not dest2check:
  997. rel_path = self._check_proto(path)
  998. log.debug(
  999. 'In saltenv \'%s\', looking at rel_path \'%s\' to resolve '
  1000. '\'%s\'', saltenv, rel_path, path
  1001. )
  1002. with self._cache_loc(
  1003. rel_path, saltenv, cachedir=cachedir) as cache_dest:
  1004. dest2check = cache_dest
  1005. log.debug(
  1006. 'In saltenv \'%s\', ** considering ** path \'%s\' to resolve '
  1007. '\'%s\'', saltenv, dest2check, path
  1008. )
  1009. if dest2check and os.path.isfile(dest2check):
  1010. if not salt.utils.platform.is_windows():
  1011. hash_local, stat_local = \
  1012. self.hash_and_stat_file(dest2check, saltenv)
  1013. try:
  1014. mode_local = stat_local[0]
  1015. except (IndexError, TypeError):
  1016. mode_local = None
  1017. else:
  1018. hash_local = self.hash_file(dest2check, saltenv)
  1019. mode_local = None
  1020. if hash_local == hash_server:
  1021. return dest2check
  1022. log.debug(
  1023. 'Fetching file from saltenv \'%s\', ** attempting ** \'%s\'',
  1024. saltenv, path
  1025. )
  1026. d_tries = 0
  1027. transport_tries = 0
  1028. path = self._check_proto(path)
  1029. load = {'path': path,
  1030. 'saltenv': saltenv,
  1031. 'cmd': '_serve_file'}
  1032. if gzip:
  1033. gzip = int(gzip)
  1034. load['gzip'] = gzip
  1035. fn_ = None
  1036. if dest:
  1037. destdir = os.path.dirname(dest)
  1038. if not os.path.isdir(destdir):
  1039. if makedirs:
  1040. try:
  1041. os.makedirs(destdir)
  1042. except OSError as exc:
  1043. if exc.errno != errno.EEXIST: # ignore if it was there already
  1044. raise
  1045. else:
  1046. return False
  1047. # We need an open filehandle here, that's why we're not using a
  1048. # with clause:
  1049. fn_ = salt.utils.files.fopen(dest, 'wb+') # pylint: disable=resource-leakage
  1050. else:
  1051. log.debug('No dest file found')
  1052. while True:
  1053. if not fn_:
  1054. load['loc'] = 0
  1055. else:
  1056. load['loc'] = fn_.tell()
  1057. data = self.channel.send(load, raw=True)
  1058. if six.PY3:
  1059. # Sometimes the source is local (eg when using
  1060. # 'salt.fileserver.FSChan'), in which case the keys are
  1061. # already strings. Sometimes the source is remote, in which
  1062. # case the keys are bytes due to raw mode. Standardize on
  1063. # strings for the top-level keys to simplify things.
  1064. data = decode_dict_keys_to_str(data)
  1065. try:
  1066. if not data['data']:
  1067. if not fn_ and data['dest']:
  1068. # This is a 0 byte file on the master
  1069. with self._cache_loc(
  1070. data['dest'],
  1071. saltenv,
  1072. cachedir=cachedir) as cache_dest:
  1073. dest = cache_dest
  1074. with salt.utils.files.fopen(cache_dest, 'wb+') as ofile:
  1075. ofile.write(data['data'])
  1076. if 'hsum' in data and d_tries < 3:
  1077. # Master has prompted a file verification, if the
  1078. # verification fails, re-download the file. Try 3 times
  1079. d_tries += 1
  1080. hsum = salt.utils.hashutils.get_hash(dest, salt.utils.stringutils.to_str(data.get('hash_type', b'md5')))
  1081. if hsum != data['hsum']:
  1082. log.warning(
  1083. 'Bad download of file %s, attempt %d of 3',
  1084. path, d_tries
  1085. )
  1086. continue
  1087. break
  1088. if not fn_:
  1089. with self._cache_loc(
  1090. data['dest'],
  1091. saltenv,
  1092. cachedir=cachedir) as cache_dest:
  1093. dest = cache_dest
  1094. # If a directory was formerly cached at this path, then
  1095. # remove it to avoid a traceback trying to write the file
  1096. if os.path.isdir(dest):
  1097. salt.utils.files.rm_rf(dest)
  1098. fn_ = salt.utils.atomicfile.atomic_open(dest, 'wb+')
  1099. if data.get('gzip', None):
  1100. data = salt.utils.gzip_util.uncompress(data['data'])
  1101. else:
  1102. data = data['data']
  1103. if six.PY3 and isinstance(data, str):
  1104. data = data.encode()
  1105. fn_.write(data)
  1106. except (TypeError, KeyError) as exc:
  1107. try:
  1108. data_type = type(data).__name__
  1109. except AttributeError:
  1110. # Shouldn't happen, but don't let this cause a traceback.
  1111. data_type = six.text_type(type(data))
  1112. transport_tries += 1
  1113. log.warning(
  1114. 'Data transport is broken, got: %s, type: %s, '
  1115. 'exception: %s, attempt %d of 3',
  1116. data, data_type, exc, transport_tries
  1117. )
  1118. self._refresh_channel()
  1119. if transport_tries > 3:
  1120. log.error(
  1121. 'Data transport is broken, got: %s, type: %s, '
  1122. 'exception: %s, retry attempts exhausted',
  1123. data, data_type, exc
  1124. )
  1125. break
  1126. if fn_:
  1127. fn_.close()
  1128. log.info(
  1129. 'Fetching file from saltenv \'%s\', ** done ** \'%s\'',
  1130. saltenv, path
  1131. )
  1132. else:
  1133. log.debug(
  1134. 'In saltenv \'%s\', we are ** missing ** the file \'%s\'',
  1135. saltenv, path
  1136. )
  1137. return dest
  1138. def file_list(self, saltenv='base', prefix=''):
  1139. '''
  1140. List the files on the master
  1141. '''
  1142. load = {'saltenv': saltenv,
  1143. 'prefix': prefix,
  1144. 'cmd': '_file_list'}
  1145. return salt.utils.data.decode(self.channel.send(load)) if six.PY2 \
  1146. else self.channel.send(load)
  1147. def file_list_emptydirs(self, saltenv='base', prefix=''):
  1148. '''
  1149. List the empty dirs on the master
  1150. '''
  1151. load = {'saltenv': saltenv,
  1152. 'prefix': prefix,
  1153. 'cmd': '_file_list_emptydirs'}
  1154. return salt.utils.data.decode(self.channel.send(load)) if six.PY2 \
  1155. else self.channel.send(load)
  1156. def dir_list(self, saltenv='base', prefix=''):
  1157. '''
  1158. List the dirs on the master
  1159. '''
  1160. load = {'saltenv': saltenv,
  1161. 'prefix': prefix,
  1162. 'cmd': '_dir_list'}
  1163. return salt.utils.data.decode(self.channel.send(load)) if six.PY2 \
  1164. else self.channel.send(load)
  1165. def symlink_list(self, saltenv='base', prefix=''):
  1166. '''
  1167. List symlinked files and dirs on the master
  1168. '''
  1169. load = {'saltenv': saltenv,
  1170. 'prefix': prefix,
  1171. 'cmd': '_symlink_list'}
  1172. return salt.utils.data.decode(self.channel.send(load)) if six.PY2 \
  1173. else self.channel.send(load)
  1174. def __hash_and_stat_file(self, path, saltenv='base'):
  1175. '''
  1176. Common code for hashing and stating files
  1177. '''
  1178. try:
  1179. path = self._check_proto(path)
  1180. except MinionError as err:
  1181. if not os.path.isfile(path):
  1182. log.warning(
  1183. 'specified file %s is not present to generate hash: %s',
  1184. path, err
  1185. )
  1186. return {}, None
  1187. else:
  1188. ret = {}
  1189. hash_type = self.opts.get('hash_type', 'md5')
  1190. ret['hsum'] = salt.utils.hashutils.get_hash(path, form=hash_type)
  1191. ret['hash_type'] = hash_type
  1192. return ret
  1193. load = {'path': path,
  1194. 'saltenv': saltenv,
  1195. 'cmd': '_file_hash'}
  1196. return self.channel.send(load)
  1197. def hash_file(self, path, saltenv='base'):
  1198. '''
  1199. Return the hash of a file, to get the hash of a file on the salt
  1200. master file server prepend the path with salt://<file on server>
  1201. otherwise, prepend the file with / for a local file.
  1202. '''
  1203. return self.__hash_and_stat_file(path, saltenv)
  1204. def hash_and_stat_file(self, path, saltenv='base'):
  1205. '''
  1206. The same as hash_file, but also return the file's mode, or None if no
  1207. mode data is present.
  1208. '''
  1209. hash_result = self.hash_file(path, saltenv)
  1210. try:
  1211. path = self._check_proto(path)
  1212. except MinionError as err:
  1213. if not os.path.isfile(path):
  1214. return hash_result, None
  1215. else:
  1216. try:
  1217. return hash_result, list(os.stat(path))
  1218. except Exception:
  1219. return hash_result, None
  1220. load = {'path': path,
  1221. 'saltenv': saltenv,
  1222. 'cmd': '_file_find'}
  1223. fnd = self.channel.send(load)
  1224. try:
  1225. stat_result = fnd.get('stat')
  1226. except AttributeError:
  1227. stat_result = None
  1228. return hash_result, stat_result
  1229. def list_env(self, saltenv='base'):
  1230. '''
  1231. Return a list of the files in the file server's specified environment
  1232. '''
  1233. load = {'saltenv': saltenv,
  1234. 'cmd': '_file_list'}
  1235. return salt.utils.data.decode(self.channel.send(load)) if six.PY2 \
  1236. else self.channel.send(load)
  1237. def envs(self):
  1238. '''
  1239. Return a list of available environments
  1240. '''
  1241. load = {'cmd': '_file_envs'}
  1242. return salt.utils.data.decode(self.channel.send(load)) if six.PY2 \
  1243. else self.channel.send(load)
  1244. def master_opts(self):
  1245. '''
  1246. Return the master opts data
  1247. '''
  1248. load = {'cmd': '_master_opts'}
  1249. return salt.utils.data.decode(self.channel.send(load)) if six.PY2 \
  1250. else self.channel.send(load)
  1251. def master_tops(self):
  1252. '''
  1253. Return the metadata derived from the master_tops system
  1254. '''
  1255. log.debug(
  1256. 'The _ext_nodes master function has been renamed to _master_tops. '
  1257. 'To ensure compatibility when using older Salt masters we will '
  1258. 'continue to invoke the function as _ext_nodes until the '
  1259. 'Magnesium release.'
  1260. )
  1261. # TODO: Change back to _master_tops
  1262. # for Magnesium release
  1263. load = {'cmd': '_ext_nodes',
  1264. 'id': self.opts['id'],
  1265. 'opts': self.opts}
  1266. if self.auth:
  1267. load['tok'] = self.auth.gen_token(b'salt')
  1268. return salt.utils.data.decode(self.channel.send(load)) if six.PY2 \
  1269. else self.channel.send(load)
  1270. class FSClient(RemoteClient):
  1271. '''
  1272. A local client that uses the RemoteClient but substitutes the channel for
  1273. the FSChan object
  1274. '''
  1275. def __init__(self, opts): # pylint: disable=W0231
  1276. Client.__init__(self, opts) # pylint: disable=W0233
  1277. self._closing = False
  1278. self.channel = salt.fileserver.FSChan(opts)
  1279. self.auth = DumbAuth()
  1280. # Provide backward compatibility for anyone directly using LocalClient (but no
  1281. # one should be doing this).
  1282. LocalClient = FSClient
  1283. class DumbAuth(object):
  1284. '''
  1285. The dumbauth class is used to stub out auth calls fired from the FSClient
  1286. subsystem
  1287. '''
  1288. def gen_token(self, clear_tok):
  1289. return clear_tok