2020-08-29 19:10:59 +00:00
from collections import Counter
from pipes import quote
from bundlewrap . exceptions import BundleError
from bundlewrap . items import Item
from bundlewrap . utils . text import mark_for_translation as _
class ZFSPool ( Item ) :
"""
2021-07-30 13:44:34 +00:00
Creates ZFS pools .
2020-08-29 19:10:59 +00:00
"""
BUNDLE_ATTRIBUTE_NAME = " zfs_pools "
ITEM_ATTRIBUTES = {
2021-07-17 16:09:35 +00:00
' config ' : None ,
' autotrim ' : None ,
' autoreplace ' : None ,
' autoexpand ' : None ,
2021-07-30 13:44:34 +00:00
' ashift ' : None ,
2020-08-29 19:10:59 +00:00
}
ITEM_TYPE_NAME = " zfs_pool "
def __repr__ ( self ) :
2021-07-30 13:44:34 +00:00
return " <ZFSPool name: {} autoexpand: {} autoreplace: {} autotrim: {} ashift: {} config: {} > " . format (
2020-08-29 19:10:59 +00:00
self . name ,
2021-07-17 16:09:35 +00:00
self . attributes [ ' autoexpand ' ] ,
self . attributes [ ' autoreplace ' ] ,
self . attributes [ ' autotrim ' ] ,
2021-07-30 13:44:34 +00:00
self . attributes [ ' ashift ' ] ,
2021-07-17 16:09:35 +00:00
self . attributes [ ' config ' ] ,
2020-08-29 19:10:59 +00:00
)
def cdict ( self ) :
2021-07-17 16:09:35 +00:00
ret = { }
2021-07-30 13:44:34 +00:00
# ashift can only be set at pool creation, that's why it's missing
# here.
2021-07-17 16:09:35 +00:00
for i in { ' autoexpand ' , ' autoreplace ' , ' autotrim ' } :
if self . attributes . get ( i ) :
ret [ i ] = self . attributes [ i ]
return ret
2020-08-29 19:10:59 +00:00
@property
def devices_used ( self ) :
2021-07-17 16:09:35 +00:00
devices = set ( )
for option in self . attributes [ ' config ' ] :
for device in option [ ' devices ' ] :
devices . add ( device )
return sorted ( devices )
2020-08-29 19:10:59 +00:00
def fix ( self , status ) :
if status . must_be_created :
2021-07-17 16:09:35 +00:00
cmdline = [ ]
for option in self . attributes [ ' config ' ] :
if option . get ( ' type ' ) :
cmdline . append ( option [ ' type ' ] )
if option [ ' type ' ] == ' log ' and len ( option [ ' devices ' ] ) > 1 :
cmdline . append ( ' mirror ' )
2021-07-30 13:44:34 +00:00
2021-07-17 16:09:35 +00:00
for device in sorted ( option [ ' devices ' ] ) :
2021-07-30 13:44:34 +00:00
res = node . run ( " lsblk -rndo fstype {} " . format ( quote ( device ) ) )
detected = res . stdout . decode ( ' UTF-8 ' ) . strip ( )
if detected != " " :
raise BundleError ( _ ( " Node {} , ZFSPool {} : Device {} to be used for ZFS, but it is not empty! Has ' {} ' . " ) . format ( self . node . name , self . name , device , detected ) )
2021-07-17 16:09:35 +00:00
2021-07-30 13:44:34 +00:00
cmdline . append ( quote ( device ) )
2021-07-17 16:09:35 +00:00
2021-07-30 13:44:34 +00:00
options = set ( )
if self . attributes [ ' ashift ' ] :
options . add ( ' -o ashift= {} ' . format ( self . attributes [ ' ashift ' ] ) )
self . run ( ' zpool create {} {} {} ' . format (
' ' . join ( sorted ( options ) ) ,
quote ( self . name ) ,
' ' . join ( cmdline ) ,
) )
for attr in status . keys_to_fix :
state_str = ' on ' if status . cdict [ attr ] else ' off '
self . run ( ' zpool set {} = {} {} ' . format ( attr , state_str , quote ( self . name ) ) )
2020-08-29 19:10:59 +00:00
def sdict ( self ) :
2021-07-17 16:09:35 +00:00
status_result = self . run ( ' zpool list {} ' . format ( quote ( self . name ) ) , may_fail = True )
if status_result . return_code != 0 :
return { }
2021-07-30 13:44:34 +00:00
pool_status = { }
for line in self . run ( ' zpool get all -H -o all {} ' . format ( quote ( self . name ) ) , may_fail = True ) . stdout . decode ( ) . splitlines ( ) :
try :
pname , prop , value , source = line . split ( )
pool_status [ prop . strip ( ) ] = value . strip ( )
except ( IndexError , ValueError ) :
continue
2021-07-17 16:09:35 +00:00
return {
2021-07-30 13:44:34 +00:00
' autoexpand ' : ( pool_status . get ( ' autoexpand ' ) == ' on ' ) ,
' autoreplace ' : ( pool_status . get ( ' autoreplace ' ) == ' on ' ) ,
' autotrim ' : ( pool_status . get ( ' autotrim ' ) == ' on ' ) ,
2021-07-17 16:09:35 +00:00
}
2020-08-29 19:10:59 +00:00
def test ( self ) :
duplicate_devices = [
item for item , count in Counter ( self . devices_used ) . items ( ) if count > 1
]
if duplicate_devices :
raise BundleError ( _ (
" {item} on node {node} uses {devices} more than once as an underlying device "
) . format (
item = self . id ,
node = self . node . name ,
devices = _ ( " and " ) . join ( duplicate_devices ) ,
) )
# Have a look at all other ZFS pools on this node and check if
# multiple pools try to use the same device.
for item in self . node . items :
if (
item . ITEM_TYPE_NAME == " zfs_pool " and
item . name != self . name and
set ( item . devices_used ) . intersection ( set ( self . devices_used ) )
) :
raise BundleError ( _ (
" Both the ZFS pools {self} and {other} on node {node} "
" try to use {devices} as the underlying storage device "
) . format (
self = self . name ,
other = item . name ,
node = self . node . name ,
devices = _ ( " and " ) . join ( set ( item . devices_used ) . intersection ( set ( self . devices_used ) ) ) ,
) )
@classmethod
def validate_attributes ( cls , bundle , item_id , attributes ) :
2021-07-17 16:09:35 +00:00
if not isinstance ( attributes [ ' config ' ] , list ) :
2020-08-29 19:10:59 +00:00
raise BundleError ( _ (
2021-07-30 13:44:34 +00:00
" {item} on node {node} : option ' config ' must be a list "
2020-08-29 19:10:59 +00:00
) . format (
item = item_id ,
node = bundle . node . name ,
) )
2021-07-17 16:09:35 +00:00
for config in attributes [ ' config ' ] :
2021-07-30 13:44:34 +00:00
if config . get ( ' type ' , None ) not in { None , ' mirror ' , ' raidz ' , ' raidz2 ' , ' raidz3 ' , ' cache ' , ' log ' } :
2021-07-17 16:09:35 +00:00
raise BundleError ( _ (
" {item} on node {node} has invalid type ' {type} ' , "
" must be one of (unset), ' mirror ' , ' raidz ' , ' raidz2 ' , "
" ' raidz3 ' , ' cache ' , ' log ' "
) . format (
item = item_id ,
node = bundle . node . name ,
type = config [ ' type ' ] ,
) )
if not config . get ( ' devices ' , set ( ) ) :
raise BundleError ( _ (
" {item} on node {node} uses no devices! "
) . format (
item = item_id ,
node = bundle . node . name ,
) )
if config . get ( ' type ' ) == ' log ' :
if not 0 < len ( config [ ' devices ' ] ) < 3 :
raise BundleError ( _ (
" {item} on node {node} type ' log ' must use exactly "
" one or two devices "
) . format (
item = item_id ,
node = bundle . node . name ,
) )