1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3 4# Copyright: (c) 2016, Adam Števko <adam.stevko@gmail.com> 5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8__metaclass__ = type 9 10 11DOCUMENTATION = r''' 12--- 13module: beadm 14short_description: Manage ZFS boot environments on FreeBSD/Solaris/illumos systems. 15description: 16 - Create, delete or activate ZFS boot environments. 17 - Mount and unmount ZFS boot environments. 18author: Adam Števko (@xen0l) 19options: 20 name: 21 description: 22 - ZFS boot environment name. 23 type: str 24 required: True 25 aliases: [ "be" ] 26 snapshot: 27 description: 28 - If specified, the new boot environment will be cloned from the given 29 snapshot or inactive boot environment. 30 type: str 31 description: 32 description: 33 - Associate a description with a new boot environment. This option is 34 available only on Solarish platforms. 35 type: str 36 options: 37 description: 38 - Create the datasets for new BE with specific ZFS properties. 39 - Multiple options can be specified. 40 - This option is available only on Solarish platforms. 41 type: str 42 mountpoint: 43 description: 44 - Path where to mount the ZFS boot environment. 45 type: path 46 state: 47 description: 48 - Create or delete ZFS boot environment. 49 type: str 50 choices: [ absent, activated, mounted, present, unmounted ] 51 default: present 52 force: 53 description: 54 - Specifies if the unmount should be forced. 55 type: bool 56 default: false 57''' 58 59EXAMPLES = r''' 60- name: Create ZFS boot environment 61 community.general.beadm: 62 name: upgrade-be 63 state: present 64 65- name: Create ZFS boot environment from existing inactive boot environment 66 community.general.beadm: 67 name: upgrade-be 68 snapshot: be@old 69 state: present 70 71- name: Create ZFS boot environment with compression enabled and description "upgrade" 72 community.general.beadm: 73 name: upgrade-be 74 options: "compression=on" 75 description: upgrade 76 state: present 77 78- name: Delete ZFS boot environment 79 community.general.beadm: 80 name: old-be 81 state: absent 82 83- name: Mount ZFS boot environment on /tmp/be 84 community.general.beadm: 85 name: BE 86 mountpoint: /tmp/be 87 state: mounted 88 89- name: Unmount ZFS boot environment 90 community.general.beadm: 91 name: BE 92 state: unmounted 93 94- name: Activate ZFS boot environment 95 community.general.beadm: 96 name: upgrade-be 97 state: activated 98''' 99 100RETURN = r''' 101name: 102 description: BE name 103 returned: always 104 type: str 105 sample: pre-upgrade 106snapshot: 107 description: ZFS snapshot to create BE from 108 returned: always 109 type: str 110 sample: rpool/ROOT/oi-hipster@fresh 111description: 112 description: BE description 113 returned: always 114 type: str 115 sample: Upgrade from 9.0 to 10.0 116options: 117 description: BE additional options 118 returned: always 119 type: str 120 sample: compression=on 121mountpoint: 122 description: BE mountpoint 123 returned: always 124 type: str 125 sample: /mnt/be 126state: 127 description: state of the target 128 returned: always 129 type: str 130 sample: present 131force: 132 description: If forced action is wanted 133 returned: always 134 type: bool 135 sample: False 136''' 137 138import os 139import re 140from ansible.module_utils.basic import AnsibleModule 141 142 143class BE(object): 144 def __init__(self, module): 145 self.module = module 146 147 self.name = module.params['name'] 148 self.snapshot = module.params['snapshot'] 149 self.description = module.params['description'] 150 self.options = module.params['options'] 151 self.mountpoint = module.params['mountpoint'] 152 self.state = module.params['state'] 153 self.force = module.params['force'] 154 self.is_freebsd = os.uname()[0] == 'FreeBSD' 155 156 def _beadm_list(self): 157 cmd = [self.module.get_bin_path('beadm'), 'list', '-H'] 158 if '@' in self.name: 159 cmd.append('-s') 160 return self.module.run_command(cmd) 161 162 def _find_be_by_name(self, out): 163 if '@' in self.name: 164 for line in out.splitlines(): 165 if self.is_freebsd: 166 check = line.split() 167 if(check == []): 168 continue 169 full_name = check[0].split('/') 170 if(full_name == []): 171 continue 172 check[0] = full_name[len(full_name) - 1] 173 if check[0] == self.name: 174 return check 175 else: 176 check = line.split(';') 177 if check[0] == self.name: 178 return check 179 else: 180 for line in out.splitlines(): 181 if self.is_freebsd: 182 check = line.split() 183 if check[0] == self.name: 184 return check 185 else: 186 check = line.split(';') 187 if check[0] == self.name: 188 return check 189 return None 190 191 def exists(self): 192 (rc, out, dummy) = self._beadm_list() 193 194 if rc == 0: 195 if self._find_be_by_name(out): 196 return True 197 else: 198 return False 199 else: 200 return False 201 202 def is_activated(self): 203 (rc, out, dummy) = self._beadm_list() 204 205 if rc == 0: 206 line = self._find_be_by_name(out) 207 if line is None: 208 return False 209 if self.is_freebsd: 210 if 'R' in line[1]: 211 return True 212 else: 213 if 'R' in line[2]: 214 return True 215 216 return False 217 218 def activate_be(self): 219 cmd = [self.module.get_bin_path('beadm'), 'activate', self.name] 220 return self.module.run_command(cmd) 221 222 def create_be(self): 223 cmd = [self.module.get_bin_path('beadm'), 'create'] 224 225 if self.snapshot: 226 cmd.extend(['-e', self.snapshot]) 227 if not self.is_freebsd: 228 if self.description: 229 cmd.extend(['-d', self.description]) 230 if self.options: 231 cmd.extend(['-o', self.options]) 232 233 cmd.append(self.name) 234 235 return self.module.run_command(cmd) 236 237 def destroy_be(self): 238 cmd = [self.module.get_bin_path('beadm'), 'destroy', '-F', self.name] 239 return self.module.run_command(cmd) 240 241 def is_mounted(self): 242 (rc, out, dummy) = self._beadm_list() 243 244 if rc == 0: 245 line = self._find_be_by_name(out) 246 if line is None: 247 return False 248 if self.is_freebsd: 249 # On FreeBSD, we exclude currently mounted BE on /, as it is 250 # special and can be activated even if it is mounted. That is not 251 # possible with non-root BEs. 252 if line[2] != '-' and line[2] != '/': 253 return True 254 else: 255 if line[3]: 256 return True 257 258 return False 259 260 def mount_be(self): 261 cmd = [self.module.get_bin_path('beadm'), 'mount', self.name] 262 263 if self.mountpoint: 264 cmd.append(self.mountpoint) 265 266 return self.module.run_command(cmd) 267 268 def unmount_be(self): 269 cmd = [self.module.get_bin_path('beadm'), 'unmount'] 270 if self.force: 271 cmd.append('-f') 272 cmd.append(self.name) 273 274 return self.module.run_command(cmd) 275 276 277def main(): 278 module = AnsibleModule( 279 argument_spec=dict( 280 name=dict(type='str', required=True, aliases=['be']), 281 snapshot=dict(type='str'), 282 description=dict(type='str'), 283 options=dict(type='str'), 284 mountpoint=dict(type='path'), 285 state=dict(type='str', default='present', choices=['absent', 'activated', 'mounted', 'present', 'unmounted']), 286 force=dict(type='bool', default=False), 287 ), 288 supports_check_mode=True, 289 ) 290 291 be = BE(module) 292 293 rc = None 294 out = '' 295 err = '' 296 result = {} 297 result['name'] = be.name 298 result['state'] = be.state 299 300 if be.snapshot: 301 result['snapshot'] = be.snapshot 302 303 if be.description: 304 result['description'] = be.description 305 306 if be.options: 307 result['options'] = be.options 308 309 if be.mountpoint: 310 result['mountpoint'] = be.mountpoint 311 312 if be.state == 'absent': 313 # beadm on FreeBSD and Solarish systems differs in delete behaviour in 314 # that we are not allowed to delete activated BE on FreeBSD while on 315 # Solarish systems we cannot delete BE if it is mounted. We add mount 316 # check for both platforms as BE should be explicitly unmounted before 317 # being deleted. On FreeBSD, we also check if the BE is activated. 318 if be.exists(): 319 if not be.is_mounted(): 320 if module.check_mode: 321 module.exit_json(changed=True) 322 323 if be.is_freebsd: 324 if be.is_activated(): 325 module.fail_json(msg='Unable to remove active BE!') 326 327 (rc, out, err) = be.destroy_be() 328 329 if rc != 0: 330 module.fail_json(msg='Error while destroying BE: "%s"' % err, 331 name=be.name, 332 stderr=err, 333 rc=rc) 334 else: 335 module.fail_json(msg='Unable to remove BE as it is mounted!') 336 337 elif be.state == 'present': 338 if not be.exists(): 339 if module.check_mode: 340 module.exit_json(changed=True) 341 342 (rc, out, err) = be.create_be() 343 344 if rc != 0: 345 module.fail_json(msg='Error while creating BE: "%s"' % err, 346 name=be.name, 347 stderr=err, 348 rc=rc) 349 350 elif be.state == 'activated': 351 if not be.is_activated(): 352 if module.check_mode: 353 module.exit_json(changed=True) 354 355 # On FreeBSD, beadm is unable to activate mounted BEs, so we add 356 # an explicit check for that case. 357 if be.is_freebsd: 358 if be.is_mounted(): 359 module.fail_json(msg='Unable to activate mounted BE!') 360 361 (rc, out, err) = be.activate_be() 362 363 if rc != 0: 364 module.fail_json(msg='Error while activating BE: "%s"' % err, 365 name=be.name, 366 stderr=err, 367 rc=rc) 368 elif be.state == 'mounted': 369 if not be.is_mounted(): 370 if module.check_mode: 371 module.exit_json(changed=True) 372 373 (rc, out, err) = be.mount_be() 374 375 if rc != 0: 376 module.fail_json(msg='Error while mounting BE: "%s"' % err, 377 name=be.name, 378 stderr=err, 379 rc=rc) 380 381 elif be.state == 'unmounted': 382 if be.is_mounted(): 383 if module.check_mode: 384 module.exit_json(changed=True) 385 386 (rc, out, err) = be.unmount_be() 387 388 if rc != 0: 389 module.fail_json(msg='Error while unmounting BE: "%s"' % err, 390 name=be.name, 391 stderr=err, 392 rc=rc) 393 394 if rc is None: 395 result['changed'] = False 396 else: 397 result['changed'] = True 398 399 if out: 400 result['stdout'] = out 401 if err: 402 result['stderr'] = err 403 404 module.exit_json(**result) 405 406 407if __name__ == '__main__': 408 main() 409