Add boot from volume functionality to rax and rax_cbs modules

This commit is contained in:
Matt Martz 2014-11-28 12:22:12 -06:00 committed by Matt Clay
parent 3aba0d5272
commit 1f51a5effd
2 changed files with 184 additions and 23 deletions

View file

@ -35,6 +35,34 @@ options:
- "yes" - "yes"
- "no" - "no"
version_added: 1.5 version_added: 1.5
boot_from_volume:
description:
- Whether or not to boot the instance from a Cloud Block Storage volume.
If C(yes) and I(image) is specified a new volume will be created at
boot time. I(boot_volume_size) is required with I(image) to create a
new volume at boot time.
default: "no"
choices:
- "yes"
- "no"
version_added: 1.9
boot_volume:
description:
- Cloud Block Storage ID or Name to use as the boot volume of the
instance
version_added: 1.9
boot_volume_size:
description:
- Size of the volume to create in Gigabytes. This is only required with
I(image) and I(boot_from_volume).
default: 100
version_added: 1.9
boot_volume_terminate:
description:
- Whether the I(boot_volume) or newly created volume from I(image) will
be terminated when the server is terminated
default: false
version_added: 1.9
config_drive: config_drive:
description: description:
- Attach read-only configuration drive to server as label config-2 - Attach read-only configuration drive to server as label config-2
@ -99,7 +127,9 @@ options:
version_added: 1.4 version_added: 1.4
image: image:
description: description:
- image to use for the instance. Can be an C(id), C(human_id) or C(name) - image to use for the instance. Can be an C(id), C(human_id) or C(name).
With I(boot_from_volume), a Cloud Block Storage volume will be created
with this image
default: null default: null
instance_ids: instance_ids:
description: description:
@ -213,7 +243,7 @@ except ImportError:
def create(module, names=[], flavor=None, image=None, meta={}, key_name=None, def create(module, names=[], flavor=None, image=None, meta={}, key_name=None,
files={}, wait=True, wait_timeout=300, disk_config=None, files={}, wait=True, wait_timeout=300, disk_config=None,
group=None, nics=[], extra_create_args={}, user_data=None, group=None, nics=[], extra_create_args={}, user_data=None,
config_drive=False, existing=[]): config_drive=False, existing=[], block_device_mapping_v2=[]):
cs = pyrax.cloudservers cs = pyrax.cloudservers
changed = False changed = False
@ -239,6 +269,7 @@ def create(module, names=[], flavor=None, image=None, meta={}, key_name=None,
module.fail_json(msg='Failed to load %s' % lpath) module.fail_json(msg='Failed to load %s' % lpath)
try: try:
servers = [] servers = []
bdmv2 = block_device_mapping_v2
for name in names: for name in names:
servers.append(cs.servers.create(name=name, image=image, servers.append(cs.servers.create(name=name, image=image,
flavor=flavor, meta=meta, flavor=flavor, meta=meta,
@ -247,6 +278,7 @@ def create(module, names=[], flavor=None, image=None, meta={}, key_name=None,
disk_config=disk_config, disk_config=disk_config,
config_drive=config_drive, config_drive=config_drive,
userdata=user_data, userdata=user_data,
block_device_mapping_v2=bdmv2,
**extra_create_args)) **extra_create_args))
except Exception, e: except Exception, e:
module.fail_json(msg='%s' % e.message) module.fail_json(msg='%s' % e.message)
@ -394,7 +426,9 @@ def cloudservers(module, state=None, name=None, flavor=None, image=None,
disk_config=None, count=1, group=None, instance_ids=[], disk_config=None, count=1, group=None, instance_ids=[],
exact_count=False, networks=[], count_offset=0, exact_count=False, networks=[], count_offset=0,
auto_increment=False, extra_create_args={}, user_data=None, auto_increment=False, extra_create_args={}, user_data=None,
config_drive=False): config_drive=False, boot_from_volume=False,
boot_volume=None, boot_volume_size=None,
boot_volume_terminate=False):
cs = pyrax.cloudservers cs = pyrax.cloudservers
cnw = pyrax.cloud_networks cnw = pyrax.cloud_networks
if not cnw: if not cnw:
@ -402,6 +436,26 @@ def cloudservers(module, state=None, name=None, flavor=None, image=None,
'typically indicates an invalid region or an ' 'typically indicates an invalid region or an '
'incorrectly capitalized region name.') 'incorrectly capitalized region name.')
if state == 'present' or (state == 'absent' and instance_ids is None):
for arg, value in dict(name=name, flavor=flavor).iteritems():
if not value:
module.fail_json(msg='%s is required for the "rax" module' %
arg)
if not boot_from_volume and not boot_volume and not image:
module.fail_json(msg='image is required for the "rax" module')
if boot_from_volume and not image and not boot_volume:
module.fail_json(msg='image or boot_volume are required for the '
'"rax" with boot_from_volume')
if boot_from_volume and image and not boot_volume_size:
module.fail_json(msg='boot_volume_size is required for the "rax" '
'module with boot_from_volume and image')
if boot_from_volume and image and boot_volume:
image = None
servers = [] servers = []
# Add the group meta key # Add the group meta key
@ -438,12 +492,6 @@ def cloudservers(module, state=None, name=None, flavor=None, image=None,
# act on the state # act on the state
if state == 'present': if state == 'present':
for arg, value in dict(name=name, flavor=flavor,
image=image).iteritems():
if not value:
module.fail_json(msg='%s is required for the "rax" module' %
arg)
# Idempotent ensurance of a specific count of servers # Idempotent ensurance of a specific count of servers
if exact_count is not False: if exact_count is not False:
# See if we can find servers that match our options # See if we can find servers that match our options
@ -583,7 +631,6 @@ def cloudservers(module, state=None, name=None, flavor=None, image=None,
# Perform more simplistic matching # Perform more simplistic matching
search_opts = { search_opts = {
'name': '^%s$' % name, 'name': '^%s$' % name,
'image': image,
'flavor': flavor 'flavor': flavor
} }
servers = [] servers = []
@ -591,6 +638,36 @@ def cloudservers(module, state=None, name=None, flavor=None, image=None,
# Ignore DELETED servers # Ignore DELETED servers
if server.status == 'DELETED': if server.status == 'DELETED':
continue continue
if not image and boot_volume:
vol = rax_find_bootable_volume(module, pyrax, server,
exit=False)
if not vol:
continue
volume_image_metadata = vol.volume_image_metadata
vol_image_id = volume_image_metadata.get('image_id')
if vol_image_id:
server_image = rax_find_image(module, pyrax,
vol_image_id, exit=False)
if server_image:
server.image = dict(id=server_image)
# Match image IDs taking care of boot from volume
if image and not server.image:
vol = rax_find_bootable_volume(module, pyrax, server)
volume_image_metadata = vol.volume_image_metadata
vol_image_id = volume_image_metadata.get('image_id')
if not vol_image_id:
continue
server_image = rax_find_image(module, pyrax,
vol_image_id, exit=False)
if image != server_image:
continue
server.image = dict(id=server_image)
elif image and server.image['id'] != image:
continue
# Ignore servers with non matching metadata # Ignore servers with non matching metadata
if server.metadata != meta: if server.metadata != meta:
continue continue
@ -616,34 +693,85 @@ def cloudservers(module, state=None, name=None, flavor=None, image=None,
# them, we aren't performing auto_increment here # them, we aren't performing auto_increment here
names = [name] * (count - len(servers)) names = [name] * (count - len(servers))
block_device_mapping_v2 = []
if boot_from_volume:
mapping = {
'boot_index': '0',
'delete_on_termination': boot_volume_terminate,
'destination_type': 'volume',
}
if image:
if boot_volume_size < 100:
module.fail_json(msg='"boot_volume_size" must be greater '
'than or equal to 100')
mapping.update({
'uuid': image,
'source_type': 'image',
'volume_size': boot_volume_size,
})
image = None
elif boot_volume:
volume = rax_find_volume(module, pyrax, boot_volume)
mapping.update({
'uuid': pyrax.utils.get_id(volume),
'source_type': 'volume',
})
block_device_mapping_v2.append(mapping)
create(module, names=names, flavor=flavor, image=image, create(module, names=names, flavor=flavor, image=image,
meta=meta, key_name=key_name, files=files, wait=wait, meta=meta, key_name=key_name, files=files, wait=wait,
wait_timeout=wait_timeout, disk_config=disk_config, group=group, wait_timeout=wait_timeout, disk_config=disk_config, group=group,
nics=nics, extra_create_args=extra_create_args, nics=nics, extra_create_args=extra_create_args,
user_data=user_data, config_drive=config_drive, user_data=user_data, config_drive=config_drive,
existing=servers) existing=servers,
block_device_mapping_v2=block_device_mapping_v2)
elif state == 'absent': elif state == 'absent':
if instance_ids is None: if instance_ids is None:
# We weren't given an explicit list of server IDs to delete # We weren't given an explicit list of server IDs to delete
# Let's match instead # Let's match instead
for arg, value in dict(name=name, flavor=flavor,
image=image).iteritems():
if not value:
module.fail_json(msg='%s is required for the "rax" '
'module' % arg)
search_opts = { search_opts = {
'name': '^%s$' % name, 'name': '^%s$' % name,
'image': image,
'flavor': flavor 'flavor': flavor
} }
for server in cs.servers.list(search_opts=search_opts): for server in cs.servers.list(search_opts=search_opts):
# Ignore DELETED servers # Ignore DELETED servers
if server.status == 'DELETED': if server.status == 'DELETED':
continue continue
if not image and boot_volume:
vol = rax_find_bootable_volume(module, pyrax, server,
exit=False)
if not vol:
continue
volume_image_metadata = vol.volume_image_metadata
vol_image_id = volume_image_metadata.get('image_id')
if vol_image_id:
server_image = rax_find_image(module, pyrax,
vol_image_id, exit=False)
if server_image:
server.image = dict(id=server_image)
# Match image IDs taking care of boot from volume
if image and not server.image:
vol = rax_find_bootable_volume(module, pyrax, server)
volume_image_metadata = vol.volume_image_metadata
vol_image_id = volume_image_metadata.get('image_id')
if not vol_image_id:
continue
server_image = rax_find_image(module, pyrax,
vol_image_id, exit=False)
if image != server_image:
continue
server.image = dict(id=server_image)
elif image and server.image['id'] != image:
continue
# Ignore servers with non matching metadata # Ignore servers with non matching metadata
if meta != server.metadata: if meta != server.metadata:
continue continue
servers.append(server) servers.append(server)
# Build a list of server IDs to delete # Build a list of server IDs to delete
@ -672,6 +800,10 @@ def main():
argument_spec.update( argument_spec.update(
dict( dict(
auto_increment=dict(default=True, type='bool'), auto_increment=dict(default=True, type='bool'),
boot_from_volume=dict(default=False, type='bool'),
boot_volume=dict(type='str'),
boot_volume_size=dict(type='int', default=100),
boot_volume_terminate=dict(type='bool', default=False),
config_drive=dict(default=False, type='bool'), config_drive=dict(default=False, type='bool'),
count=dict(default=1, type='int'), count=dict(default=1, type='int'),
count_offset=dict(default=1, type='int'), count_offset=dict(default=1, type='int'),
@ -712,6 +844,10 @@ def main():
'playbook pertaining to the "rax" module') 'playbook pertaining to the "rax" module')
auto_increment = module.params.get('auto_increment') auto_increment = module.params.get('auto_increment')
boot_from_volume = module.params.get('boot_from_volume')
boot_volume = module.params.get('boot_volume')
boot_volume_size = module.params.get('boot_volume_size')
boot_volume_terminate = module.params.get('boot_volume_terminate')
config_drive = module.params.get('config_drive') config_drive = module.params.get('config_drive')
count = module.params.get('count') count = module.params.get('count')
count_offset = module.params.get('count_offset') count_offset = module.params.get('count_offset')
@ -757,7 +893,9 @@ def main():
exact_count=exact_count, networks=networks, exact_count=exact_count, networks=networks,
count_offset=count_offset, auto_increment=auto_increment, count_offset=count_offset, auto_increment=auto_increment,
extra_create_args=extra_create_args, user_data=user_data, extra_create_args=extra_create_args, user_data=user_data,
config_drive=config_drive) config_drive=config_drive, boot_from_volume=boot_from_volume,
boot_volume=boot_volume, boot_volume_size=boot_volume_size,
boot_volume_terminate=boot_volume_terminate)
# import module snippets # import module snippets

View file

@ -28,6 +28,12 @@ options:
description: description:
- Description to give the volume being created - Description to give the volume being created
default: null default: null
image:
description:
- image to use for bootable volumes. Can be an C(id), C(human_id) or
C(name). This option requires C(pyrax>=1.9.3)
default: null
version_added: 1.9
meta: meta:
description: description:
- A hash of metadata to associate with the volume - A hash of metadata to associate with the volume
@ -99,6 +105,8 @@ EXAMPLES = '''
register: my_volume register: my_volume
''' '''
from distutils.version import LooseVersion
try: try:
import pyrax import pyrax
HAS_PYRAX = True HAS_PYRAX = True
@ -107,7 +115,8 @@ except ImportError:
def cloud_block_storage(module, state, name, description, meta, size, def cloud_block_storage(module, state, name, description, meta, size,
snapshot_id, volume_type, wait, wait_timeout): snapshot_id, volume_type, wait, wait_timeout,
image):
changed = False changed = False
volume = None volume = None
instance = {} instance = {}
@ -119,15 +128,26 @@ def cloud_block_storage(module, state, name, description, meta, size,
'typically indicates an invalid region or an ' 'typically indicates an invalid region or an '
'incorrectly capitalized region name.') 'incorrectly capitalized region name.')
if image:
# pyrax<1.9.3 did not have support for specifying an image when
# creating a volume which is required for bootable volumes
if LooseVersion(pyrax.version.version) < LooseVersion('1.9.3'):
module.fail_json(msg='Creating a bootable volume requires '
'pyrax>=1.9.3')
image = rax_find_image(module, pyrax, image)
volume = rax_find_volume(module, pyrax, name) volume = rax_find_volume(module, pyrax, name)
if state == 'present': if state == 'present':
if not volume: if not volume:
kwargs = dict()
if image:
kwargs['image'] = image
try: try:
volume = cbs.create(name, size=size, volume_type=volume_type, volume = cbs.create(name, size=size, volume_type=volume_type,
description=description, description=description,
metadata=meta, metadata=meta,
snapshot_id=snapshot_id) snapshot_id=snapshot_id, **kwargs)
changed = True changed = True
except Exception, e: except Exception, e:
module.fail_json(msg='%s' % e.message) module.fail_json(msg='%s' % e.message)
@ -168,7 +188,8 @@ def main():
argument_spec = rax_argument_spec() argument_spec = rax_argument_spec()
argument_spec.update( argument_spec.update(
dict( dict(
description=dict(), description=dict(type='str'),
image=dict(type='str'),
meta=dict(type='dict', default={}), meta=dict(type='dict', default={}),
name=dict(required=True), name=dict(required=True),
size=dict(type='int', default=100), size=dict(type='int', default=100),
@ -189,6 +210,7 @@ def main():
module.fail_json(msg='pyrax is required for this module') module.fail_json(msg='pyrax is required for this module')
description = module.params.get('description') description = module.params.get('description')
image = module.params.get('image')
meta = module.params.get('meta') meta = module.params.get('meta')
name = module.params.get('name') name = module.params.get('name')
size = module.params.get('size') size = module.params.get('size')
@ -201,11 +223,12 @@ def main():
setup_rax_module(module, pyrax) setup_rax_module(module, pyrax)
cloud_block_storage(module, state, name, description, meta, size, cloud_block_storage(module, state, name, description, meta, size,
snapshot_id, volume_type, wait, wait_timeout) snapshot_id, volume_type, wait, wait_timeout,
image)
# import module snippets # import module snippets
from ansible.module_utils.basic import * from ansible.module_utils.basic import *
from ansible.module_utils.rax import * from ansible.module_utils.rax import *
### invoke the module # invoke the module
main() main()