]>
git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/util/system.py
10 from ceph_volume
import process
, terminal
11 from . import as_string
13 # python2 has no FileNotFoundError
17 FileNotFoundError
= OSError
19 logger
= logging
.getLogger(__name__
)
20 mlogger
= terminal
.MultiLogger(__name__
)
22 # TODO: get these out of here and into a common area for others to consume
23 if platform
.system() == 'FreeBSD':
25 DEFAULT_FS_TYPE
= 'zfs'
26 PROCDIR
= '/compat/linux/proc'
27 # FreeBSD does not have blockdevices any more
32 DEFAULT_FS_TYPE
= 'xfs'
34 BLOCKDIR
= '/sys/block'
37 host_rootfs
= '/rootfs'
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
)
47 return str(uuid
.uuid4())
49 def find_executable_on_host(locations
=[], executable
='', binary_check
='/bin/ls'):
50 paths
= ['{}/{}'.format(location
, executable
) for location
in locations
]
52 command
.extend(run_host_cmd
+ [binary_check
] + paths
)
53 process
= subprocess
.Popen(
55 stdout
=subprocess
.PIPE
,
56 stderr
=subprocess
.PIPE
,
57 stdin
=subprocess
.PIPE
,
60 stdout
= as_string(process
.stdout
.read())
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
66 logger
.warning('Executable {} not found on the host, will return {} as-is'.format(executable
, executable
))
69 def which(executable
, run_on_host
=False):
70 """find the location of an executable"""
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
88 path
= os
.getenv('PATH', '')
89 path_locations
= path
.split(':')
90 exec_in_path
= _get_path(executable
, path_locations
)
93 mlogger
.warning('Executable {} not in PATH: {}'.format(executable
, path
))
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
100 executable
= find_executable_on_host(static_locations
, executable
)
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
107 def get_ceph_user_ids():
109 Return the id and gid of the ceph user
112 user
= pwd
.getpwnam('ceph')
114 # is this even possible?
115 raise RuntimeError('"ceph" user is not available in the current system')
116 return user
[2], user
[3]
119 def get_file_contents(path
, default
=''):
121 if not os
.path
.exists(path
):
124 with
open(path
, 'r') as open_file
:
125 contents
= open_file
.read().strip()
127 logger
.exception('Failed to read contents from: %s' % path
)
132 def mkdir_p(path
, chown
=True):
134 A `mkdir -p` that defaults to chown the path to the ceph user
139 if e
.errno
== errno
.EEXIST
:
144 uid
, gid
= get_ceph_user_ids()
145 os
.chown(path
, uid
, gid
)
148 def chown(path
, recursive
=True):
150 ``chown`` a path to the ceph user (uid and guid fetched at runtime)
152 uid
, gid
= get_ceph_user_ids()
153 if os
.path
.islink(path
):
154 process
.run(['chown', '-h', 'ceph:ceph', path
])
155 path
= os
.path
.realpath(path
)
157 process
.run(['chown', '-R', 'ceph:ceph', path
])
159 os
.chown(path
, uid
, gid
)
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)
167 with
open(path
, 'rb') as fp
:
168 contents
= fp
.read(8192)
169 if b
'\x00' in contents
: # a null byte may signal binary
174 class tmp_mount(object):
176 Temporarily mount a device on a temporary directory,
177 and unmount it upon exit
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
185 def __init__(self
, device
, encrypted
=False):
188 self
.encrypted
= encrypted
191 self
.path
= tempfile
.mkdtemp()
200 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
207 # avoid a circular import from the encryption module
208 from ceph_volume
.util
import encryption
209 encryption
.dmcrypt_close(self
.device
)
212 def unmount_tmpfs(path
):
214 Removes the mount at the given path iff the path is a tmpfs mount point.
215 Otherwise no action is taken.
217 _out
, _err
, rc
= process
.call(['findmnt', '-t', 'tmpfs', '-M', path
])
219 logger
.info('{} does not appear to be a tmpfs mount'.format(path
))
221 logger
.info('Unmounting tmpfs path at {}'.format( path
))
227 Removes mounts at the given path
236 def path_is_mounted(path
, destination
=None):
238 Check if the given path is mounted
240 m
= Mounts(paths
=True)
241 mounts
= m
.get_mounts()
242 realpath
= os
.path
.realpath(path
)
243 mounted_locations
= mounts
.get(realpath
, [])
246 return destination
in mounted_locations
247 return mounted_locations
!= []
250 def device_is_mounted(dev
, destination
=None):
252 Check if the given device is mounted, optionally validating that a
255 plain_mounts
= Mounts(devices
=True)
256 realpath_mounts
= Mounts(devices
=True, realpath
=True)
258 realpath_dev
= os
.path
.realpath(dev
) if dev
.startswith('/') else dev
259 destination
= os
.path
.realpath(destination
) if destination
else None
261 plain_dev_mounts
= plain_mounts
.get_mounts().get(dev
, [])
262 realpath_dev_mounts
= plain_mounts
.get_mounts().get(realpath_dev
, [])
264 plain_dev_real_mounts
= realpath_mounts
.get_mounts().get(dev
, [])
265 realpath_dev_real_mounts
= realpath_mounts
.get_mounts().get(realpath_dev
, [])
270 plain_dev_real_mounts
,
271 realpath_dev_real_mounts
274 for mounts
in mount_locations
:
275 if mounts
: # we have a matching mount
277 if destination
in mounts
:
279 '%s detected as mounted, exists at destination: %s', dev
, destination
283 logger
.info('%s was found as mounted', dev
)
285 logger
.info('%s was not found as mounted', dev
)
288 class Mounts(object):
291 def __init__(self
, devices
=False, paths
=False, realpath
=False):
292 self
.devices
= devices
294 self
.realpath
= realpath
296 def safe_realpath(self
, path
, timeout
=0.2):
297 def _realpath(path
, result
):
298 p
= os
.path
.realpath(path
)
302 t
= threading
.Thread(target
=_realpath
, args
=(path
, result
))
310 def get_mounts(self
):
312 Create a mapping of all available system mounts so that other helpers can
313 detect nicely what path or device is mounted
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:
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
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
331 do_not_skip
= ['tmpfs', 'devtmpfs', '/dev/root']
332 default_to_devices
= self
.devices
is False and self
.paths
is False
335 with
open(PROCDIR
+ '/mounts', 'rb') as mounts
:
336 proc_mounts
= mounts
.readlines()
338 for line
in proc_mounts
:
339 fields
= [as_string(f
) for f
in line
.split()]
342 if fields
[0] in Mounts
.excluded_paths
or \
343 fields
[1] in Mounts
.excluded_paths
:
346 if fields
[0].startswith('/'):
347 device
= self
.safe_realpath(fields
[0])
349 logger
.warning(f
"Can't get realpath on {fields[0]}, skipping.")
350 Mounts
.excluded_paths
.append(fields
[0])
356 path
= self
.safe_realpath(fields
[1])
358 logger
.warning(f
"Can't get realpath on {fields[1]}, skipping.")
359 Mounts
.excluded_paths
.append(fields
[1])
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
:
365 if device
in devices_mounted
.keys():
366 devices_mounted
[device
].append(path
)
368 devices_mounted
[device
] = [path
]
369 if path
in paths_mounted
.keys():
370 paths_mounted
[path
].append(device
)
372 paths_mounted
[path
] = [device
]
374 # Default to returning information for devices if
375 if self
.devices
is True or default_to_devices
:
376 return devices_mounted
381 def set_context(path
, recursive
=False):
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
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.
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
396 skip
= os
.environ
.get('CEPH_VOLUME_SKIP_RESTORECON', '')
397 if skip
.lower() in ['1', 'true', 'yes']:
399 'CEPH_VOLUME_SKIP_RESTORECON environ is set, will not call restorecon'
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')
411 logger
.info('SELinux is not enabled, will not call restorecon')
414 # restore selinux context to default policy values
415 if which('restorecon').startswith('/'):
417 process
.run(['restorecon', '-R', path
])
419 process
.run(['restorecon', path
])