2013-05-23 04:25:05 +02:00
#!/usr/bin/python
#coding: utf-8 -*-
2014-01-28 17:20:36 +01:00
# (c) 2013, Benno Joy <benno@ansible.com>
2013-11-23 06:37:47 +01:00
# (c) 2013, John Dewey <john@dewey.ws>
2013-05-23 04:25:05 +02:00
#
# This module 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.
#
# This software 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 this software. If not, see <http://www.gnu.org/licenses/>.
try:
from novaclient.v1_1 import client as nova_client
2014-04-11 21:01:14 +02:00
from novaclient.v1_1 import floating_ips
2013-11-23 06:37:47 +01:00
from novaclient import exceptions
2014-04-11 21:01:14 +02:00
from novaclient import utils
2013-05-23 04:25:05 +02:00
import time
except ImportError:
print("failed=True msg='novaclient is required for this module'")
DOCUMENTATION = '''
---
module: nova_compute
2013-11-19 00:55:49 +01:00
version_added: "1.2"
2013-06-14 11:53:43 +02:00
short_description: Create/Delete VMs from OpenStack
2013-05-23 04:25:05 +02:00
description:
- Create or Remove virtual machines from Openstack.
options:
login_username:
description:
- login username to authenticate to keystone
required: true
default: admin
login_password:
description:
- Password of login user
required: true
2013-06-01 17:52:28 +02:00
default: 'yes'
2013-05-23 04:25:05 +02:00
login_tenant_name:
description:
- The tenant name of the login user
required: true
2013-06-01 17:52:28 +02:00
default: 'yes'
2013-05-23 04:25:05 +02:00
auth_url:
description:
- The keystone url for authentication
required: false
default: 'http://127.0.0.1:35357/v2.0/'
region_name:
description:
- Name of the region
required: false
default: None
state:
description:
- Indicate desired state of the resource
choices: ['present', 'absent']
default: present
name:
description:
2013-10-31 11:34:04 +01:00
- Name that has to be given to the instance
2013-05-23 04:25:05 +02:00
required: true
default: None
image_id:
description:
- The id of the image that has to be cloned
required: true
default: None
flavor_id:
description:
2013-06-14 11:53:43 +02:00
- The id of the flavor in which the new VM has to be created
2013-05-23 04:25:05 +02:00
required: false
default: 1
key_name:
description:
2013-06-14 11:53:43 +02:00
- The key pair name to be used when creating a VM
2013-05-23 04:25:05 +02:00
required: false
default: None
security_groups:
description:
2013-06-14 11:53:43 +02:00
- The name of the security group to which the VM should be added
2013-05-23 04:25:05 +02:00
required: false
default: None
nics:
description:
2013-11-23 06:37:47 +01:00
- A list of network id's to which the VM's interface should be attached
2013-05-23 04:25:05 +02:00
required: false
default: None
2014-04-11 21:01:14 +02:00
floating_ip:
description:
- list of key value pairs that determine how to assign, if specified, floating IPs. Either use an explicite list of valid floating IPs, list of floating IP pools to choose from, or auto-assign
required: false
default: None
2014-08-03 00:39:50 +02:00
availability_zone:
description:
- Name of the availability zone
required: false
default: None
2013-05-23 04:25:05 +02:00
meta:
description:
2013-06-14 11:53:43 +02:00
- A list of key value pairs that should be provided as a metadata to the new VM
2013-05-23 04:25:05 +02:00
required: false
default: None
wait:
description:
2013-06-14 11:53:43 +02:00
- If the module should wait for the VM to be created.
2013-05-23 04:25:05 +02:00
required: false
default: 'yes'
wait_for:
description:
2013-06-14 11:53:43 +02:00
- The amount of time the module should wait for the VM to get into active state
2013-05-23 04:25:05 +02:00
required: false
default: 180
2014-02-08 15:45:03 +01:00
user_data:
description:
- Opaque blob of data which is made available to the instance
required: false
default: None
2014-03-11 19:45:04 +01:00
version_added: "1.6"
2013-06-14 11:53:43 +02:00
requirements: ["novaclient"]
'''
EXAMPLES = '''
# Creates a new VM and attaches to a network and passes metadata to the instance
- nova_compute:
2013-05-23 04:25:05 +02:00
state: present
login_username: admin
login_password: admin
login_tenant_name: admin
name: vm1
image_id: 4f905f38-e52a-43d2-b6ec-754a13ffb529
key_name: ansible_key
wait_for: 200
flavor_id: 4
nics:
- net-id: 34605f38-e52a-25d2-b6ec-754a13ffb723
meta:
hostname: test1
2013-06-14 11:53:43 +02:00
group: uge_master
2014-04-11 21:01:14 +02:00
2014-08-03 00:39:50 +02:00
# Creates a new VM in HP Cloud AE1 region availability zone az2 and automatically assigns a floating IP
2014-04-11 21:01:14 +02:00
- name: launch a nova instance
hosts: localhost
tasks:
- name: launch an instance
nova_compute:
state: present
login_username: username
login_password: Equality7-2521
login_tenant_name: username-project1
name: vm1
auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/
region_name: region-b.geo-1
2014-08-03 00:39:50 +02:00
availability_zone: az2
2014-04-11 21:01:14 +02:00
image_id: 9302692b-b787-4b52-a3a6-daebb79cb498
key_name: test
wait_for: 200
flavor_id: 101
security_groups: default
floating_ip:
auto: True
# If one wants to specify a floating ip to use:
<snip>
floating_ip:
ips:
- 15.126.238.160
2013-05-23 04:25:05 +02:00
'''
2013-06-14 11:53:43 +02:00
2014-04-11 21:01:14 +02:00
2013-05-23 04:25:05 +02:00
def _delete_server(module, nova):
name = None
2013-10-10 08:14:30 +02:00
server_list = None
2013-05-23 04:25:05 +02:00
try:
2013-10-10 08:14:30 +02:00
server_list = nova.servers.list(True, {'name': module.params['name']})
if server_list:
server = [x for x in server_list if x.name == module.params['name']]
nova.servers.delete(server.pop())
2014-02-02 18:33:27 +01:00
except Exception, e:
2013-05-25 05:34:48 +02:00
module.fail_json( msg = "Error in deleting vm: %s" % e.message)
2013-05-23 04:25:05 +02:00
if module.params['wait'] == 'no':
2013-05-25 05:34:48 +02:00
module.exit_json(changed = True, result = "deleted")
2013-09-08 23:36:37 +02:00
expire = time.time() + int(module.params['wait_for'])
2013-05-23 04:25:05 +02:00
while time.time() < expire:
2013-05-31 14:09:00 +02:00
name = nova.servers.list(True, {'name': module.params['name']})
2013-05-25 05:34:48 +02:00
if not name:
module.exit_json(changed = True, result = "deleted")
time.sleep(5)
2013-05-23 04:25:05 +02:00
module.fail_json(msg = "Timed out waiting for server to get deleted, please check manually")
2014-08-03 00:05:42 +02:00
def _add_floating_ip_from_pool(module, nova, server, floating_ip_obj):
# empty dict and list
usable_floating_ips = {}
pools = []
# user specified
pools = module.params['floating_ip']['pools']
# get the list of all floating IPs. Mileage may
# vary according to Nova Compute configuration
# per cloud provider
all_floating_ips = floating_ip_obj.list()
# iterate through all pools of IP address. Empty
# string means all and is the default value
for pool in pools:
# temporary list per pool
pool_ips = []
# loop through all floating IPs
for f_ip in all_floating_ips:
# if not reserved and the correct pool, add
if f_ip.instance_id is None and (f_ip.pool == pool):
pool_ips.append(f_ip.ip)
# only need one
break
# if the list is empty, add for this pool
if not pool_ips:
try:
new_ip = nova.floating_ips.create(pool)
except Exception, e:
module.fail_json(msg = "Unable to create floating ip")
pool_ips.append(new_ip.ip)
# Add to the main list
usable_floating_ips[pool] = pool_ips
# finally, add ip(s) to instance for each pool
for pool in usable_floating_ips:
for ip in usable_floating_ips[pool]:
try:
server.add_floating_ip(ip)
# We only need to assign one ip - but there is an inherent
# race condition and some other cloud operation may have
# stolen an available floating ip
break
except Exception, e:
module.fail_json(msg = "Error attaching IP %s to instance %s: %s " % (ip, server.id, e.message))
def _add_floating_ip_no_pool(module, nova, server, floating_ip_obj):
usable_floating_ips = list()
# if there is a list of IP addresses, make that the list
if module.params['floating_ip'].has_key('ips'):
usable_floating_ips = module.params['floating_ip']['ips']
else:
# get the list of all floating IPs. Mileage may
# vary according to Nova Compute configuration
# per cloud provider
for f_ip in floating_ip_obj.list():
# if not reserved and the correct pool, add
if f_ip.instance_id is None:
usable_floating_ips.append(f_ip.ip)
if not usable_floating_ips:
try:
new_ip = nova.floating_ips.create()
except Exception, e:
module.fail_json(msg = "Unable to create floating ip")
usable_floating_ips.append(new_ip.ip)
# finally, add ip(s) to instance
for ip in usable_floating_ips:
try:
server.add_floating_ip(ip)
except Exception, e:
module.fail_json(msg = "Error attaching IP %s to instance %s: %s " % (ip, server.id, e.message))
def _add_floating_ip(module, nova, server):
# instantiate FloatingIPManager object
floating_ip_obj = floating_ips.FloatingIPManager(nova)
if module.params['floating_ip'].has_key('pools'):
_add_floating_ip_from_pool(module, nova, server, floating_ip_obj)
else:
_add_floating_ip_no_pool(module, nova, server, floating_ip_obj)
# this may look redundant, but if there is now a
# floating IP, then it needs to be obtained from
# a recent server object if the above code path exec'd
try:
server = nova.servers.get(server.id)
except Exception, e:
module.fail_json(msg = "Error in getting info from instance: %s " % e.message)
return server
2013-05-23 04:25:05 +02:00
def _create_server(module, nova):
2014-04-11 21:01:14 +02:00
# issue an error early on and not launch the instance
2014-08-03 00:05:42 +02:00
if module.params['floating_ip'] is not None:
2014-04-11 21:01:14 +02:00
if module.params['floating_ip'].has_key('ips'):
# can't specify "ips" and "auto" both
2014-08-03 00:05:42 +02:00
if module.params['floating_ip'].has_key('auto') and module.params['floating_ip']['auto']:
module.fail_json(msg = "For floating_ips - you cannot specify both 'auto' and 'ips'!")
2014-04-11 21:01:14 +02:00
# can't specify "ips" and "pools" both
if module.params['floating_ip'].has_key('pools'):
2014-08-03 00:05:42 +02:00
module.fail_json(msg = "For floating_ips - you cannot specify both 'pools' and 'ips'!")
2014-04-11 21:01:14 +02:00
2013-05-23 04:25:05 +02:00
bootargs = [module.params['name'], module.params['image_id'], module.params['flavor_id']]
bootkwargs = {
2013-05-25 05:34:48 +02:00
'nics' : module.params['nics'],
'meta' : module.params['meta'],
'security_groups': module.params['security_groups'].split(','),
2014-02-08 15:45:03 +01:00
#userdata is unhyphenated in novaclient, but hyphenated here for consistency with the ec2 module:
'userdata': module.params['user_data'],
2013-05-23 04:25:05 +02:00
}
2014-08-03 00:15:18 +02:00
2014-08-03 00:39:50 +02:00
for optional_param in ('region_name', 'key_name', 'availability_zone'):
2014-08-03 00:15:18 +02:00
if module.params[optional_param]:
bootkwargs[optional_param] = module.params[optional_param]
2013-05-23 04:25:05 +02:00
try:
2013-05-25 05:46:23 +02:00
server = nova.servers.create(*bootargs, **bootkwargs)
2013-05-25 05:34:48 +02:00
server = nova.servers.get(server.id)
2014-02-02 18:33:27 +01:00
except Exception, e:
2013-05-25 05:34:48 +02:00
module.fail_json( msg = "Error in creating instance: %s " % e.message)
2013-05-23 04:25:05 +02:00
if module.params['wait'] == 'yes':
2013-09-08 23:36:37 +02:00
expire = time.time() + int(module.params['wait_for'])
2013-05-25 05:34:48 +02:00
while time.time() < expire:
try:
2013-05-23 04:25:05 +02:00
server = nova.servers.get(server.id)
2014-02-02 18:33:27 +01:00
except Exception, e:
2014-08-03 00:05:42 +02:00
module.fail_json( msg = "Error in getting info from instance: %s" % e.message)
2013-05-25 05:34:48 +02:00
if server.status == 'ACTIVE':
2014-04-11 21:01:14 +02:00
# if floating_ip is specified, then attach
2014-08-03 00:05:42 +02:00
if module.params['floating_ip'] is not None:
server = _add_floating_ip(module, nova, server)
2014-04-11 21:01:14 +02:00
Fix traceback when openstack do not have OS-EXT-IPS:type attached to address
failed: [127.0.0.1] => {"failed": true, "parsed": false}
invalid output was: Traceback (most recent call last):
File "/tmp/ansible-1376083321.99-111209413777779/nova_compute", line 1176, in <module>
main()
File "/tmp/ansible-1376083321.99-111209413777779/nova_compute", line 239, in main
_get_server_state(module, nova)
File "/tmp/ansible-1376083321.99-111209413777779/nova_compute", line 198, in _get_server_state
private = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if x['OS-EXT-IPS:type'] == 'fixed']
KeyError: 'OS-EXT-IPS:type'
This extension was added less than 6 month ago, and so cannot be used on a release
older than Grizzly ( like Folsom ).
Commit of the extension : https://review.openstack.org/#/c/21453/
See https://wiki.openstack.org/wiki/ReleaseNotes/Grizzly#Key_New_Features_2
2013-08-09 23:25:15 +02:00
private = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if 'OS-EXT-IPS:type' in x and x['OS-EXT-IPS:type'] == 'fixed']
public = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if 'OS-EXT-IPS:type' in x and x['OS-EXT-IPS:type'] == 'floating']
2014-04-11 21:01:14 +02:00
# now exit with info
2013-05-25 05:34:48 +02:00
module.exit_json(changed = True, id = server.id, private_ip=''.join(private), public_ip=''.join(public), status = server.status, info = server._info)
2014-04-11 21:01:14 +02:00
2013-05-25 05:34:48 +02:00
if server.status == 'ERROR':
module.fail_json(msg = "Error in creating the server, please check logs")
time.sleep(2)
2013-11-23 06:37:47 +01:00
2013-05-25 05:34:48 +02:00
module.fail_json(msg = "Timeout waiting for the server to come up.. Please check manually")
2013-05-23 04:25:05 +02:00
if server.status == 'ERROR':
2013-05-25 05:34:48 +02:00
module.fail_json(msg = "Error in creating the server.. Please check manually")
2013-05-23 04:25:05 +02:00
private = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if x['OS-EXT-IPS:type'] == 'fixed']
public = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if x['OS-EXT-IPS:type'] == 'floating']
2014-04-11 21:01:14 +02:00
2013-05-23 04:25:05 +02:00
module.exit_json(changed = True, id = info['id'], private_ip=''.join(private), public_ip=''.join(public), status = server.status, info = server._info)
2013-11-23 06:37:47 +01:00
2013-05-23 04:25:05 +02:00
def _get_server_state(module, nova):
server = None
try:
2013-05-31 14:09:00 +02:00
servers = nova.servers.list(True, {'name': module.params['name']})
2013-05-25 05:34:48 +02:00
if servers:
2014-03-01 03:05:52 +01:00
# the {'name': module.params['name']} will also return servers
# with names that partially match the server name, so we have to
# strictly filter here
servers = [x for x in servers if x.name == module.params['name']]
2014-03-03 19:55:28 +01:00
if servers:
server = servers[0]
2014-02-02 18:33:27 +01:00
except Exception, e:
2013-05-25 05:34:48 +02:00
module.fail_json(msg = "Error in getting the server list: %s" % e.message)
2013-05-23 04:25:05 +02:00
if server and module.params['state'] == 'present':
2013-05-25 05:34:48 +02:00
if server.status != 'ACTIVE':
module.fail_json( msg="The VM is available but not Active. state:" + server.status)
Fix traceback when openstack do not have OS-EXT-IPS:type attached to address
failed: [127.0.0.1] => {"failed": true, "parsed": false}
invalid output was: Traceback (most recent call last):
File "/tmp/ansible-1376083321.99-111209413777779/nova_compute", line 1176, in <module>
main()
File "/tmp/ansible-1376083321.99-111209413777779/nova_compute", line 239, in main
_get_server_state(module, nova)
File "/tmp/ansible-1376083321.99-111209413777779/nova_compute", line 198, in _get_server_state
private = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if x['OS-EXT-IPS:type'] == 'fixed']
KeyError: 'OS-EXT-IPS:type'
This extension was added less than 6 month ago, and so cannot be used on a release
older than Grizzly ( like Folsom ).
Commit of the extension : https://review.openstack.org/#/c/21453/
See https://wiki.openstack.org/wiki/ReleaseNotes/Grizzly#Key_New_Features_2
2013-08-09 23:25:15 +02:00
private = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if 'OS-EXT-IPS:type' in x and x['OS-EXT-IPS:type'] == 'fixed']
public = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if 'OS-EXT-IPS:type' in x and x['OS-EXT-IPS:type'] == 'floating']
2013-11-23 06:37:47 +01:00
module.exit_json(changed = False, id = server.id, public_ip = ''.join(public), private_ip = ''.join(private), info = server._info)
2013-05-23 04:25:05 +02:00
if server and module.params['state'] == 'absent':
2013-05-25 05:34:48 +02:00
return True
2013-05-23 04:25:05 +02:00
if module.params['state'] == 'absent':
2013-05-25 05:34:48 +02:00
module.exit_json(changed = False, result = "not present")
2013-05-23 04:25:05 +02:00
return True
2013-11-23 06:37:47 +01:00
2013-05-23 04:25:05 +02:00
def main():
module = AnsibleModule(
2013-05-25 05:34:48 +02:00
argument_spec = dict(
login_username = dict(default='admin'),
login_password = dict(required=True),
login_tenant_name = dict(required='True'),
auth_url = dict(default='http://127.0.0.1:35357/v2.0/'),
region_name = dict(default=None),
name = dict(required=True),
2013-11-23 06:37:47 +01:00
image_id = dict(default=None),
2013-05-25 05:34:48 +02:00
flavor_id = dict(default=1),
key_name = dict(default=None),
security_groups = dict(default='default'),
nics = dict(default=None),
meta = dict(default=None),
wait = dict(default='yes', choices=['yes', 'no']),
2013-09-08 23:38:48 +02:00
wait_for = dict(default=180),
2014-02-08 15:45:03 +01:00
state = dict(default='present', choices=['absent', 'present']),
2014-04-11 21:01:14 +02:00
user_data = dict(default=None),
2014-08-03 00:39:50 +02:00
floating_ip = dict(default=None),
availability_zone = dict(default=None),
2013-05-23 04:25:05 +02:00
),
)
2013-11-23 06:37:47 +01:00
nova = nova_client.Client(module.params['login_username'],
module.params['login_password'],
module.params['login_tenant_name'],
module.params['auth_url'],
2014-03-20 14:07:49 +01:00
region_name=module.params['region_name'],
2013-11-23 06:37:47 +01:00
service_type='compute')
2013-05-23 04:25:05 +02:00
try:
2013-11-23 06:37:47 +01:00
nova.authenticate()
2014-03-04 17:30:15 +01:00
except exceptions.Unauthorized, e:
2013-11-23 06:37:47 +01:00
module.fail_json(msg = "Invalid OpenStack Nova credentials.: %s" % e.message)
2014-03-04 17:30:15 +01:00
except exceptions.AuthorizationFailure, e:
2013-11-23 06:37:47 +01:00
module.fail_json(msg = "Unable to authorize user: %s" % e.message)
2013-05-23 04:25:05 +02:00
if module.params['state'] == 'present':
2013-08-17 17:56:19 +02:00
if not module.params['image_id']:
module.fail_json( msg = "Parameter 'image_id' is required if state == 'present'")
else:
_get_server_state(module, nova)
_create_server(module, nova)
2013-05-23 04:25:05 +02:00
if module.params['state'] == 'absent':
2013-05-25 05:34:48 +02:00
_get_server_state(module, nova)
_delete_server(module, nova)
2013-11-23 06:37:47 +01:00
2013-05-23 04:25:05 +02:00
# this is magic, see lib/ansible/module.params['common.py
2013-12-02 21:11:23 +01:00
from ansible.module_utils.basic import *
2013-05-23 04:25:05 +02:00
main()