storage.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. # -*- encoding: utf8 -*-
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2017 Marek Marczykowski-Górecki
  6. # <marmarek@invisiblethingslab.com>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU Lesser General Public License as published by
  10. # the Free Software Foundation; either version 2.1 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU Lesser General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public License along
  19. # with this program; if not, see <http://www.gnu.org/licenses/>.
  20. '''Storage subsystem.'''
  21. class Volume(object):
  22. '''Storage volume.'''
  23. def __init__(self, app, pool=None, vid=None, vm=None, vm_name=None):
  24. '''Construct a Volume object.
  25. Volume may be identified using pool+vid, or vm+vm_name. Either of
  26. those argument pairs must be given.
  27. :param Qubes app: application instance
  28. :param str pool: pool name
  29. :param str vid: volume id (within pool)
  30. :param str vm: owner VM name
  31. :param str vm_name: name within owning VM (like 'private', 'root' etc)
  32. '''
  33. self.app = app
  34. if pool is None and vm is None:
  35. raise ValueError('Either pool or vm must be given')
  36. if pool is not None and vid is None:
  37. raise ValueError('If pool is given, vid must be too.')
  38. if vm is not None and vm_name is None:
  39. raise ValueError('If vm is given, vm_name must be too.')
  40. self._pool = pool
  41. self._vid = vid
  42. self._vm = vm
  43. self._vm_name = vm_name
  44. self._info = None
  45. def _qubesd_call(self, func_name, payload=None, payload_stream=None):
  46. '''Make a call to qubesd regarding this volume
  47. :param str func_name: API function name, like `Info` or `Resize`
  48. :param bytes payload: Payload to send.
  49. :param file payload_stream: Stream to pipe payload from. Only one of
  50. `payload` and `payload_stream` can be used.
  51. '''
  52. if self._vm is not None:
  53. method = 'admin.vm.volume.' + func_name
  54. dest = self._vm
  55. arg = self._vm_name
  56. else:
  57. if payload_stream:
  58. raise NotImplementedError(
  59. 'payload_stream not implemented for '
  60. 'admin.pool.volume.* calls')
  61. method = 'admin.pool.volume.' + func_name
  62. dest = 'dom0'
  63. arg = self._pool
  64. if payload is not None:
  65. payload = self._vid.encode('ascii') + b' ' + payload
  66. else:
  67. payload = self._vid.encode('ascii')
  68. return self.app.qubesd_call(dest, method, arg, payload=payload,
  69. payload_stream=payload_stream)
  70. def _fetch_info(self, force=True):
  71. '''Fetch volume properties
  72. Populate self._info dict
  73. :param bool force: refresh self._info, even if already populated.
  74. '''
  75. if not force and self._info is not None:
  76. return
  77. info = self._qubesd_call('Info')
  78. info = info.decode('ascii')
  79. self._info = dict([line.split('=', 1) for line in info.splitlines()])
  80. def __eq__(self, other):
  81. if isinstance(other, Volume):
  82. return self.pool == other.pool and self.vid == other.vid
  83. return NotImplemented
  84. def __lt__(self, other):
  85. # pylint: disable=protected-access
  86. if isinstance(other, Volume):
  87. if self._vm and other._vm:
  88. return (self._vm, self._vm_name) < (other._vm, other._vm_name)
  89. if self._vid and other._vid:
  90. return (self._pool, self._vid) < (other._pool, other._vid)
  91. return NotImplemented
  92. @property
  93. def name(self):
  94. '''per-VM volume name, if available'''
  95. return self._vm_name
  96. @property
  97. def pool(self):
  98. '''Storage volume pool name.'''
  99. if self._pool is not None:
  100. return self._pool
  101. self._fetch_info()
  102. return str(self._info['pool'])
  103. @property
  104. def vid(self):
  105. '''Storage volume id, unique within given pool.'''
  106. if self._vid is not None:
  107. return self._vid
  108. self._fetch_info()
  109. return str(self._info['vid'])
  110. @property
  111. def size(self):
  112. '''Size of volume, in bytes.'''
  113. self._fetch_info(True)
  114. return int(self._info['size'])
  115. @property
  116. def usage(self):
  117. '''Used volume space, in bytes.'''
  118. self._fetch_info(True)
  119. return int(self._info['usage'])
  120. @property
  121. def rw(self):
  122. '''True if volume is read-write.'''
  123. self._fetch_info()
  124. return self._info['rw'] == 'True'
  125. @rw.setter
  126. def rw(self, value):
  127. '''Set rw property'''
  128. self._qubesd_call('Set.rw', str(value).encode('ascii'))
  129. self._info = None
  130. @property
  131. def snap_on_start(self):
  132. '''Create a snapshot from source on VM start.'''
  133. self._fetch_info()
  134. return self._info['snap_on_start'] == 'True'
  135. @property
  136. def save_on_stop(self):
  137. '''Commit changes to original volume on VM stop.'''
  138. self._fetch_info()
  139. return self._info['save_on_stop'] == 'True'
  140. @property
  141. def source(self):
  142. '''Volume ID of source volume (for :py:attr:`snap_on_start`).
  143. If None, this volume itself will be used.
  144. '''
  145. self._fetch_info()
  146. if self._info['source']:
  147. return self._info['source']
  148. return None
  149. @property
  150. def revisions_to_keep(self):
  151. '''Number of revisions to keep around'''
  152. self._fetch_info()
  153. return int(self._info['revisions_to_keep'])
  154. @revisions_to_keep.setter
  155. def revisions_to_keep(self, value):
  156. '''Set revisions_to_keep property'''
  157. self._qubesd_call('Set.revisions_to_keep', str(value).encode('ascii'))
  158. self._info = None
  159. def is_outdated(self):
  160. '''Returns `True` if this snapshot of a source volume (for
  161. `snap_on_start`=True) is outdated.
  162. '''
  163. self._fetch_info(True)
  164. return self._info.get('is_outdated', False) == 'True'
  165. def resize(self, size):
  166. '''Resize volume.
  167. Currently only extending is supported.
  168. :param int size: new size in bytes.
  169. '''
  170. self._qubesd_call('Resize', str(size).encode('ascii'))
  171. @property
  172. def revisions(self):
  173. ''' Returns iterable containing revision identifiers'''
  174. revisions = self._qubesd_call('ListSnapshots')
  175. return revisions.decode('ascii').splitlines()
  176. def revert(self, revision):
  177. ''' Revert volume to previous revision
  178. :param str revision: Revision identifier to revert to
  179. '''
  180. if not isinstance(revision, str):
  181. raise TypeError('revision must be a str')
  182. self._qubesd_call('Revert', revision.encode('ascii'))
  183. def import_data(self, stream):
  184. ''' Import volume data from a given file-like object.
  185. This function override existing volume content
  186. :param stream: file-like object, must support fileno()
  187. '''
  188. self._qubesd_call('Import', payload_stream=stream)
  189. def clone(self, source):
  190. ''' Clone data from sane volume of another VM.
  191. This function override existing volume content.
  192. This operation is implemented for VM volumes - those in vm.volumes
  193. collection (not pool.volumes).
  194. :param source: source volume object
  195. '''
  196. # pylint: disable=protected-access
  197. # get a token from source volume
  198. token = source._qubesd_call('CloneFrom')
  199. # and use it to actually clone volume data
  200. self._qubesd_call('CloneTo', payload=token)
  201. class Pool(object):
  202. ''' A Pool is used to manage different kind of volumes (File
  203. based/LVM/Btrfs/...).
  204. '''
  205. def __init__(self, app, name=None):
  206. ''' Initialize storage pool wrapper
  207. :param app: Qubes() object
  208. :param name: name of the pool
  209. '''
  210. self.app = app
  211. self.name = name
  212. self._config = None
  213. def __str__(self):
  214. return self.name
  215. def __eq__(self, other):
  216. if isinstance(other, Pool):
  217. return self.name == other.name
  218. if isinstance(other, str):
  219. return self.name == other
  220. return NotImplemented
  221. def __lt__(self, other):
  222. if isinstance(other, Pool):
  223. return self.name < other.name
  224. return NotImplemented
  225. @property
  226. def config(self):
  227. ''' Storage pool config '''
  228. if self._config is None:
  229. pool_info_data = self.app.qubesd_call(
  230. 'dom0', 'admin.pool.Info', self.name, None)
  231. pool_info_data = pool_info_data.decode('utf-8')
  232. assert pool_info_data.endswith('\n')
  233. pool_info_data = pool_info_data[:-1]
  234. self._config = dict(
  235. l.split('=', 1) for l in pool_info_data.splitlines())
  236. return self._config
  237. @property
  238. def size(self):
  239. ''' Storage pool size, in bytes'''
  240. try:
  241. return int(self.config['size'])
  242. except KeyError:
  243. # pool driver does not provide size information
  244. return None
  245. @property
  246. def usage(self):
  247. ''' Space used in the pool, in bytes '''
  248. try:
  249. return int(self.config['usage'])
  250. except KeyError:
  251. # pool driver does not provide usage information
  252. return None
  253. @property
  254. def driver(self):
  255. ''' Storage pool driver '''
  256. return self.config['driver']
  257. @property
  258. def revisions_to_keep(self):
  259. '''Number of revisions to keep around'''
  260. return int(self.config['revisions_to_keep'])
  261. @revisions_to_keep.setter
  262. def revisions_to_keep(self, value):
  263. '''Set revisions_to_keep property'''
  264. self.app.qubesd_call('dom0',
  265. 'admin.pool.Set.revisions_to_keep',
  266. self.name,
  267. str(value).encode('ascii'))
  268. self._config = None
  269. @property
  270. def volumes(self):
  271. ''' Volumes managed by this pool '''
  272. volumes_data = self.app.qubesd_call(
  273. 'dom0', 'admin.pool.volume.List', self.name, None)
  274. assert volumes_data.endswith(b'\n')
  275. volumes_data = volumes_data[:-1].decode('ascii')
  276. for vid in volumes_data.splitlines():
  277. yield Volume(self.app, self.name, vid)