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