Updates based on community review.

* Changed 'config' from a list to a string so any valid zonecfg(1M) syntax is accepted.
* Made default state 'present'
* Added 'attached', 'detached' and 'configured' states to allow zones to be moved between hosts.
* Updated documentation and examples.
* Code tidy up and refactoring.
This commit is contained in:
Paul Markham 2015-07-03 14:28:28 +10:00
parent eb44a5b6b8
commit c5c3b8133f

View file

@ -1,6 +1,6 @@
#!/usr/bin/python #!/usr/bin/python
# (c) 2013, Paul Markham <pmarkham@netrefinery.com> # (c) 2015, Paul Markham <pmarkham@netrefinery.com>
# #
# This file is part of Ansible # This file is part of Ansible
# #
@ -37,13 +37,18 @@ options:
state: state:
required: true required: true
description: description:
- C(present), create the zone. - C(present), configure and install the zone.
- C(running), if the zone already exists, boot it, otherwise, create the zone - C(installed), synonym for C(present).
first, then boot it. - C(running), if the zone already exists, boot it, otherwise, configure and install
the zone first, then boot it.
- C(started), synonym for C(running). - C(started), synonym for C(running).
- C(stopped), shutdown a zone. - C(stopped), shutdown a zone.
- C(absent), destroy the zone. - C(absent), destroy the zone.
choices: ['present', 'started', 'running', 'stopped', 'absent'] - C(configured), configure the ready so that it's to be attached.
- C(attached), attach a zone, but do not boot it.
- C(detach), stop and detach a zone
choices: ['present', 'installed', 'started', 'running', 'stopped', 'absent', 'configured', 'attached', 'detached']
default: present
name: name:
description: description:
- Zone name. - Zone name.
@ -68,19 +73,25 @@ options:
config: config:
required: false required: false
description: description:
- 'The zonecfg configuration commands for this zone, separated by commas, e.g. - 'The zonecfg configuration commands for this zone. See zonecfg(1M) for the valid options
"set auto-boot=true,add net,set physical=bge0,set address=10.1.1.1,end" and syntax. Typically this is a list of options separated by semi-colons or new lines, e.g.
See the Solaris Systems Administrator guide for a list of all configuration commands "set auto-boot=true;add net;set physical=bge0;set address=10.1.1.1;end"'
that can be used.'
required: false required: false
default: null default: empty string
create_options: create_options:
required: false required: false
description: description:
- 'Extra options to the zonecfg create command. For example, this can be used to create a - 'Extra options to the zonecfg(1M) create command.'
Solaris 11 kernel zone'
required: false required: false
default: null default: empty string
attach_options:
required: false
description:
- 'Extra options to the zoneadm attach command. For example, this can be used to specify
whether a minimum or full update of packages is required and if any packages need to
be deleted. For valid values, see zoneadm(1M)'
required: false
default: empty string
timeout: timeout:
description: description:
- Timeout, in seconds, for zone to boot. - Timeout, in seconds, for zone to boot.
@ -89,15 +100,15 @@ options:
''' '''
EXAMPLES = ''' EXAMPLES = '''
# Create a zone, but don't boot it # Create and install a zone, but don't boot it
solaris_zone: name=zone1 state=present path=/zones/zone1 sparse=true root_password="Be9oX7OSwWoU." solaris_zone: name=zone1 state=present path=/zones/zone1 sparse=true root_password="Be9oX7OSwWoU."
config='set autoboot=true, add net, set physical=bge0, set address=10.1.1.1, end' config='set autoboot=true; add net; set physical=bge0; set address=10.1.1.1; end'
# Create a zone and boot it # Create and install a zone and boot it
solaris_zone: name=zone1 state=running path=/zones/zone1 root_password="Be9oX7OSwWoU." solaris_zone: name=zone1 state=running path=/zones/zone1 root_password="Be9oX7OSwWoU."
config='set autoboot=true, add net, set physical=bge0, set address=10.1.1.1, end' config='set autoboot=true; add net; set physical=bge0; set address=10.1.1.1; end'
# Boot an already created zone # Boot an already installed zone
solaris_zone: name=zone1 state=running solaris_zone: name=zone1 state=running
# Stop a zone # Stop a zone
@ -105,6 +116,16 @@ solaris_zone: name=zone1 state=stopped
# Destroy a zone # Destroy a zone
solaris_zone: name=zone1 state=absent solaris_zone: name=zone1 state=absent
# Detach a zone
solaris_zone: name=zone1 state=detached
# Configure a zone, ready to be attached
solaris_zone: name=zone1 state=configured path=/zones/zone1 root_password="Be9oX7OSwWoU."
config='set autoboot=true; add net; set physical=bge0; set address=10.1.1.1; end'
# Attach a zone
solaris_zone: name=zone1 state=attached attach_options='-u'
''' '''
class Zone(object): class Zone(object):
@ -120,45 +141,60 @@ class Zone(object):
self.timeout = self.module.params['timeout'] self.timeout = self.module.params['timeout']
self.config = self.module.params['config'] self.config = self.module.params['config']
self.create_options = self.module.params['create_options'] self.create_options = self.module.params['create_options']
self.attach_options = self.module.params['attach_options']
self.zoneadm_cmd = self.module.get_bin_path('zoneadm', True) self.zoneadm_cmd = self.module.get_bin_path('zoneadm', True)
self.zonecfg_cmd = self.module.get_bin_path('zonecfg', True) self.zonecfg_cmd = self.module.get_bin_path('zonecfg', True)
self.ssh_keygen_cmd = self.module.get_bin_path('ssh-keygen', True) self.ssh_keygen_cmd = self.module.get_bin_path('ssh-keygen', True)
def create(self): def configure(self):
if not self.path: if not self.path:
self.module.fail_json(msg='Missing required argument: path') self.module.fail_json(msg='Missing required argument: path')
t = tempfile.NamedTemporaryFile(delete = False) if not self.module.check_mode:
t = tempfile.NamedTemporaryFile(delete = False)
if self.sparse: if self.sparse:
t.write('create %s\n' % self.create_options) t.write('create %s\n' % self.create_options)
self.msg.append('creating sparse root zone') self.msg.append('creating sparse-root zone')
else: else:
t.write('create -b %s\n' % self.create_options) t.write('create -b %s\n' % self.create_options)
self.msg.append('creating whole root zone') self.msg.append('creating whole-root zone')
t.write('set zonepath=%s\n' % self.path) t.write('set zonepath=%s\n' % self.path)
t.write('%s\n' % self.config)
t.close()
if self.config: cmd = '%s -z %s -f %s' % (self.zonecfg_cmd, self.name, t.name)
for line in self.config: (rc, out, err) = self.module.run_command(cmd)
t.write('%s\n' % line) if rc != 0:
t.close() self.module.fail_json(msg='Failed to create zone. %s' % (out + err))
os.unlink(t.name)
cmd = '%s -z %s -f %s' % (self.zonecfg_cmd, self.name, t.name) self.changed = True
(rc, out, err) = self.module.run_command(cmd) self.msg.append('zone configured')
if rc != 0:
self.module.fail_json(msg='Failed to create zone. %s' % (out + err))
os.unlink(t.name)
cmd = '%s -z %s install' % (self.zoneadm_cmd, self.name) def install(self):
(rc, out, err) = self.module.run_command(cmd) if not self.module.check_mode:
if rc != 0: cmd = '%s -z %s install' % (self.zoneadm_cmd, self.name)
self.module.fail_json(msg='Failed to install zone. %s' % (out + err)) (rc, out, err) = self.module.run_command(cmd)
if rc != 0:
self.module.fail_json(msg='Failed to install zone. %s' % (out + err))
self.configure_sysid()
self.configure_password()
self.configure_ssh_keys()
self.changed = True
self.msg.append('zone installed')
self.configure_sysid() def uninstall(self):
self.configure_password() if self.is_installed():
self.configure_ssh_keys() if not self.module.check_mode:
cmd = '%s -z %s uninstall -F' % (self.zoneadm_cmd, self.name)
(rc, out, err) = self.module.run_command(cmd)
if rc != 0:
self.module.fail_json(msg='Failed to uninstall zone. %s' % (out + err))
self.changed = True
self.msg.append('zone uninstalled')
def configure_sysid(self): def configure_sysid(self):
if os.path.isfile('%s/root/etc/.UNCONFIGURED' % self.path): if os.path.isfile('%s/root/etc/.UNCONFIGURED' % self.path):
@ -220,39 +256,70 @@ class Zone(object):
f.close() f.close()
def boot(self): def boot(self):
cmd = '%s -z %s boot' % (self.zoneadm_cmd, self.name) if not self.module.check_mode:
(rc, out, err) = self.module.run_command(cmd) cmd = '%s -z %s boot' % (self.zoneadm_cmd, self.name)
if rc != 0: (rc, out, err) = self.module.run_command(cmd)
self.module.fail_json(msg='Failed to boot zone. %s' % (out + err)) if rc != 0:
self.module.fail_json(msg='Failed to boot zone. %s' % (out + err))
""" """
The boot command can return before the zone has fully booted. This is especially The boot command can return before the zone has fully booted. This is especially
true on the first boot when the zone initializes the SMF services. Unless the zone true on the first boot when the zone initializes the SMF services. Unless the zone
has fully booted, subsequent tasks in the playbook may fail as services aren't running yet. has fully booted, subsequent tasks in the playbook may fail as services aren't running yet.
Wait until the zone's console login is running; once that's running, consider the zone booted. Wait until the zone's console login is running; once that's running, consider the zone booted.
""" """
elapsed = 0 elapsed = 0
while True: while True:
if elapsed > self.timeout: if elapsed > self.timeout:
self.module.fail_json(msg='timed out waiting for zone to boot') self.module.fail_json(msg='timed out waiting for zone to boot')
rc = os.system('ps -z %s -o args|grep "/usr/lib/saf/ttymon.*-d /dev/console" > /dev/null 2>/dev/null' % self.name) rc = os.system('ps -z %s -o args|grep "/usr/lib/saf/ttymon.*-d /dev/console" > /dev/null 2>/dev/null' % self.name)
if rc == 0: if rc == 0:
break break
time.sleep(10) time.sleep(10)
elapsed += 10 elapsed += 10
self.changed = True
self.msg.append('zone booted')
def destroy(self): def destroy(self):
cmd = '%s -z %s delete -F' % (self.zonecfg_cmd, self.name) if self.is_running():
(rc, out, err) = self.module.run_command(cmd) self.stop()
if rc != 0: if self.is_installed():
self.module.fail_json(msg='Failed to delete zone. %s' % (out + err)) self.uninstall()
if not self.module.check_mode:
cmd = '%s -z %s delete -F' % (self.zonecfg_cmd, self.name)
(rc, out, err) = self.module.run_command(cmd)
if rc != 0:
self.module.fail_json(msg='Failed to delete zone. %s' % (out + err))
self.changed = True
self.msg.append('zone deleted')
def stop(self): def stop(self):
cmd = '%s -z %s halt' % (self.zoneadm_cmd, self.name) if not self.module.check_mode:
(rc, out, err) = self.module.run_command(cmd) cmd = '%s -z %s halt' % (self.zoneadm_cmd, self.name)
if rc != 0: (rc, out, err) = self.module.run_command(cmd)
self.module.fail_json(msg='Failed to stop zone. %s' % (out + err)) if rc != 0:
self.module.fail_json(msg='Failed to stop zone. %s' % (out + err))
self.changed = True
self.msg.append('zone stopped')
def detach(self):
if not self.module.check_mode:
cmd = '%s -z %s detach' % (self.zoneadm_cmd, self.name)
(rc, out, err) = self.module.run_command(cmd)
if rc != 0:
self.module.fail_json(msg='Failed to detach zone. %s' % (out + err))
self.changed = True
self.msg.append('zone detached')
def attach(self):
if not self.module.check_mode:
cmd = '%s -z %s attach %s' % (self.zoneadm_cmd, self.name, self.attach_options)
(rc, out, err) = self.module.run_command(cmd)
if rc != 0:
self.module.fail_json(msg='Failed to attach zone. %s' % (out + err))
self.changed = True
self.msg.append('zone attached')
def exists(self): def exists(self):
cmd = '%s -z %s list' % (self.zoneadm_cmd, self.name) cmd = '%s -z %s list' % (self.zoneadm_cmd, self.name)
@ -262,74 +329,85 @@ class Zone(object):
else: else:
return False return False
def running(self): def is_running(self):
return self.status() == 'running'
def is_installed(self):
return self.status() == 'installed'
def is_configured(self):
return self.status() == 'configured'
def status(self):
cmd = '%s -z %s list -p' % (self.zoneadm_cmd, self.name) cmd = '%s -z %s list -p' % (self.zoneadm_cmd, self.name)
(rc, out, err) = self.module.run_command(cmd) (rc, out, err) = self.module.run_command(cmd)
if rc != 0: if rc != 0:
self.module.fail_json(msg='Failed to determine zone state. %s' % (out + err)) self.module.fail_json(msg='Failed to determine zone state. %s' % (out + err))
return out.split(':')[2]
if out.split(':')[2] == 'running':
return True
else:
return False
def state_present(self): def state_present(self):
if self.exists(): if self.exists():
self.msg.append('zone already exists') self.msg.append('zone already exists')
else: else:
if not self.module.check_mode: self.configure()
self.create() self.install()
self.changed = True
self.msg.append('zone created')
def state_running(self): def state_running(self):
self.state_present() self.state_present()
if self.running(): if self.is_running():
self.msg.append('zone already running') self.msg.append('zone already running')
else: else:
if not self.module.check_mode: self.boot()
self.boot()
self.changed = True
self.msg.append('zone booted')
def state_stopped(self): def state_stopped(self):
if self.exists(): if self.exists():
if self.running(): self.stop()
if not self.module.check_mode:
self.stop()
self.changed = True
self.msg.append('zone stopped')
else:
self.msg.append('zone not running')
else: else:
self.module.fail_json(msg='zone does not exist') self.module.fail_json(msg='zone does not exist')
def state_absent(self): def state_absent(self):
if self.exists(): if self.exists():
self.state_stopped() if self.is_running():
if not self.module.check_mode: self.stop()
self.destroy() self.destroy()
self.changed = True
self.msg.append('zone deleted')
else: else:
self.msg.append('zone does not exist') self.msg.append('zone does not exist')
def exit_with_msg(self): def state_configured(self):
msg = ', '.join(self.msg) if self.exists():
self.module.exit_json(changed=self.changed, msg=msg) self.msg.append('zone already exists')
else:
self.configure()
def state_detached(self):
if not self.exists():
self.module.fail_json(msg='zone does not exist')
if self.is_configured():
self.msg.append('zone already detached')
else:
self.stop()
self.detach()
def state_attached(self):
if not self.exists():
self.msg.append('zone does not exist')
if self.is_configured():
self.attach()
else:
self.msg.append('zone already attached')
def main(): def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec = dict( argument_spec = dict(
name = dict(required=True), name = dict(required=True),
state = dict(required=True, choices=['running', 'started', 'present', 'stopped', 'absent']), state = dict(default='present', choices=['running', 'started', 'present', 'installed', 'stopped', 'absent', 'configured', 'detached', 'attached']),
path = dict(defalt=None), path = dict(defalt=None),
sparse = dict(default=False, type='bool'), sparse = dict(default=False, type='bool'),
root_password = dict(default=None), root_password = dict(default=None),
timeout = dict(default=600, type='int'), timeout = dict(default=600, type='int'),
config = dict(default=None, type='list'), config = dict(default=''),
create_options = dict(default=''), create_options = dict(default=''),
attach_options = dict(default=''),
), ),
supports_check_mode=True supports_check_mode=True
) )
@ -347,16 +425,22 @@ def main():
if state == 'running' or state == 'started': if state == 'running' or state == 'started':
zone.state_running() zone.state_running()
elif state == 'present': elif state == 'present' or state == 'installed':
zone.state_present() zone.state_present()
elif state == 'stopped': elif state == 'stopped':
zone.state_stopped() zone.state_stopped()
elif state == 'absent': elif state == 'absent':
zone.state_absent() zone.state_absent()
elif state == 'configured':
zone.state_configured()
elif state == 'detached':
zone.state_detached()
elif state == 'attached':
zone.state_attached()
else: else:
module.fail_json(msg='Invalid state: %s' % state) module.fail_json(msg='Invalid state: %s' % state)
zone.exit_with_msg() module.exit_json(changed=zone.changed, msg=', '.join(zone.msg))
from ansible.module_utils.basic import * from ansible.module_utils.basic import *
main() main()