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