]> git.proxmox.com Git - ceph.git/blame - ceph/src/ceph-volume/ceph_volume/util/system.py
bump version to 18.2.4-pve3
[ceph.git] / ceph / src / ceph-volume / ceph_volume / util / system.py
CommitLineData
d2e6a577 1import errno
b32b8144 2import logging
d2e6a577
FG
3import os
4import pwd
5import platform
3efd9988 6import tempfile
d2e6a577 7import uuid
20effc67 8import subprocess
2a845540 9import threading
94b18763 10from ceph_volume import process, terminal
d2e6a577
FG
11from . import as_string
12
eafe8130
TL
13# python2 has no FileNotFoundError
14try:
15 FileNotFoundError
16except NameError:
17 FileNotFoundError = OSError
18
b32b8144 19logger = logging.getLogger(__name__)
94b18763 20mlogger = terminal.MultiLogger(__name__)
d2e6a577
FG
21
22# TODO: get these out of here and into a common area for others to consume
23if platform.system() == 'FreeBSD':
24 FREEBSD = True
25 DEFAULT_FS_TYPE = 'zfs'
26 PROCDIR = '/compat/linux/proc'
27 # FreeBSD does not have blockdevices any more
28 BLOCKDIR = '/dev'
29 ROOTGROUP = 'wheel'
30else:
31 FREEBSD = False
32 DEFAULT_FS_TYPE = 'xfs'
33 PROCDIR = '/proc'
34 BLOCKDIR = '/sys/block'
35 ROOTGROUP = 'root'
36
20effc67
TL
37host_rootfs = '/rootfs'
38run_host_cmd = [
39 'nsenter',
40 '--mount={}/proc/1/ns/mnt'.format(host_rootfs),
41 '--ipc={}/proc/1/ns/ipc'.format(host_rootfs),
42 '--net={}/proc/1/ns/net'.format(host_rootfs),
43 '--uts={}/proc/1/ns/uts'.format(host_rootfs)
44]
d2e6a577
FG
45
46def generate_uuid():
47 return str(uuid.uuid4())
48
20effc67
TL
49def find_executable_on_host(locations=[], executable='', binary_check='/bin/ls'):
50 paths = ['{}/{}'.format(location, executable) for location in locations]
51 command = []
52 command.extend(run_host_cmd + [binary_check] + paths)
53 process = subprocess.Popen(
54 command,
55 stdout=subprocess.PIPE,
56 stderr=subprocess.PIPE,
57 stdin=subprocess.PIPE,
58 close_fds=True
59 )
60 stdout = as_string(process.stdout.read())
61 if stdout:
62 executable_on_host = stdout.split('\n')[0]
63 logger.info('Executable {} found on the host, will use {}'.format(executable, executable_on_host))
64 return executable_on_host
65 else:
66 logger.warning('Executable {} not found on the host, will return {} as-is'.format(executable, executable))
67 return executable
d2e6a577 68
20effc67 69def which(executable, run_on_host=False):
94b18763 70 """find the location of an executable"""
92f5a8d4
TL
71 def _get_path(executable, locations):
72 for location in locations:
73 executable_path = os.path.join(location, executable)
74 if os.path.exists(executable_path) and os.path.isfile(executable_path):
75 return executable_path
76 return None
77
92f5a8d4 78 static_locations = (
94b18763
FG
79 '/usr/local/bin',
80 '/bin',
81 '/usr/bin',
82 '/usr/local/sbin',
83 '/usr/sbin',
84 '/sbin',
85 )
94b18763 86
20effc67
TL
87 if not run_on_host:
88 path = os.getenv('PATH', '')
89 path_locations = path.split(':')
90 exec_in_path = _get_path(executable, path_locations)
91 if exec_in_path:
92 return exec_in_path
93 mlogger.warning('Executable {} not in PATH: {}'.format(executable, path))
94
95 exec_in_static_locations = _get_path(executable, static_locations)
96 if exec_in_static_locations:
97 mlogger.warning('Found executable under {}, please ensure $PATH is set correctly!'.format(exec_in_static_locations))
98 return exec_in_static_locations
99 else:
100 executable = find_executable_on_host(static_locations, executable)
101
102 # At this point, either `find_executable_on_host()` found an executable on the host
103 # or we fallback to just returning the argument as-is, to prevent a hard fail, and
104 # hoping that the system might have the executable somewhere custom
105 return executable
94b18763 106
d2e6a577
FG
107def get_ceph_user_ids():
108 """
109 Return the id and gid of the ceph user
110 """
111 try:
112 user = pwd.getpwnam('ceph')
113 except KeyError:
114 # is this even possible?
115 raise RuntimeError('"ceph" user is not available in the current system')
116 return user[2], user[3]
117
118
1adf2230
AA
119def get_file_contents(path, default=''):
120 contents = default
121 if not os.path.exists(path):
122 return contents
123 try:
124 with open(path, 'r') as open_file:
125 contents = open_file.read().strip()
126 except Exception:
127 logger.exception('Failed to read contents from: %s' % path)
128
129 return contents
130
131
d2e6a577
FG
132def mkdir_p(path, chown=True):
133 """
134 A `mkdir -p` that defaults to chown the path to the ceph user
135 """
136 try:
137 os.mkdir(path)
138 except OSError as e:
139 if e.errno == errno.EEXIST:
140 pass
141 else:
142 raise
143 if chown:
144 uid, gid = get_ceph_user_ids()
145 os.chown(path, uid, gid)
146
147
148def chown(path, recursive=True):
149 """
150 ``chown`` a path to the ceph user (uid and guid fetched at runtime)
151 """
152 uid, gid = get_ceph_user_ids()
153 if os.path.islink(path):
28e407b8 154 process.run(['chown', '-h', 'ceph:ceph', path])
d2e6a577
FG
155 path = os.path.realpath(path)
156 if recursive:
157 process.run(['chown', '-R', 'ceph:ceph', path])
158 else:
159 os.chown(path, uid, gid)
160
161
3efd9988 162def is_binary(path):
d2e6a577 163 """
3efd9988
FG
164 Detect if a file path is a binary or not. Will falsely report as binary
165 when utf-16 encoded. In the ceph universe there is no such risk (yet)
166 """
167 with open(path, 'rb') as fp:
168 contents = fp.read(8192)
169 if b'\x00' in contents: # a null byte may signal binary
170 return True
171 return False
172
173
174class tmp_mount(object):
175 """
176 Temporarily mount a device on a temporary directory,
177 and unmount it upon exit
b32b8144
FG
178
179 When ``encrypted`` is set to ``True``, the exit method will call out to
180 close the device so that it doesn't remain open after mounting. It is
181 assumed that it will be open because otherwise it wouldn't be possible to
182 mount in the first place
3efd9988
FG
183 """
184
b32b8144 185 def __init__(self, device, encrypted=False):
3efd9988
FG
186 self.device = device
187 self.path = None
b32b8144 188 self.encrypted = encrypted
3efd9988
FG
189
190 def __enter__(self):
191 self.path = tempfile.mkdtemp()
192 process.run([
3efd9988
FG
193 'mount',
194 '-v',
195 self.device,
196 self.path
197 ])
198 return self.path
199
200 def __exit__(self, exc_type, exc_val, exc_tb):
201 process.run([
3efd9988
FG
202 'umount',
203 '-v',
204 self.path
205 ])
b32b8144
FG
206 if self.encrypted:
207 # avoid a circular import from the encryption module
208 from ceph_volume.util import encryption
209 encryption.dmcrypt_close(self.device)
210
211
92f5a8d4
TL
212def unmount_tmpfs(path):
213 """
214 Removes the mount at the given path iff the path is a tmpfs mount point.
215 Otherwise no action is taken.
216 """
217 _out, _err, rc = process.call(['findmnt', '-t', 'tmpfs', '-M', path])
218 if rc != 0:
219 logger.info('{} does not appear to be a tmpfs mount'.format(path))
220 else:
221 logger.info('Unmounting tmpfs path at {}'.format( path))
222 unmount(path)
223
224
b32b8144
FG
225def unmount(path):
226 """
227 Removes mounts at the given path
228 """
229 process.run([
230 'umount',
231 '-v',
232 path,
233 ])
3efd9988
FG
234
235
236def path_is_mounted(path, destination=None):
237 """
238 Check if the given path is mounted
239 """
2a845540
TL
240 m = Mounts(paths=True)
241 mounts = m.get_mounts()
3efd9988
FG
242 realpath = os.path.realpath(path)
243 mounted_locations = mounts.get(realpath, [])
244
245 if destination:
3efd9988
FG
246 return destination in mounted_locations
247 return mounted_locations != []
248
d2e6a577 249
3efd9988
FG
250def device_is_mounted(dev, destination=None):
251 """
252 Check if the given device is mounted, optionally validating that a
253 destination exists
254 """
2a845540
TL
255 plain_mounts = Mounts(devices=True)
256 realpath_mounts = Mounts(devices=True, realpath=True)
257
b32b8144 258 realpath_dev = os.path.realpath(dev) if dev.startswith('/') else dev
3efd9988 259 destination = os.path.realpath(destination) if destination else None
b32b8144 260 # plain mounts
2a845540
TL
261 plain_dev_mounts = plain_mounts.get_mounts().get(dev, [])
262 realpath_dev_mounts = plain_mounts.get_mounts().get(realpath_dev, [])
b32b8144 263 # realpath mounts
2a845540
TL
264 plain_dev_real_mounts = realpath_mounts.get_mounts().get(dev, [])
265 realpath_dev_real_mounts = realpath_mounts.get_mounts().get(realpath_dev, [])
b32b8144
FG
266
267 mount_locations = [
268 plain_dev_mounts,
269 realpath_dev_mounts,
270 plain_dev_real_mounts,
271 realpath_dev_real_mounts
272 ]
273
274 for mounts in mount_locations:
275 if mounts: # we have a matching mount
276 if destination:
277 if destination in mounts:
278 logger.info(
279 '%s detected as mounted, exists at destination: %s', dev, destination
280 )
281 return True
282 else:
494da23a 283 logger.info('%s was found as mounted', dev)
b32b8144 284 return True
494da23a 285 logger.info('%s was not found as mounted', dev)
b32b8144 286 return False
3efd9988 287
2a845540
TL
288class Mounts(object):
289 excluded_paths = []
290
291 def __init__(self, devices=False, paths=False, realpath=False):
292 self.devices = devices
293 self.paths = paths
294 self.realpath = realpath
295
296 def safe_realpath(self, path, timeout=0.2):
297 def _realpath(path, result):
298 p = os.path.realpath(path)
299 result.append(p)
300
301 result = []
302 t = threading.Thread(target=_realpath, args=(path, result))
303 t.setDaemon(True)
304 t.start()
305 t.join(timeout)
306 if t.is_alive():
307 return None
308 return result[0]
309
310 def get_mounts(self):
311 """
312 Create a mapping of all available system mounts so that other helpers can
313 detect nicely what path or device is mounted
314
315 It ignores (most of) non existing devices, but since some setups might need
316 some extra device information, it will make an exception for:
317
318 - tmpfs
319 - devtmpfs
320 - /dev/root
321
322 If ``devices`` is set to ``True`` the mapping will be a device-to-path(s),
323 if ``paths`` is set to ``True`` then the mapping will be
324 a path-to-device(s)
325
326 :param realpath: Resolve devices to use their realpaths. This is useful for
327 paths like LVM where more than one path can point to the same device
328 """
329 devices_mounted = {}
330 paths_mounted = {}
331 do_not_skip = ['tmpfs', 'devtmpfs', '/dev/root']
332 default_to_devices = self.devices is False and self.paths is False
333
334
335 with open(PROCDIR + '/mounts', 'rb') as mounts:
336 proc_mounts = mounts.readlines()
337
338 for line in proc_mounts:
339 fields = [as_string(f) for f in line.split()]
340 if len(fields) < 3:
d2e6a577 341 continue
2a845540
TL
342 if fields[0] in Mounts.excluded_paths or \
343 fields[1] in Mounts.excluded_paths:
344 continue
345 if self.realpath:
346 if fields[0].startswith('/'):
347 device = self.safe_realpath(fields[0])
348 if device is None:
349 logger.warning(f"Can't get realpath on {fields[0]}, skipping.")
350 Mounts.excluded_paths.append(fields[0])
351 continue
352 else:
353 device = fields[0]
354 else:
355 device = fields[0]
356 path = self.safe_realpath(fields[1])
357 if path is None:
358 logger.warning(f"Can't get realpath on {fields[1]}, skipping.")
359 Mounts.excluded_paths.append(fields[1])
360 continue
361 # only care about actual existing devices
362 if not os.path.exists(device) or not device.startswith('/'):
363 if device not in do_not_skip:
364 continue
365 if device in devices_mounted.keys():
366 devices_mounted[device].append(path)
367 else:
368 devices_mounted[device] = [path]
369 if path in paths_mounted.keys():
370 paths_mounted[path].append(device)
371 else:
372 paths_mounted[path] = [device]
3efd9988 373
2a845540
TL
374 # Default to returning information for devices if
375 if self.devices is True or default_to_devices:
376 return devices_mounted
377 else:
378 return paths_mounted
1adf2230
AA
379
380
eafe8130
TL
381def set_context(path, recursive=False):
382 """
383 Calls ``restorecon`` to set the proper context on SELinux systems. Only if
384 the ``restorecon`` executable is found anywhere in the path it will get
385 called.
386
387 If the ``CEPH_VOLUME_SKIP_RESTORECON`` environment variable is set to
388 any of: "1", "true", "yes" the call will be skipped as well.
389
390 Finally, if SELinux is not enabled, or not available in the system,
391 ``restorecon`` will not be called. This is checked by calling out to the
392 ``selinuxenabled`` executable. If that tool is not installed or returns
393 a non-zero exit status then no further action is taken and this function
394 will return.
395 """
396 skip = os.environ.get('CEPH_VOLUME_SKIP_RESTORECON', '')
397 if skip.lower() in ['1', 'true', 'yes']:
398 logger.info(
399 'CEPH_VOLUME_SKIP_RESTORECON environ is set, will not call restorecon'
400 )
401 return
402
403 try:
404 stdout, stderr, code = process.call(['selinuxenabled'],
405 verbose_on_failure=False)
406 except FileNotFoundError:
407 logger.info('No SELinux found, skipping call to restorecon')
408 return
409
410 if code != 0:
411 logger.info('SELinux is not enabled, will not call restorecon')
412 return
413
1adf2230
AA
414 # restore selinux context to default policy values
415 if which('restorecon').startswith('/'):
416 if recursive:
417 process.run(['restorecon', '-R', path])
418 else:
419 process.run(['restorecon', path])