]> git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/util/system.py
Import ceph 15.2.8
[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
264 If ``devices`` is set to ``True`` the mapping will be a device-to-path(s),
265 if ``paths`` is set to ``True`` then the mapping will be
266 a path-to-device(s)
267
268 :param realpath: Resolve devices to use their realpaths. This is useful for
269 paths like LVM where more than one path can point to the same device
270 """
271 devices_mounted = {}
272 paths_mounted = {}
273 do_not_skip = ['tmpfs', 'devtmpfs']
274 default_to_devices = devices is False and paths is False
275
276 with open(PROCDIR + '/mounts', 'rb') as mounts:
277 proc_mounts = mounts.readlines()
278
279 for line in proc_mounts:
280 fields = [as_string(f) for f in line.split()]
281 if len(fields) < 3:
282 continue
283 if realpath:
284 device = os.path.realpath(fields[0]) if fields[0].startswith('/') else fields[0]
285 else:
286 device = fields[0]
287 path = os.path.realpath(fields[1])
288 # only care about actual existing devices
289 if not os.path.exists(device) or not device.startswith('/'):
290 if device not in do_not_skip:
291 continue
292 if device in devices_mounted.keys():
293 devices_mounted[device].append(path)
294 else:
295 devices_mounted[device] = [path]
296 if path in paths_mounted.keys():
297 paths_mounted[path].append(device)
298 else:
299 paths_mounted[path] = [device]
300
301 # Default to returning information for devices if
302 if devices is True or default_to_devices:
303 return devices_mounted
304 else:
305 return paths_mounted
306
307
308 def set_context(path, recursive=False):
309 """
310 Calls ``restorecon`` to set the proper context on SELinux systems. Only if
311 the ``restorecon`` executable is found anywhere in the path it will get
312 called.
313
314 If the ``CEPH_VOLUME_SKIP_RESTORECON`` environment variable is set to
315 any of: "1", "true", "yes" the call will be skipped as well.
316
317 Finally, if SELinux is not enabled, or not available in the system,
318 ``restorecon`` will not be called. This is checked by calling out to the
319 ``selinuxenabled`` executable. If that tool is not installed or returns
320 a non-zero exit status then no further action is taken and this function
321 will return.
322 """
323 skip = os.environ.get('CEPH_VOLUME_SKIP_RESTORECON', '')
324 if skip.lower() in ['1', 'true', 'yes']:
325 logger.info(
326 'CEPH_VOLUME_SKIP_RESTORECON environ is set, will not call restorecon'
327 )
328 return
329
330 try:
331 stdout, stderr, code = process.call(['selinuxenabled'],
332 verbose_on_failure=False)
333 except FileNotFoundError:
334 logger.info('No SELinux found, skipping call to restorecon')
335 return
336
337 if code != 0:
338 logger.info('SELinux is not enabled, will not call restorecon')
339 return
340
341 # restore selinux context to default policy values
342 if which('restorecon').startswith('/'):
343 if recursive:
344 process.run(['restorecon', '-R', path])
345 else:
346 process.run(['restorecon', path])