From 5a79b5ab0dfe59763ac131c1a77fd10b1dfe00ac Mon Sep 17 00:00:00 2001 From: Paul Markham Date: Thu, 25 Jun 2015 12:46:35 +1000 Subject: [PATCH 1/9] Added zone.py module to manage Solaris zones --- system/zone.py | 353 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 system/zone.py diff --git a/system/zone.py b/system/zone.py new file mode 100644 index 00000000000..d24001c973f --- /dev/null +++ b/system/zone.py @@ -0,0 +1,353 @@ +#!/usr/bin/python + +# (c) 2013, Paul Markham +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import sys +import os +import platform +import tempfile + +DOCUMENTATION = ''' +--- +module: zone +short_description: Manage Solaris zones +description: + - Create, start, stop and delete Solaris zones. This module doesn't currently allow + changing of options for a zone that's already been created. +version_added: "1.5" +author: Paul Markham +requirements: + - Solaris 10 or later +options: + state: + required: true + description: + - C(present), create the zone. + C(running), if the zone already exists, boot it, otherwise, create the zone + first, then boot it. + C(stopped), shutdown a zone. + C(absent), destroy the zone. + choices: ['present', 'running', 'stopped', 'absent'] + name: + description: + - Zone name. + required: true + path: + description: + - The path where the zone will be created. This is required when the zone is created, but not + used otherwise. + required: false + default: null + whole_root: + description: + - Whether to create a whole root (C(true)) or sparse root (C(false)) zone. + required: false + default: true + root_password: + description: + - The password hash for the root account. If not specified, the zone's root account + will not have a password. + required: false + default: null + config: + required: false + description: + - 'The zonecfg configuration commands for this zone, separated by commas, e.g. + "set auto-boot=true,add net,set physical=bge0,set address=10.1.1.1,end" + See the Solaris Systems Administrator guide for a list of all configuration commands + that can be used.' + required: false + default: null + create_options: + required: false + description: + - 'Extra options to the zonecfg create command. For example, this can be used to create a + Solaris 11 kernel zone' + required: false + default: null + timeout: + description: + - Timeout, in seconds, for zone to boot. + required: false + default: 600 +''' + +EXAMPLES = ''' +# Create a zone, but don't boot it +zone: name=zone1 state=present path=/zones/zone1 whole_root=true root_password="Be9oX7OSwWoU." + config='set autoboot=true, add net, set physical=bge0, set address=10.1.1.1, end' + +# Create a zone and boot it +zone: name=zone1 state=running path=/zones/zone1 whole_root=true root_password="Be9oX7OSwWoU." + config='set autoboot=true, add net, set physical=bge0, set address=10.1.1.1, end' + +# Boot an already created zone +zone: name=zone1 state=running + +# Stop a zone +zone: name=zone1 state=stopped + +# Destroy a zone +zone: name=zone1 state=absent +''' + +class Zone(object): + def __init__(self, module): + self.changed = False + self.msg = [] + + self.module = module + self.path = self.module.params['path'] + self.name = self.module.params['name'] + self.whole_root = self.module.params['whole_root'] + self.root_password = self.module.params['root_password'] + self.timeout = self.module.params['timeout'] + self.config = self.module.params['config'] + self.create_options = self.module.params['create_options'] + + self.zoneadm_cmd = self.module.get_bin_path('zoneadm', True) + self.zonecfg_cmd = self.module.get_bin_path('zonecfg', True) + self.ssh_keygen_cmd = self.module.get_bin_path('ssh-keygen', True) + + def create(self): + if not self.path: + self.module.fail_json(msg='Missing required argument: path') + + t = tempfile.NamedTemporaryFile(delete = False) + + if self.whole_root: + t.write('create -b %s\n' % self.create_options) + self.msg.append('creating whole root zone') + else: + t.write('create %s\n' % self.create_options) + self.msg.append('creating sparse root zone') + + t.write('set zonepath=%s\n' % self.path) + + if self.config: + for line in self.config: + t.write('%s\n' % line) + t.close() + + cmd = '%s -z %s -f %s' % (self.zonecfg_cmd, self.name, t.name) + (rc, out, err) = self.module.run_command(cmd) + 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) + (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() + + def configure_sysid(self): + os.unlink('%s/root/etc/.UNCONFIGURED' % self.path) + + open('%s/root/noautoshutdown' % self.path, 'w').close() + + node = open('%s/root/etc/nodename' % self.path, 'w') + node.write(self.name) + node.close + + id = open('%s/root/etc/.sysIDtool.state' % self.path, 'w') + id.write('1 # System previously configured?\n') + id.write('1 # Bootparams succeeded?\n') + id.write('1 # System is on a network?\n') + id.write('1 # Extended network information gathered?\n') + id.write('0 # Autobinder succeeded?\n') + id.write('1 # Network has subnets?\n') + id.write('1 # root password prompted for?\n') + id.write('1 # locale and term prompted for?\n') + id.write('1 # security policy in place\n') + id.write('1 # NFSv4 domain configured\n') + id.write('0 # Auto Registration Configured\n') + id.write('vt100') + id.close() + + def configure_ssh_keys(self): + rsa_key_file = '%s/root/etc/ssh/ssh_host_rsa_key' % self.path + dsa_key_file = '%s/root/etc/ssh/ssh_host_dsa_key' % self.path + + if not os.path.isfile(rsa_key_file): + cmd = '%s -f %s -t rsa -N ""' % (self.ssh_keygen_cmd, rsa_key_file) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to create rsa key. %s' % (out + err)) + + if not os.path.isfile(dsa_key_file): + cmd = '%s -f %s -t dsa -N ""' % (self.ssh_keygen_cmd, dsa_key_file) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to create dsa key. %s' % (out + err)) + + def configure_password(self): + shadow = '%s/root/etc/shadow' % self.path + if self.root_password: + f = open(shadow, 'r') + lines = f.readlines() + f.close() + + for i in range(0, len(lines)): + fields = lines[i].split(':') + if fields[0] == 'root': + fields[1] = self.root_password + lines[i] = ':'.join(fields) + + f = open(shadow, 'w') + for line in lines: + f.write(line) + f.close() + + def boot(self): + cmd = '%s -z %s boot' % (self.zoneadm_cmd, self.name) + (rc, out, err) = self.module.run_command(cmd) + 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 + 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. + Wait until the zone's console login is running; once that's running, consider the zone booted. + """ + + elapsed = 0 + while True: + if elapsed > self.timeout: + 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) + if rc == 0: + break + time.sleep(10) + elapsed += 10 + + def destroy(self): + 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)) + + def stop(self): + cmd = '%s -z %s halt' % (self.zoneadm_cmd, self.name) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to stop zone. %s' % (out + err)) + + def exists(self): + cmd = '%s -z %s list' % (self.zoneadm_cmd, self.name) + (rc, out, err) = self.module.run_command(cmd) + if rc == 0: + return True + else: + return False + + def running(self): + cmd = '%s -z %s list -p' % (self.zoneadm_cmd, self.name) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to determine zone state. %s' % (out + err)) + + if out.split(':')[2] == 'running': + return True + else: + return False + + + def state_present(self): + if self.exists(): + self.msg.append('zone already exists') + else: + if not self.module.check_mode: + self.create() + self.changed = True + self.msg.append('zone created') + + def state_running(self): + self.state_present() + if self.running(): + self.msg.append('zone already running') + else: + if not self.module.check_mode: + self.boot() + self.changed = True + self.msg.append('zone booted') + + def state_stopped(self): + if self.exists(): + if self.running(): + if not self.module.check_mode: + self.stop() + self.changed = True + self.msg.append('zone stopped') + else: + self.msg.append('zone not running') + else: + self.module.fail_json(msg='zone does not exist') + + def state_absent(self): + if self.exists(): + self.state_stopped() + if not self.module.check_mode: + self.destroy() + self.changed = True + self.msg.append('zone deleted') + else: + self.msg.append('zone does not exist') + + def exit_with_msg(self): + msg = ', '.join(self.msg) + self.module.exit_json(changed=self.changed, msg=msg) + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True), + state = dict(required=True, choices=['running', 'present', 'stopped', 'absent']), + path = dict(defalt=None), + whole_root = dict(default=True, type='bool'), + root_password = dict(default=None), + timeout = dict(default=600, type='int'), + config = dict(default=None, type='list'), + create_options = dict(default=''), + ), + supports_check_mode=True + ) + + zone = Zone(module) + + state = module.params['state'] + + if state == 'running': + zone.state_running() + elif state == 'present': + zone.state_present() + elif state == 'stopped': + zone.state_stopped() + elif state == 'absent': + zone.state_absent() + else: + module.fail_json(msg='Invalid state: %s' % state) + + zone.exit_with_msg() + +from ansible.module_utils.basic import * +main() From 7c231a340a30dcea2668cf6f2b9c5c15f992ae29 Mon Sep 17 00:00:00 2001 From: Paul Markham Date: Fri, 26 Jun 2015 09:44:33 +1000 Subject: [PATCH 2/9] - Renamed module to solaris_zone.py - Updated 'version_added' - Updated description of 'state' to make each line a list item - Check that OS is Solaris --- system/{zone.py => solaris_zone.py} | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) rename system/{zone.py => solaris_zone.py} (93%) diff --git a/system/zone.py b/system/solaris_zone.py similarity index 93% rename from system/zone.py rename to system/solaris_zone.py index d24001c973f..be693bf1425 100644 --- a/system/zone.py +++ b/system/solaris_zone.py @@ -24,12 +24,12 @@ import tempfile DOCUMENTATION = ''' --- -module: zone +module: solaris_zone short_description: Manage Solaris zones description: - Create, start, stop and delete Solaris zones. This module doesn't currently allow changing of options for a zone that's already been created. -version_added: "1.5" +version_added: "2.0" author: Paul Markham requirements: - Solaris 10 or later @@ -38,10 +38,10 @@ options: required: true description: - C(present), create the zone. - C(running), if the zone already exists, boot it, otherwise, create the zone + - C(running), if the zone already exists, boot it, otherwise, create the zone first, then boot it. - C(stopped), shutdown a zone. - C(absent), destroy the zone. + - C(stopped), shutdown a zone. + - C(absent), destroy the zone. choices: ['present', 'running', 'stopped', 'absent'] name: description: @@ -89,21 +89,21 @@ options: EXAMPLES = ''' # Create a zone, but don't boot it -zone: name=zone1 state=present path=/zones/zone1 whole_root=true root_password="Be9oX7OSwWoU." +solaris_zone: name=zone1 state=present path=/zones/zone1 whole_root=true root_password="Be9oX7OSwWoU." config='set autoboot=true, add net, set physical=bge0, set address=10.1.1.1, end' # Create a zone and boot it -zone: name=zone1 state=running path=/zones/zone1 whole_root=true root_password="Be9oX7OSwWoU." +solaris_zone: name=zone1 state=running path=/zones/zone1 whole_root=true root_password="Be9oX7OSwWoU." config='set autoboot=true, add net, set physical=bge0, set address=10.1.1.1, end' # Boot an already created zone -zone: name=zone1 state=running +solaris_zone: name=zone1 state=running # Stop a zone -zone: name=zone1 state=stopped +solaris_zone: name=zone1 state=stopped # Destroy a zone -zone: name=zone1 state=absent +solaris_zone: name=zone1 state=absent ''' class Zone(object): @@ -332,6 +332,13 @@ def main(): supports_check_mode=True ) + if platform.system() == 'SunOS': + (major, minor) = platform.release().split('.') + if minor < 10: + module.fail_json(msg='This module requires Solaris 10 or later') + else: + module.fail_json(msg='This module requires Solaris') + zone = Zone(module) state = module.params['state'] From 3f395861a0543cfbe3cb138a7ecdbd24e6dffc5d Mon Sep 17 00:00:00 2001 From: Paul Markham Date: Mon, 29 Jun 2015 11:00:30 +1000 Subject: [PATCH 3/9] Changed 'whole_root' option to 'sparse'. Added state='started' as synonym for state='running'. --- system/solaris_zone.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/system/solaris_zone.py b/system/solaris_zone.py index be693bf1425..edd078de812 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -40,9 +40,10 @@ options: - C(present), create the zone. - C(running), if the zone already exists, boot it, otherwise, create the zone first, then boot it. + - C(started), synonym for C(running). - C(stopped), shutdown a zone. - C(absent), destroy the zone. - choices: ['present', 'running', 'stopped', 'absent'] + choices: ['present', 'started', 'running', 'stopped', 'absent'] name: description: - Zone name. @@ -53,11 +54,11 @@ options: used otherwise. required: false default: null - whole_root: + sparse: description: - - Whether to create a whole root (C(true)) or sparse root (C(false)) zone. + - Whether to create a sparse (C(true)) or whole root (C(false)) zone. required: false - default: true + default: false root_password: description: - The password hash for the root account. If not specified, the zone's root account @@ -89,11 +90,11 @@ options: EXAMPLES = ''' # Create a zone, but don't boot it -solaris_zone: name=zone1 state=present path=/zones/zone1 whole_root=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' # Create a zone and boot it -solaris_zone: name=zone1 state=running path=/zones/zone1 whole_root=true 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' # Boot an already created zone @@ -114,7 +115,7 @@ class Zone(object): self.module = module self.path = self.module.params['path'] self.name = self.module.params['name'] - self.whole_root = self.module.params['whole_root'] + self.sparse = self.module.params['sparse'] self.root_password = self.module.params['root_password'] self.timeout = self.module.params['timeout'] self.config = self.module.params['config'] @@ -130,12 +131,12 @@ class Zone(object): t = tempfile.NamedTemporaryFile(delete = False) - if self.whole_root: - t.write('create -b %s\n' % self.create_options) - self.msg.append('creating whole root zone') - else: + if self.sparse: t.write('create %s\n' % self.create_options) self.msg.append('creating sparse root zone') + else: + t.write('create -b %s\n' % self.create_options) + self.msg.append('creating whole root zone') t.write('set zonepath=%s\n' % self.path) @@ -321,9 +322,9 @@ def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), - state = dict(required=True, choices=['running', 'present', 'stopped', 'absent']), + state = dict(default=None, choices=['running', 'started', 'present', 'stopped', 'absent']), path = dict(defalt=None), - whole_root = dict(default=True, type='bool'), + sparse = dict(default=False, type='bool'), root_password = dict(default=None), timeout = dict(default=600, type='int'), config = dict(default=None, type='list'), @@ -343,7 +344,7 @@ def main(): state = module.params['state'] - if state == 'running': + if state == 'running' or state == 'started': zone.state_running() elif state == 'present': zone.state_present() From 219ec18b335d5e986a8fa2edf9b29e285083fc45 Mon Sep 17 00:00:00 2001 From: Paul Markham Date: Mon, 29 Jun 2015 11:19:36 +1000 Subject: [PATCH 4/9] Change state back to a required parameter --- system/solaris_zone.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/solaris_zone.py b/system/solaris_zone.py index edd078de812..bac448fcb8d 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -322,7 +322,7 @@ def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), - state = dict(default=None, choices=['running', 'started', 'present', 'stopped', 'absent']), + state = dict(required=True, choices=['running', 'started', 'present', 'stopped', 'absent']), path = dict(defalt=None), sparse = dict(default=False, type='bool'), root_password = dict(default=None), From eb44a5b6b88cfeaf1cfe1feb24ba616fff908b79 Mon Sep 17 00:00:00 2001 From: Paul Markham Date: Mon, 29 Jun 2015 12:43:14 +1000 Subject: [PATCH 5/9] Handle case where .UNFONFIGURE file isn't there --- system/solaris_zone.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/solaris_zone.py b/system/solaris_zone.py index bac448fcb8d..54ab86eee20 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -161,7 +161,8 @@ class Zone(object): self.configure_ssh_keys() def configure_sysid(self): - os.unlink('%s/root/etc/.UNCONFIGURED' % self.path) + if os.path.isfile('%s/root/etc/.UNCONFIGURED' % self.path): + os.unlink('%s/root/etc/.UNCONFIGURED' % self.path) open('%s/root/noautoshutdown' % self.path, 'w').close() From c5c3b8133f3a6afc2e449c7cbef43aa8a29e3275 Mon Sep 17 00:00:00 2001 From: Paul Markham Date: Fri, 3 Jul 2015 14:28:28 +1000 Subject: [PATCH 6/9] 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. --- system/solaris_zone.py | 306 ++++++++++++++++++++++++++--------------- 1 file changed, 195 insertions(+), 111 deletions(-) diff --git a/system/solaris_zone.py b/system/solaris_zone.py index 54ab86eee20..fd143e9dc01 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# (c) 2013, Paul Markham +# (c) 2015, Paul Markham # # This file is part of Ansible # @@ -37,13 +37,18 @@ options: state: required: true description: - - C(present), create the zone. - - C(running), if the zone already exists, boot it, otherwise, create the zone - first, then boot it. + - C(present), configure and install the zone. + - C(installed), synonym for C(present). + - 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(stopped), shutdown a 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: description: - Zone name. @@ -68,19 +73,25 @@ options: config: required: false description: - - 'The zonecfg configuration commands for this zone, separated by commas, e.g. - "set auto-boot=true,add net,set physical=bge0,set address=10.1.1.1,end" - See the Solaris Systems Administrator guide for a list of all configuration commands - that can be used.' + - 'The zonecfg configuration commands for this zone. See zonecfg(1M) for the valid options + and syntax. Typically this is a list of options separated by semi-colons or new lines, e.g. + "set auto-boot=true;add net;set physical=bge0;set address=10.1.1.1;end"' required: false - default: null + default: empty string create_options: required: false description: - - 'Extra options to the zonecfg create command. For example, this can be used to create a - Solaris 11 kernel zone' + - 'Extra options to the zonecfg(1M) create command.' 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: description: - Timeout, in seconds, for zone to boot. @@ -89,15 +100,15 @@ options: ''' 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." - 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." - 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 # Stop a zone @@ -105,6 +116,16 @@ solaris_zone: name=zone1 state=stopped # Destroy a zone 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): @@ -120,45 +141,60 @@ class Zone(object): self.timeout = self.module.params['timeout'] self.config = self.module.params['config'] 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.zonecfg_cmd = self.module.get_bin_path('zonecfg', True) self.ssh_keygen_cmd = self.module.get_bin_path('ssh-keygen', True) - def create(self): + def configure(self): if not self.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: - t.write('create %s\n' % self.create_options) - self.msg.append('creating sparse root zone') - else: - t.write('create -b %s\n' % self.create_options) - self.msg.append('creating whole root zone') + if self.sparse: + t.write('create %s\n' % self.create_options) + self.msg.append('creating sparse-root zone') + else: + t.write('create -b %s\n' % self.create_options) + 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: - for line in self.config: - t.write('%s\n' % line) - t.close() + cmd = '%s -z %s -f %s' % (self.zonecfg_cmd, self.name, t.name) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + 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) - (rc, out, err) = self.module.run_command(cmd) - if rc != 0: - self.module.fail_json(msg='Failed to create zone. %s' % (out + err)) - os.unlink(t.name) + self.changed = True + self.msg.append('zone configured') - cmd = '%s -z %s install' % (self.zoneadm_cmd, self.name) - (rc, out, err) = self.module.run_command(cmd) - if rc != 0: - self.module.fail_json(msg='Failed to install zone. %s' % (out + err)) + def install(self): + if not self.module.check_mode: + cmd = '%s -z %s install' % (self.zoneadm_cmd, self.name) + (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() - self.configure_password() - self.configure_ssh_keys() + def uninstall(self): + if self.is_installed(): + 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): if os.path.isfile('%s/root/etc/.UNCONFIGURED' % self.path): @@ -208,51 +244,82 @@ class Zone(object): lines = f.readlines() f.close() - for i in range(0, len(lines)): - fields = lines[i].split(':') + for i in range(0, len(lines)): + fields = lines[i].split(':') if fields[0] == 'root': - fields[1] = self.root_password + fields[1] = self.root_password lines[i] = ':'.join(fields) f = open(shadow, 'w') for line in lines: - f.write(line) + f.write(line) f.close() - + def boot(self): - cmd = '%s -z %s boot' % (self.zoneadm_cmd, self.name) - (rc, out, err) = self.module.run_command(cmd) - if rc != 0: - self.module.fail_json(msg='Failed to boot zone. %s' % (out + err)) + if not self.module.check_mode: + cmd = '%s -z %s boot' % (self.zoneadm_cmd, self.name) + (rc, out, err) = self.module.run_command(cmd) + 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 - 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. - Wait until the zone's console login is running; once that's running, consider the zone booted. - """ + """ + 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 + 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. + """ - elapsed = 0 - while True: - if elapsed > self.timeout: - 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) - if rc == 0: - break - time.sleep(10) - elapsed += 10 + elapsed = 0 + while True: + if elapsed > self.timeout: + 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) + if rc == 0: + break + time.sleep(10) + elapsed += 10 + self.changed = True + self.msg.append('zone booted') def destroy(self): - 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)) + if self.is_running(): + self.stop() + if self.is_installed(): + 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): - cmd = '%s -z %s halt' % (self.zoneadm_cmd, self.name) - (rc, out, err) = self.module.run_command(cmd) - if rc != 0: - self.module.fail_json(msg='Failed to stop zone. %s' % (out + err)) + if not self.module.check_mode: + cmd = '%s -z %s halt' % (self.zoneadm_cmd, self.name) + (rc, out, err) = self.module.run_command(cmd) + 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): cmd = '%s -z %s list' % (self.zoneadm_cmd, self.name) @@ -262,74 +329,85 @@ class Zone(object): else: 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) (rc, out, err) = self.module.run_command(cmd) if rc != 0: self.module.fail_json(msg='Failed to determine zone state. %s' % (out + err)) - - if out.split(':')[2] == 'running': - return True - else: - return False - + return out.split(':')[2] def state_present(self): if self.exists(): self.msg.append('zone already exists') else: - if not self.module.check_mode: - self.create() - self.changed = True - self.msg.append('zone created') + self.configure() + self.install() def state_running(self): self.state_present() - if self.running(): + if self.is_running(): self.msg.append('zone already running') else: - if not self.module.check_mode: - self.boot() - self.changed = True - self.msg.append('zone booted') + self.boot() def state_stopped(self): if self.exists(): - if self.running(): - if not self.module.check_mode: - self.stop() - self.changed = True - self.msg.append('zone stopped') - else: - self.msg.append('zone not running') + self.stop() else: self.module.fail_json(msg='zone does not exist') def state_absent(self): if self.exists(): - self.state_stopped() - if not self.module.check_mode: - self.destroy() - self.changed = True - self.msg.append('zone deleted') + if self.is_running(): + self.stop() + self.destroy() else: self.msg.append('zone does not exist') - def exit_with_msg(self): - msg = ', '.join(self.msg) - self.module.exit_json(changed=self.changed, msg=msg) + def state_configured(self): + if self.exists(): + 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(): module = AnsibleModule( argument_spec = dict( 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), sparse = dict(default=False, type='bool'), root_password = dict(default=None), timeout = dict(default=600, type='int'), - config = dict(default=None, type='list'), + config = dict(default=''), create_options = dict(default=''), + attach_options = dict(default=''), ), supports_check_mode=True ) @@ -342,21 +420,27 @@ def main(): module.fail_json(msg='This module requires Solaris') zone = Zone(module) - + state = module.params['state'] if state == 'running' or state == 'started': zone.state_running() - elif state == 'present': + elif state == 'present' or state == 'installed': zone.state_present() elif state == 'stopped': zone.state_stopped() elif state == 'absent': zone.state_absent() + elif state == 'configured': + zone.state_configured() + elif state == 'detached': + zone.state_detached() + elif state == 'attached': + zone.state_attached() else: 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 * main() From d46810fb5eb407c7050f2837355d8d3b7f3cbd9f Mon Sep 17 00:00:00 2001 From: Paul Markham Date: Sat, 4 Jul 2015 11:10:30 +1000 Subject: [PATCH 7/9] Documentation fixes --- system/solaris_zone.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/solaris_zone.py b/system/solaris_zone.py index fd143e9dc01..0f064f9efc0 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -32,7 +32,7 @@ description: version_added: "2.0" author: Paul Markham requirements: - - Solaris 10 or later + - Solaris 10 options: state: required: true @@ -46,7 +46,7 @@ options: - C(absent), destroy the zone. - 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 + - C(detached), shutdown and detach a zone choices: ['present', 'installed', 'started', 'running', 'stopped', 'absent', 'configured', 'attached', 'detached'] default: present name: From 69f330ff970b7e9972565997b9423661b3a634cd Mon Sep 17 00:00:00 2001 From: Paul Markham Date: Fri, 10 Jul 2015 13:15:47 +1000 Subject: [PATCH 8/9] Updates for Solaris 11 --- system/solaris_zone.py | 83 ++++++++++++++++++++++++------------------ 1 file changed, 48 insertions(+), 35 deletions(-) diff --git a/system/solaris_zone.py b/system/solaris_zone.py index 0f064f9efc0..d1277366e2a 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -32,7 +32,7 @@ description: version_added: "2.0" author: Paul Markham requirements: - - Solaris 10 + - Solaris 10 or 11 options: state: required: true @@ -84,6 +84,12 @@ options: - 'Extra options to the zonecfg(1M) create command.' required: false default: empty string + install_options: + required: false + description: + - 'Extra options to the zoneadm(1M) install command. To automate Solaris 11 zone creation, + use this to specify the profile XML file, e.g. install_options="-c sc_profile.xml"' + required: false attach_options: required: false description: @@ -130,23 +136,34 @@ solaris_zone: name=zone1 state=attached attach_options='-u' class Zone(object): def __init__(self, module): - self.changed = False - self.msg = [] + self.changed = False + self.msg = [] - self.module = module - self.path = self.module.params['path'] - self.name = self.module.params['name'] - self.sparse = self.module.params['sparse'] - self.root_password = self.module.params['root_password'] - self.timeout = self.module.params['timeout'] - self.config = self.module.params['config'] - self.create_options = self.module.params['create_options'] - self.attach_options = self.module.params['attach_options'] + self.module = module + self.path = self.module.params['path'] + self.name = self.module.params['name'] + self.sparse = self.module.params['sparse'] + self.root_password = self.module.params['root_password'] + self.timeout = self.module.params['timeout'] + self.config = self.module.params['config'] + self.create_options = self.module.params['create_options'] + self.install_options = self.module.params['install_options'] + self.attach_options = self.module.params['attach_options'] self.zoneadm_cmd = self.module.get_bin_path('zoneadm', True) self.zonecfg_cmd = self.module.get_bin_path('zonecfg', True) self.ssh_keygen_cmd = self.module.get_bin_path('ssh-keygen', True) + if self.module.check_mode: + self.msg.append('Running in check mode') + + if platform.system() != 'SunOS': + self.module.fail_json(msg='This module requires Solaris') + + (self.os_major, self.os_minor) = platform.release().split('.') + if int(self.os_minor) < 10: + self.module.fail_json(msg='This module requires Solaris 10 or later') + def configure(self): if not self.path: self.module.fail_json(msg='Missing required argument: path') @@ -176,11 +193,12 @@ class Zone(object): def install(self): if not self.module.check_mode: - cmd = '%s -z %s install' % (self.zoneadm_cmd, self.name) + cmd = '%s -z %s install %s' % (self.zoneadm_cmd, self.name, self.install_options) (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() + if int(self.os_minor) == 10: + self.configure_sysid() self.configure_password() self.configure_ssh_keys() self.changed = True @@ -273,7 +291,7 @@ class Zone(object): while True: if elapsed > self.timeout: 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 "ttymon.*-d /dev/console" > /dev/null 2>/dev/null' % self.name) if rc == 0: break time.sleep(10) @@ -341,9 +359,10 @@ class Zone(object): def status(self): cmd = '%s -z %s list -p' % (self.zoneadm_cmd, self.name) (rc, out, err) = self.module.run_command(cmd) - if rc != 0: - self.module.fail_json(msg='Failed to determine zone state. %s' % (out + err)) - return out.split(':')[2] + if rc == 0: + return out.split(':')[2] + else: + return 'undefined' def state_present(self): if self.exists(): @@ -398,27 +417,21 @@ class Zone(object): def main(): module = AnsibleModule( - argument_spec = dict( - name = dict(required=True), - state = dict(default='present', choices=['running', 'started', 'present', 'installed', 'stopped', 'absent', 'configured', 'detached', 'attached']), - path = dict(defalt=None), - sparse = dict(default=False, type='bool'), - root_password = dict(default=None), - timeout = dict(default=600, type='int'), - config = dict(default=''), - create_options = dict(default=''), - attach_options = dict(default=''), + argument_spec = dict( + name = dict(required=True), + state = dict(default='present', choices=['running', 'started', 'present', 'installed', 'stopped', 'absent', 'configured', 'detached', 'attached']), + path = dict(defalt=None), + sparse = dict(default=False, type='bool'), + root_password = dict(default=None), + timeout = dict(default=600, type='int'), + config = dict(default=''), + create_options = dict(default=''), + install_options = dict(default=''), + attach_options = dict(default=''), ), supports_check_mode=True ) - if platform.system() == 'SunOS': - (major, minor) = platform.release().split('.') - if minor < 10: - module.fail_json(msg='This module requires Solaris 10 or later') - else: - module.fail_json(msg='This module requires Solaris') - zone = Zone(module) state = module.params['state'] From 80d959f9373dd1095a43a51f1fb0ac0fa905127b Mon Sep 17 00:00:00 2001 From: Paul Markham Date: Sat, 11 Jul 2015 10:08:51 +1000 Subject: [PATCH 9/9] Documentation fixes --- system/solaris_zone.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/system/solaris_zone.py b/system/solaris_zone.py index d1277366e2a..375196cb1e7 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -71,7 +71,6 @@ options: required: false default: null config: - required: false description: - 'The zonecfg configuration commands for this zone. See zonecfg(1M) for the valid options and syntax. Typically this is a list of options separated by semi-colons or new lines, e.g. @@ -79,19 +78,17 @@ options: required: false default: empty string create_options: - required: false description: - 'Extra options to the zonecfg(1M) create command.' required: false default: empty string install_options: - required: false description: - 'Extra options to the zoneadm(1M) install command. To automate Solaris 11 zone creation, use this to specify the profile XML file, e.g. install_options="-c sc_profile.xml"' required: false + 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