You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

619 lines
23 KiB

  1. import os
  2. import re
  3. from . import files
  4. from .tools import Cachable, GenericFS, unique
  5. from time import sleep
  6. def get_uuid_dict(reverse=False, devices=()):
  7. '''Получить словарь со значениями UUID блочных устройств.'''
  8. blkid_process = files.Process('/sbin/blkid', '-s', 'UUID',
  9. '-c', '/dev/null', *devices)
  10. re_split = re.compile('^([^:]+):.*UUID="([^"]+)"', re.S)
  11. output_items = {}
  12. search_results = []
  13. lines = blkid_process.read_lines()
  14. for line in lines:
  15. search_result = re_split.search(line)
  16. if search_result:
  17. search_results.append(search_result.groups())
  18. output_items = (("UUID={}".format(uuid), udev.get_devname(name=dev,
  19. fallback=dev))
  20. for dev, uuid in search_results)
  21. if reverse:
  22. return {v: k for k, v in output_items}
  23. else:
  24. return {k: v for k, v in output_items}
  25. def find_device_by_partition(device):
  26. '''Найти устройство, к которому относится данный раздел.'''
  27. info = udev.get_device_info(name=device)
  28. if info.get('DEVTYPE', '') != 'partition':
  29. return ''
  30. parent_path = os.path.dirname(info.get('DEVPATH', ''))
  31. if parent_path:
  32. device_info = udev.get_device_info(path=parent_path)
  33. return device_info.get('DEVNAME', '')
  34. return False
  35. def count_partitions(device_name):
  36. '''Посчитать количество разделов данного устройства.'''
  37. syspath = udev.get_device_info(name=device_name).get('DEVPATH', '')
  38. if not syspath:
  39. return 0
  40. device_name = os.path.basename(syspath)
  41. return len([x for x in sysfs.listdir(syspath)
  42. if x.startswith(device_name)])
  43. def get_lspci_output(filter_name=None, short_info=False):
  44. '''Функция для чтения вывода lspci. Возвращает словарь с идентификаторами
  45. устройств и информацией о них.'''
  46. re_data = re.compile(r'(\S+)\s"([^"]+)"\s+"([^"]+)"\s+"([^"]+)"', re.S)
  47. lspci_column_names = ('type', 'vendor', 'name')
  48. if filter_name:
  49. if callable(filter_name):
  50. filter_function = filter_name
  51. else:
  52. def filter_function(input):
  53. return filter_name in input
  54. else:
  55. def filter_function(input):
  56. return input
  57. lspci_path = files.check_utils('/usr/sbin/lspci')
  58. lspci_output = files.Process(lspci_path, '-m').read_lines()
  59. output = {}
  60. lspci_output = list(filter(filter_function, lspci_output))
  61. for line in lspci_output:
  62. search_result = re_data.search(line)
  63. if search_result:
  64. search_result = search_result.groups()
  65. device_id = search_result[0]
  66. output[device_id] = {key: value for key, value in zip(
  67. lspci_column_names,
  68. search_result[1:])}
  69. return output
  70. class LvmCommand:
  71. '''Класс для работы с командой lvm и выполнения с ее помощью различных
  72. действий.'''
  73. @property
  74. def lvm_command(self):
  75. '''Возвращает кешированный путь к lvm.'''
  76. return files.get_program_path('/sbin/lvm')
  77. def get_physical_extent_size(self):
  78. if not self.lvm_command:
  79. return ''
  80. pv_data = files.Process(self.lvm_command, 'lvmconfig', '--type',
  81. 'full', 'allocation/physical_extent_size')
  82. if pv_data.success():
  83. return pv_data.read()
  84. return ''
  85. def get_pvdisplay_output(self, option):
  86. '''Получить вывод pv_display c указанной option.'''
  87. if not self.lvm_command:
  88. return ''
  89. pv_data = files.Process(self.lvm_command, 'pvdisplay', '-C', '-o',
  90. option, '--noh')
  91. if pv_data.success():
  92. return pv_data.read()
  93. return False
  94. def vg_scan(self):
  95. '''Найти существующие в системе группы томов.'''
  96. if self.lvm_command:
  97. return files.Process(self.lvm_command, 'vgscan').success()
  98. return False
  99. def vg_change(self):
  100. '''Изменить атрибуты группы томов.'''
  101. if self.lvm_command:
  102. failed = files.Process('vgchange', '-ay').failed()
  103. failed |= files.Process('vgchange', '--refresh').failed()
  104. return not failed
  105. return False
  106. def lv_change(self, groups):
  107. '''Изменить атрибуты логического тома.'''
  108. if self.lvm_command:
  109. failed = False
  110. for group in groups:
  111. failed |= files.Process('lvchange', '-ay', group).failed()
  112. failed |= files.Process('lvchange', '--refresh',
  113. group).failed()
  114. return failed
  115. return False
  116. def execute(self, *command):
  117. '''Выполнить указанную LVM команду.'''
  118. if self.lvm_command:
  119. return files.Process(self.lvm_command, *command).success()
  120. return False
  121. def double_execute(self, *command):
  122. '''Выполнить дважды указанную LVM команду.'''
  123. if not self.execute(*command):
  124. sleep(2)
  125. return self.execute(*command)
  126. return False
  127. def remove_lv(self, vg, lv):
  128. '''Удалить указанный логических томов из системы.'''
  129. return self.double_execute('lvremove', '{0}/{1}'.format(vg, lv), '-f')
  130. def remove_vg(self, vg):
  131. '''Удалить указанную группу томов из системы.'''
  132. return self.double_execute('vgremove', vg, '-f')
  133. def remove_pv(self, pv):
  134. '''Удалить LVM метку с физического тома.'''
  135. return self.double_execute('pvremove', pv, '-ffy')
  136. class Lvm(Cachable):
  137. '''Класс для получения информации о lvm.'''
  138. def __init__(self, lvm_command):
  139. super().__init__()
  140. self.lvm_command = lvm_command
  141. def get_pvdisplay_full(self):
  142. '''Получить полный вывод команды pvdisplay.'''
  143. get_pvdisplay_output = self.get_pvdisplay_output(
  144. 'vg_name,lv_name,pv_name'
  145. ).strip()
  146. if get_pvdisplay_output:
  147. output = (line.split() for line in
  148. get_pvdisplay_output.split('\n'))
  149. return [tuple(y.strip() for y in x)
  150. for x in output if x and len(x) == 3]
  151. @property
  152. def get_volume_groups(self):
  153. '''Получить имеющиеся в системе группы томов.'''
  154. return sorted({vg for vg, lv, pv in self.get_pvdisplay_full()})
  155. @Cachable.method_cached()
  156. def get_physical_extent_size(self):
  157. '''Получить размер физического диапазона.'''
  158. return self.lvm_command.get_physical_extent_size()
  159. @Cachable.method_cached()
  160. def get_pvdisplay_output(self, option):
  161. '''Получить с помощью pvdisplay информацию .'''
  162. return self.lvm_command.get_pvdisplay_output(option)
  163. def get_used_partitions(self, vg_condition, lv_condition):
  164. '''Получить испоьзуемые разделы.'''
  165. return list(sorted({part for vg, lv, part in self.get_pvdisplay_full()
  166. if vg == vg_condition and lv == lv_condition}))
  167. def refresh(self):
  168. if not os.environ.get('EBUILD_PHASE', False):
  169. self.lvm_command.vg_scan()
  170. self.lvm_command.vg_change()
  171. self.lvm_command.lv_change(lvm)
  172. def remove_lv(self, vg, lv):
  173. return self.lvm_command.remove_lv(vg, lv)
  174. def remove_vg(self, vg):
  175. return self.lvm_command.remove_vg(vg)
  176. def remove_pv(self, pv):
  177. return self.lvm_command.remove_pv(pv)
  178. class MdadmCommand:
  179. '''Класс для работы с командой mdadm.'''
  180. @property
  181. def mdadm_command(self):
  182. '''Возвращает кешированный путь к mdamd.'''
  183. return files.get_program_path('/sbin/mdadm')
  184. def stop_raid(self, raid_device):
  185. '''Остановить устройство из RAID-массива.'''
  186. if not self.mdadm_command:
  187. return False
  188. return files.Process(self.mdadm_command, '-S', raid_device).success()
  189. def zero_superblock(self, device):
  190. '''Затереть superblock для составляющей RAID-массива.'''
  191. if not self.mdadm_command:
  192. return False
  193. return files.Process(self.mdadm_command, '--zero_superblock',
  194. device).success()
  195. class RAID:
  196. '''Класс для работы с RAID-массивами.'''
  197. def __init__(self, mdadm_command):
  198. self.mdadm_command = mdadm_command
  199. def get_devices_info(self, raid_device):
  200. '''Получить информацию о RAID-массиве.'''
  201. device = udev.get_device_info(path=raid_device)
  202. if udev.is_raid(device):
  203. for raid_block in sysfs.glob(raid_device, 'md/dev-*', 'block'):
  204. yield udev.get_device_info(sysfs.join_path(raid_block))
  205. def get_devices(self, raid_device, path_name='DEVPATH'):
  206. '''Получить устройства /dev, из которых состоит RAID-массив.
  207. Не возвращает список этих устройств для раздела сделанного для RAID
  208. (/dev/md0p1).'''
  209. for device_info in self.get_devices_info(raid_device):
  210. device_name = device_info.get(path_name, '')
  211. if device_name:
  212. yield device_name
  213. def get_devices_syspath(self, raid_device):
  214. '''Получить sysfs пути устройств, из которых состоит RAID-массив.'''
  215. for device in self.get_devices(raid_device, path_name='DEVPATH'):
  216. yield device
  217. def remove_raid(self, raid_name):
  218. '''Удалить RAID-массив.'''
  219. raid_parts = list(self.get_devices(udev.get_syspath(name=raid_name)))
  220. failed = False
  221. failed |= not (self.mdadm_command.stop_raid(raid_name) or
  222. self.mdadm_command.stop_raid(raid_name))
  223. for device in raid_parts:
  224. failed |= not (self.mdadm_command.zero_superblock(device) or
  225. self.mdadm_command.zero_superblock(device))
  226. return not failed
  227. class DeviceFS(GenericFS):
  228. '''Базовый класс для классов предназначенных для работы с sysfs и /dev'''
  229. def __init__(self, filesystem=None):
  230. if isinstance(filesystem, GenericFS):
  231. self.filesystem = filesystem
  232. else:
  233. self.filesystem = files.RealFS('/')
  234. def join_path(self, *paths):
  235. output = os.path.join('/', *(_path[1:] if _path.startswith('/')
  236. else _path for _path in paths))
  237. return output
  238. def exists(self, *paths):
  239. return self.filesystem.exists(self.join_path(*paths))
  240. def read(self, *paths):
  241. return self.filesystem.read(self.join_path(*paths))
  242. def realpath(self, *paths):
  243. return self.filesystem.realpath(self.join_path(*paths))
  244. def write(self, *args):
  245. data = args[-1]
  246. paths = args[:-1]
  247. self.filesystem.write(self.join_path(*paths), data)
  248. def listdir(self, *paths, full_path=False):
  249. return self.filesystem.listdir(self.join_path(*paths),
  250. full_path=full_path)
  251. def glob(self, *paths):
  252. for file_path in self.filesystem.glob(self.join_path(*paths)):
  253. yield file_path
  254. class SysFS(DeviceFS):
  255. '''Класс для работы с sysfs.'''
  256. BASE_DIRECTORY = '/sys'
  257. PATH = {'firmware': 'firmware',
  258. 'efi': 'firmware/efi',
  259. 'efivars': 'firmware/efi/efivars',
  260. 'classnet': 'class/net',
  261. 'input': 'class/input',
  262. 'block': 'block',
  263. 'dmi': 'class/dmi/id',
  264. 'module': 'module',
  265. 'block_scheduler': 'queue/scheduler'}
  266. def join_path(self, *paths):
  267. output = os.path.join('/', *(_path[1:] if _path.startswith('/')
  268. else _path for _path in paths))
  269. if output.startswith(self.BASE_DIRECTORY):
  270. return output
  271. else:
  272. return os.path.join(self.BASE_DIRECTORY, output[1:])
  273. def glob(self, *paths):
  274. for _path in self.filesystem.glob(self.join_path(*paths)):
  275. yield _path[len(self.BASE_DIRECTORY):]
  276. class DevFS(DeviceFS):
  277. '''Класс для работы с /dev.'''
  278. BASE_DIRECTORY = '/dev'
  279. def join_path(self, *paths):
  280. output = os.path.join('/', *(_path[1:] if _path.startswith('/')
  281. else _path for _path in paths))
  282. if output.startswith(self.BASE_DIRECTORY):
  283. return output
  284. else:
  285. return os.path.join(self.BASE_DIRECTORY, output[1:])
  286. def glob(self, *paths):
  287. for _path in self.filesystem.glob(self.join_path(*paths)):
  288. yield _path[len(self.BASE_DIRECTORY):]
  289. class UdevAdmNull:
  290. def info_property(self, path=None, name=None):
  291. return {}
  292. def info_export(self):
  293. return ''
  294. def settle(self, timeout=15):
  295. pass
  296. def trigger(self, subsystem=None):
  297. pass
  298. @property
  299. def broken(self):
  300. return False
  301. class UdevAdmCommand(UdevAdmNull):
  302. def __init__(self):
  303. self.first_run = True
  304. @property
  305. def udevadm_cmd(self):
  306. return files.get_program_path('/sbin/udevadm')
  307. @property
  308. def broken(self):
  309. return not bool(self.udevadm_cmd)
  310. def info_property(self, path=None, name=None):
  311. if self.first_run:
  312. self.trigger("block")
  313. self.settle()
  314. self.first_run = False
  315. if name is not None:
  316. type_query = "--name"
  317. value = name
  318. else:
  319. type_query = "--path"
  320. value = path
  321. udevadm_output = files.Process(self.udevadm_cmd, "info",
  322. "--query", "property",
  323. type_query, value).read_lines()
  324. output_items = []
  325. for line in udevadm_output:
  326. if '=' in line:
  327. output_items.append(line.partition('=')[0::2])
  328. return dict(output_items)
  329. def info_export(self):
  330. return files.Process(self.udevadm_cmd, 'info', '-e').read().strip()
  331. def trigger(self, subsystem=None):
  332. if subsystem:
  333. files.Process(self.udevadm_cmd, 'trigger', '--subsystem-match',
  334. subsystem).success()
  335. else:
  336. files.Process(self.udevadm_cmd, 'trigger').success()
  337. def settle(self, timeout=15):
  338. files.Process(self.udevadm_cmd, 'settle', '--timeout={}'.
  339. format(timeout)).success()
  340. class Udev:
  341. '''Класс возвращающий преобразованную или кэшированную информацию о системе
  342. из udev.'''
  343. def __init__(self, udevadm=None):
  344. self.path_cache = {}
  345. self.name_cache = {}
  346. if isinstance(udevadm, UdevAdmCommand):
  347. self.udevadm = udevadm
  348. else:
  349. self.udevadm = UdevAdmCommand()
  350. if self.udevadm.broken:
  351. self.udevadm = UdevAdmNull()
  352. def clear_cache(self):
  353. self.path_cache = {}
  354. self.name_cache = {}
  355. self.udevadm = UdevAdmCommand()
  356. def get_device_info(self, path=None, name=None):
  357. if name is not None:
  358. cache = self.name_cache
  359. value = devfs.realpath(name)
  360. name = value
  361. else:
  362. cache = self.path_cache
  363. value = sysfs.realpath(path)
  364. path = value
  365. if value not in cache:
  366. data = self.udevadm.info_property(path, name)
  367. devname = data.get('DEVNAME', '')
  368. devpath = data.get('DEVPATH', '')
  369. if devname:
  370. self.name_cache[devname] = data
  371. if devpath:
  372. devpath = '/sys{}'.format(devpath)
  373. self.path_cache[devname] = data
  374. return data
  375. else:
  376. return cache[value]
  377. def get_block_devices(self):
  378. for block in sysfs.glob(sysfs.PATH['block'], '*'):
  379. yield block
  380. blockname = os.path.basename(block)
  381. for part in sysfs.glob(block, '{}*'.format(blockname)):
  382. yield part
  383. def syspath_to_devname(self, devices, drop_empty=True):
  384. for device_path in devices:
  385. info = self.get_device_info(path=device_path)
  386. if 'DEVNAME' in info:
  387. yield info['DEVNAME']
  388. elif not drop_empty:
  389. yield ''
  390. def devname_to_syspath(self, devices, drop_empty=True):
  391. for device_name in devices:
  392. info = self.get_device_info(name=device_name)
  393. if 'DEVPATH' in info:
  394. yield info['DEVPATH']
  395. elif not drop_empty:
  396. yield ''
  397. def get_devname(self, path=None, name=None, fallback=None):
  398. if fallback is None:
  399. if name:
  400. fallback = name
  401. else:
  402. fallback = ''
  403. return self.get_device_info(path, name).get('DEVNAME', fallback)
  404. def get_syspath(self, path=None, name=None, fallback=None):
  405. if fallback is None:
  406. if path:
  407. fallback = path
  408. else:
  409. fallback = ''
  410. return self.get_device_info(path, name).get('DEVPATH', fallback)
  411. def refresh(self, trigger_only=False):
  412. if not trigger_only:
  413. self.clear_cache()
  414. files.quite_unlink('/etc/blkid.tab')
  415. if not os.environ.get('EBUILD_PHASE'):
  416. self.udevadm.trigger(subsystem='block')
  417. self.udevadm.settle(15)
  418. def is_cdrom(self, udev_data):
  419. return udev_data.get('ID_CDROM', '') == '1'
  420. def is_device(self, udev_data):
  421. if 'DEVPATH' not in udev_data:
  422. return False
  423. return (sysfs.exists(udev_data.get('DEVPATH', ''), 'device') and
  424. udev_data.get('DEVTYPE', '') == 'disk')
  425. def is_raid(self, udev_data):
  426. return (udev_data.get('MD_LEVEL', '').startswith('raid') and
  427. udev_data.get('DEVTYPE', '') == 'disk')
  428. def is_raid_partition(self, udev_data):
  429. return (udev_data.get('MD_LEVEL', '').startswith('raid') and
  430. udev_data.get('DEVTYPE', '') == 'partition')
  431. def is_lvm(self, udev_data):
  432. return 'DM_LV_NAME' in udev_data and 'DM_VG_NAME' in udev_data
  433. def is_partition(self, udev_data):
  434. return udev_data.get('DEVTYPE', '') == 'partition'
  435. def is_block_device(self, udev_data):
  436. return udev_data.get('SUBSYSTEM', '') == 'block'
  437. def get_device_type(self, path=None, name=None):
  438. info = self.get_device_info(path, name)
  439. syspath = info.get('DEVPATH', '')
  440. if self.is_cdrom(info):
  441. return 'cdrom'
  442. if self.is_device(info):
  443. return 'disk'
  444. if self.is_partition(info):
  445. return '{}-partition'.format(
  446. self.get_device_type(os.path.dirname(syspath)))
  447. if self.is_raid(info):
  448. for raid_device in raid.get_devices_syspath(syspath):
  449. return '{}-{}'.format(self.get_device_type(raid_device),
  450. info['MD_LEVEL'])
  451. else:
  452. return 'loop'
  453. if self.is_lvm(info):
  454. for lv_device in lvm.get_used_partitions(info['DM_VG_NAME'],
  455. info['DM_LV_NAME']):
  456. return '{}-lvm'.format(self.get_device_type(name=lv_device))
  457. if self.is_block_device(info):
  458. return 'loop'
  459. return ''
  460. def get_partition_type(self, path=None, name=None):
  461. info = self.get_device_info(path, name)
  462. if info.get('ID_PART_ENTRY_SCHEME') == 'dos':
  463. part_id = info.get('ID_PART_ENTRY_TYPE', '')
  464. part_number = info.get('ID_PART_ENTRY_NUMBER', '')
  465. if part_id and part_number:
  466. if part_id == '0x5':
  467. return 'extended'
  468. elif int(part_number) > 4:
  469. return 'logical'
  470. else:
  471. return 'primary'
  472. return info.get('ID_PART_TABLE_TYPE', '')
  473. def _get_disk_devices(self, path=None, name=None):
  474. info = udev.get_device_info(path=path, name=name)
  475. syspath = info.get('DEVPATH', '')
  476. if syspath:
  477. # real device
  478. if self.is_device(info):
  479. yield True, info.get('DEVNAME', '')
  480. # partition
  481. elif self.is_partition(info):
  482. for device in self._get_disk_devices(
  483. path=os.path.dirname(syspath)):
  484. yield device
  485. # md raid
  486. elif udev.is_raid(info):
  487. yield False, info.get('DEVNAME', '')
  488. for raid_device in sorted(raid.get_devices_syspath(syspath)):
  489. for device in self._get_disk_devices(path=raid_device):
  490. yield device
  491. # lvm
  492. elif udev.is_lvm(info):
  493. yield False, info.get('DEVNAME', '')
  494. for lv_device in lvm.get_used_partitions(info['DM_VG_NAME'],
  495. info['DM_LV_NAME']):
  496. for device in self._get_disk_devices(name=lv_device):
  497. yield device
  498. def get_disk_devices(self, path=None, name=None):
  499. return sorted({
  500. device for real_device, device in
  501. self._get_disk_devices(path, name) if real_device
  502. })
  503. def get_all_base_devices(self, path=None, name=None):
  504. try:
  505. devices = (device for real_device, device in
  506. self._get_disk_devices(path, name)
  507. if not device.startswith('/dev/loop'))
  508. if not self.is_partition(self.get_device_info(path, name)):
  509. next(devices)
  510. return list(unique(devices))
  511. except StopIteration:
  512. return []
  513. sysfs = SysFS()
  514. devfs = DevFS()
  515. udev = Udev(UdevAdmCommand())
  516. lvm = Lvm(LvmCommand())
  517. raid = RAID(MdadmCommand())