Add boot from volume functionality to rax and rax_cbs modules
This commit is contained in:
parent
3aba0d5272
commit
1f51a5effd
2 changed files with 184 additions and 23 deletions
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue