mirror of https://github.com/ansible/ansible.git
opennebula: new module one_host (#40041)
parent
4fd770f792
commit
44eaa2c007
@ -0,0 +1,352 @@
|
||||
#
|
||||
# Copyright 2018 www.privaz.io Valletech AB
|
||||
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
|
||||
import time
|
||||
import ssl
|
||||
from os import environ
|
||||
from ansible.module_utils.six import string_types
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
HAS_PYONE = True
|
||||
|
||||
try:
|
||||
import pyone
|
||||
from pyone import OneException
|
||||
from pyone.tester import OneServerTester
|
||||
except ImportError:
|
||||
OneException = Exception
|
||||
HAS_PYONE = False
|
||||
|
||||
|
||||
class OpenNebulaModule:
|
||||
"""
|
||||
Base class for all OpenNebula Ansible Modules.
|
||||
This is basically a wrapper of the common arguments, the pyone client and
|
||||
Some utility methods. It will also create a Test client if fixtures are
|
||||
to be replayed or recorded and manage that they are flush to disk when
|
||||
required.
|
||||
"""
|
||||
|
||||
common_args = dict(
|
||||
api_url=dict(type='str', aliases=['api_endpoint']),
|
||||
api_username=dict(type='str'),
|
||||
api_password=dict(type='str', no_log=True, aliases=['api_token']),
|
||||
validate_certs=dict(default=True, type='bool'),
|
||||
wait_timeout=dict(type='int', default=300),
|
||||
)
|
||||
|
||||
def __init__(self, argument_spec, supports_check_mode=False, mutually_exclusive=None):
|
||||
|
||||
module_args = OpenNebulaModule.common_args
|
||||
module_args.update(argument_spec)
|
||||
|
||||
self.module = AnsibleModule(argument_spec=module_args,
|
||||
supports_check_mode=supports_check_mode,
|
||||
mutually_exclusive=mutually_exclusive)
|
||||
self.result = dict(changed=False,
|
||||
original_message='',
|
||||
message='')
|
||||
self.one = self.create_one_client()
|
||||
|
||||
self.resolved_parameters = self.resolve_parameters()
|
||||
|
||||
def create_one_client(self):
|
||||
"""
|
||||
Creates an XMLPRC client to OpenNebula.
|
||||
Dependign on environment variables it will implement a test client.
|
||||
|
||||
Returns: the new xmlrpc client.
|
||||
|
||||
"""
|
||||
|
||||
test_fixture = (environ.get("ONE_TEST_FIXTURE", "False").lower() in ["1", "yes", "true"])
|
||||
test_fixture_file = environ.get("ONE_TEST_FIXTURE_FILE", "undefined")
|
||||
test_fixture_replay = (environ.get("ONE_TEST_FIXTURE_REPLAY", "True").lower() in ["1", "yes", "true"])
|
||||
test_fixture_unit = environ.get("ONE_TEST_FIXTURE_UNIT", "init")
|
||||
|
||||
# context required for not validating SSL, old python versions won't validate anyway.
|
||||
if hasattr(ssl, '_create_unverified_context'):
|
||||
no_ssl_validation_context = ssl._create_unverified_context()
|
||||
else:
|
||||
no_ssl_validation_context = None
|
||||
|
||||
# Check if the module can run
|
||||
if not HAS_PYONE:
|
||||
self.fail("pyone is required for this module")
|
||||
|
||||
if 'api_url' in self.module.params:
|
||||
url = self.module.params.get("api_url", environ.get("ONE_URL", False))
|
||||
else:
|
||||
self.fail("Either api_url or the environment variable ONE_URL must be provided")
|
||||
|
||||
if 'api_username' in self.module.params:
|
||||
username = self.module.params.get("api_username", environ.get("ONE_USERNAME", False))
|
||||
else:
|
||||
self.fail("Either api_username or the environment vairable ONE_USERNAME must be provided")
|
||||
|
||||
if 'api_password' in self.module.params:
|
||||
password = self.module.params.get("api_password", environ.get("ONE_PASSWORD", False))
|
||||
else:
|
||||
self.fail("Either api_password or the environment vairable ONE_PASSWORD must be provided")
|
||||
|
||||
session = "%s:%s" % (username, password)
|
||||
|
||||
if not test_fixture:
|
||||
if not self.module.params.get("validate_certs") and "PYTHONHTTPSVERIFY" not in environ:
|
||||
return pyone.OneServer(url, session=session, context=no_ssl_validation_context)
|
||||
else:
|
||||
return pyone.OneServer(url, session)
|
||||
else:
|
||||
if not self.module.params.get("validate_certs") and "PYTHONHTTPSVERIFY" not in environ:
|
||||
one = OneServerTester(url,
|
||||
fixture_file=test_fixture_file,
|
||||
fixture_replay=test_fixture_replay,
|
||||
session=session,
|
||||
context=no_ssl_validation_context)
|
||||
else:
|
||||
one = OneServerTester(url,
|
||||
fixture_file=test_fixture_file,
|
||||
fixture_replay=test_fixture_replay,
|
||||
session=session)
|
||||
one.set_fixture_unit_test(test_fixture_unit)
|
||||
return one
|
||||
|
||||
def close_one_client(self):
|
||||
"""
|
||||
Closing is only require in the event of fixture recording, as fixtures will be dumped to file
|
||||
"""
|
||||
if self.is_fixture_writing():
|
||||
self.one._close_fixtures()
|
||||
|
||||
def fail(self, msg):
|
||||
"""
|
||||
Utility failure method, will ensure fixtures are flushed before failing.
|
||||
Args:
|
||||
msg: human readable failure reason.
|
||||
"""
|
||||
if hasattr(self, 'one'):
|
||||
self.close_one_client()
|
||||
self.module.fail_json(msg=msg)
|
||||
|
||||
def exit(self):
|
||||
"""
|
||||
Utility exit method, will ensure fixtures are flushed before exiting.
|
||||
|
||||
"""
|
||||
if hasattr(self, 'one'):
|
||||
self.close_one_client()
|
||||
self.module.exit_json(**self.result)
|
||||
|
||||
def resolve_parameters(self):
|
||||
"""
|
||||
This method resolves parameters provided by a secondary ID to the primary ID.
|
||||
For example if cluster_name is present, cluster_id will be introduced by performing
|
||||
the required resolution
|
||||
|
||||
Returns: a copy of the parameters that includes the resolved parameters.
|
||||
|
||||
"""
|
||||
|
||||
resolved_params = dict(self.module.params)
|
||||
|
||||
if 'cluster_name' in self.module.params:
|
||||
clusters = self.one.clusterpool.info()
|
||||
for cluster in clusters.CLUSTER:
|
||||
if cluster.NAME == self.module.params.get('cluster_name'):
|
||||
resolved_params['cluster_id'] = cluster.ID
|
||||
|
||||
return resolved_params
|
||||
|
||||
def is_parameter(self, name):
|
||||
"""
|
||||
Utility method to check if a parameter was provided or is resolved
|
||||
Args:
|
||||
name: the parameter to check
|
||||
"""
|
||||
if name in self.resolved_parameters:
|
||||
return self.get_parameter(name) is not None
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_parameter(self, name):
|
||||
"""
|
||||
Utility method for accessing parameters that includes resolved ID
|
||||
parameters from provided Name parameters.
|
||||
"""
|
||||
return self.resolved_parameters.get(name)
|
||||
|
||||
def is_fixture_replay(self):
|
||||
"""
|
||||
Returns: true if we are currently running fixtures in replay mode.
|
||||
|
||||
"""
|
||||
return (environ.get("ONE_TEST_FIXTURE", "False").lower() in ["1", "yes", "true"]) and \
|
||||
(environ.get("ONE_TEST_FIXTURE_REPLAY", "True").lower() in ["1", "yes", "true"])
|
||||
|
||||
def is_fixture_writing(self):
|
||||
"""
|
||||
Returns: true if we are currently running fixtures in write mode.
|
||||
|
||||
"""
|
||||
return (environ.get("ONE_TEST_FIXTURE", "False").lower() in ["1", "yes", "true"]) and \
|
||||
(environ.get("ONE_TEST_FIXTURE_REPLAY", "True").lower() in ["0", "no", "false"])
|
||||
|
||||
def get_host_by_name(self, name):
|
||||
'''
|
||||
Returns a host given its name.
|
||||
Args:
|
||||
name: the name of the host
|
||||
|
||||
Returns: the host object or None if the host is absent.
|
||||
|
||||
'''
|
||||
hosts = self.one.hostpool.info()
|
||||
for h in hosts.HOST:
|
||||
if h.NAME == name:
|
||||
return h
|
||||
return None
|
||||
|
||||
def get_cluster_by_name(self, name):
|
||||
"""
|
||||
Returns a cluster given its name.
|
||||
Args:
|
||||
name: the name of the cluster
|
||||
|
||||
Returns: the cluster object or None if the host is absent.
|
||||
"""
|
||||
|
||||
clusters = self.one.clusterpool.info()
|
||||
for c in clusters.CLUSTER:
|
||||
if c.NAME == name:
|
||||
return c
|
||||
return None
|
||||
|
||||
def get_template_by_name(self, name):
|
||||
'''
|
||||
Returns a template given its name.
|
||||
Args:
|
||||
name: the name of the template
|
||||
|
||||
Returns: the template object or None if the host is absent.
|
||||
|
||||
'''
|
||||
templates = self.one.templatepool.info()
|
||||
for t in templates.TEMPLATE:
|
||||
if t.NAME == name:
|
||||
return t
|
||||
return None
|
||||
|
||||
def cast_template(self, template):
|
||||
"""
|
||||
OpenNebula handles all template elements as strings
|
||||
At some point there is a cast being performed on types provided by the user
|
||||
This function mimics that transformation so that required template updates are detected properly
|
||||
additionally an array will be converted to a comma separated list,
|
||||
which works for labels and hopefully for something more.
|
||||
|
||||
Args:
|
||||
template: the template to transform
|
||||
|
||||
Returns: the transformed template with data casts applied.
|
||||
"""
|
||||
|
||||
# TODO: check formally available data types in templates
|
||||
# TODO: some arrays might be converted to space separated
|
||||
|
||||
for key in template:
|
||||
value = template[key]
|
||||
if isinstance(value, dict):
|
||||
self.cast_template(template[key])
|
||||
elif isinstance(value, list):
|
||||
template[key] = ', '.join(value)
|
||||
elif not isinstance(value, string_types):
|
||||
template[key] = str(value)
|
||||
|
||||
def requires_template_update(self, current, desired):
|
||||
"""
|
||||
This function will help decide if a template update is required or not
|
||||
If a desired key is missing from the current dictionary an update is required
|
||||
If the intersection of both dictionaries is not deep equal, an update is required
|
||||
Args:
|
||||
current: current template as a dictionary
|
||||
desired: desired template as a dictionary
|
||||
|
||||
Returns: True if a template update is required
|
||||
"""
|
||||
|
||||
if not desired:
|
||||
return False
|
||||
|
||||
self.cast_template(desired)
|
||||
intersection = dict()
|
||||
for dkey in desired.keys():
|
||||
if dkey in current.keys():
|
||||
intersection[dkey] = current[dkey]
|
||||
else:
|
||||
return True
|
||||
return not (desired == intersection)
|
||||
|
||||
def wait_for_state(self, element_name, state, state_name, target_states,
|
||||
invalid_states=None, transition_states=None,
|
||||
wait_timeout=None):
|
||||
"""
|
||||
Args:
|
||||
element_name: the name of the object we are waiting for: HOST, VM, etc.
|
||||
state: lambda that returns the current state, will be queried until target state is reached
|
||||
state_name: lambda that returns the readable form of a given state
|
||||
target_states: states expected to be reached
|
||||
invalid_states: if any of this states is reached, fail
|
||||
transition_states: when used, these are the valid states during the transition.
|
||||
wait_timeout: timeout period in seconds. Defaults to the provided parameter.
|
||||
"""
|
||||
|
||||
if not wait_timeout:
|
||||
wait_timeout = self.module.params.get("wait_timeout")
|
||||
|
||||
if self.is_fixture_replay():
|
||||
sleep_time_ms = 0.01
|
||||
else:
|
||||
sleep_time_ms = 1
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
while (time.time() - start_time) < wait_timeout:
|
||||
current_state = state()
|
||||
|
||||
if current_state in invalid_states:
|
||||
self.fail('invalid %s state %s' % (element_name, state_name(current_state)))
|
||||
|
||||
if transition_states:
|
||||
if current_state not in transition_states:
|
||||
self.fail('invalid %s transition state %s' % (element_name, state_name(current_state)))
|
||||
|
||||
if current_state in target_states:
|
||||
return True
|
||||
|
||||
time.sleep(sleep_time_ms)
|
||||
|
||||
self.fail(msg="Wait timeout has expired!")
|
||||
|
||||
def run_module(self):
|
||||
"""
|
||||
trigger the start of the execution of the module.
|
||||
Returns:
|
||||
|
||||
"""
|
||||
try:
|
||||
self.run(self.one, self.module, self.result)
|
||||
except OneException as e:
|
||||
self.fail(msg="OpenNebula Exception: %s" % e)
|
||||
|
||||
def run(self, one, module, result):
|
||||
"""
|
||||
to be implemented by subclass with the actual module actions.
|
||||
Args:
|
||||
one: the OpenNebula XMLRPC client
|
||||
module: the Ansible Module object
|
||||
result: the Ansible result
|
||||
"""
|
||||
raise NotImplementedError("Method requires implementation")
|
@ -0,0 +1,280 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright 2018 www.privaz.io Valletech AB
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {
|
||||
'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'
|
||||
}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: one_host
|
||||
|
||||
short_description: Manages OpenNebula Hosts
|
||||
|
||||
version_added: "2.6"
|
||||
|
||||
requirements:
|
||||
- pyone
|
||||
|
||||
description:
|
||||
- "Manages OpenNebula Hosts"
|
||||
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Hostname of the machine to manage.
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- Takes the host to the desired lifecycle state.
|
||||
- If C(absent) the host will be deleted from the cluster.
|
||||
- If C(present) the host will be created in the cluster (includes C(enabled), C(disabled) and C(offline) states).
|
||||
- If C(enabled) the host is fully operational.
|
||||
- C(disabled), e.g. to perform maintenance operations.
|
||||
- C(offline), host is totally offline.
|
||||
choices:
|
||||
- absent
|
||||
- present
|
||||
- enabled
|
||||
- disabled
|
||||
- offline
|
||||
default: present
|
||||
im_mad_name:
|
||||
description:
|
||||
- The name of the information manager, this values are taken from the oned.conf with the tag name IM_MAD (name)
|
||||
default: kvm
|
||||
vmm_mad_name:
|
||||
description:
|
||||
- The name of the virtual machine manager mad name, this values are taken from the oned.conf with the tag name VM_MAD (name)
|
||||
default: kvm
|
||||
cluster_id:
|
||||
description:
|
||||
- The cluster ID.
|
||||
default: 0
|
||||
cluster_name:
|
||||
description:
|
||||
- The cluster specified by name.
|
||||
labels:
|
||||
description:
|
||||
- The labels for this host.
|
||||
template:
|
||||
description:
|
||||
- The template or attribute changes to merge into the host template.
|
||||
aliases:
|
||||
- attributes
|
||||
|
||||
extends_documentation_fragment: opennebula
|
||||
|
||||
author:
|
||||
- Rafael del Valle (@rvalle)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create a new host in OpenNebula
|
||||
one_host:
|
||||
name: host1
|
||||
cluster_id: 1
|
||||
api_url: http://127.0.0.1:2633/RPC2
|
||||
|
||||
- name: Create a host and adjust its template
|
||||
one_host:
|
||||
name: host2
|
||||
cluster_name: default
|
||||
template:
|
||||
LABELS:
|
||||
- gold
|
||||
- ssd
|
||||
RESERVED_CPU: -100
|
||||
'''
|
||||
|
||||
# TODO: pending setting guidelines on returned values
|
||||
RETURN = '''
|
||||
'''
|
||||
|
||||
# TODO: Documentation on valid state transitions is required to properly implement all valid cases
|
||||
# TODO: To be coherent with CLI this module should also provide "flush" functionality
|
||||
|
||||
from ansible.module_utils.opennebula import OpenNebulaModule
|
||||
|
||||
try:
|
||||
from pyone import HOST_STATES, HOST_STATUS
|
||||
except ImportError:
|
||||
pass # handled at module utils
|
||||
|
||||
|
||||
# Pseudo definitions...
|
||||
|
||||
HOST_ABSENT = -99 # the host is absent (special case defined by this module)
|
||||
|
||||
|
||||
class HostModule(OpenNebulaModule):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
argument_spec = dict(
|
||||
name=dict(type='str', required=True),
|
||||
state=dict(choices=['present', 'absent', 'enabled', 'disabled', 'offline'], default='present'),
|
||||
im_mad_name=dict(type='str', default="kvm"),
|
||||
vmm_mad_name=dict(type='str', default="kvm"),
|
||||
cluster_id=dict(type='int', default=0),
|
||||
cluster_name=dict(type='str'),
|
||||
labels=dict(type='list'),
|
||||
template=dict(type='dict', aliases=['attributes']),
|
||||
)
|
||||
|
||||
mutually_exclusive = [
|
||||
['cluster_id', 'cluster_name']
|
||||
]
|
||||
|
||||
OpenNebulaModule.__init__(self, argument_spec, mutually_exclusive=mutually_exclusive)
|
||||
|
||||
def allocate_host(self):
|
||||
"""
|
||||
Creates a host entry in OpenNebula
|
||||
Returns: True on success, fails otherwise.
|
||||
|
||||
"""
|
||||
if not self.one.host.allocate(self.get_parameter('name'),
|
||||
self.get_parameter('vmm_mad_name'),
|
||||
self.get_parameter('im_mad_name'),
|
||||
self.get_parameter('cluster_id')):
|
||||
self.fail(msg="could not allocate host")
|
||||
else:
|
||||
self.result['changed'] = True
|
||||
return True
|
||||
|
||||
def wait_for_host_state(self, host, target_states):
|
||||
"""
|
||||
Utility method that waits for a host state.
|
||||
Args:
|
||||
host:
|
||||
target_states:
|
||||
|
||||
"""
|
||||
return self.wait_for_state('host',
|
||||
lambda: self.one.host.info(host.ID).STATE,
|
||||
lambda s: HOST_STATES(s).name, target_states,
|
||||
invalid_states=[HOST_STATES.ERROR, HOST_STATES.MONITORING_ERROR])
|
||||
|
||||
def run(self, one, module, result):
|
||||
|
||||
# Get the list of hosts
|
||||
host_name = self.get_parameter("name")
|
||||
host = self.get_host_by_name(host_name)
|
||||
|
||||
# manage host state
|
||||
desired_state = self.get_parameter('state')
|
||||
if bool(host):
|
||||
current_state = host.STATE
|
||||
current_state_name = HOST_STATES(host.STATE).name
|
||||
else:
|
||||
current_state = HOST_ABSENT
|
||||
current_state_name = "ABSENT"
|
||||
|
||||
# apply properties
|
||||
if desired_state == 'present':
|
||||
if current_state == HOST_ABSENT:
|
||||
self.allocate_host()
|
||||
host = self.get_host_by_name(host_name)
|
||||
self.wait_for_host_state(host, [HOST_STATES.MONITORED])
|
||||
elif current_state in [HOST_STATES.ERROR, HOST_STATES.MONITORING_ERROR]:
|
||||
self.fail(msg="invalid host state %s" % current_state_name)
|
||||
|
||||
elif desired_state == 'enabled':
|
||||
if current_state == HOST_ABSENT:
|
||||
self.allocate_host()
|
||||
host = self.get_host_by_name(host_name)
|
||||
self.wait_for_host_state(host, [HOST_STATES.MONITORED])
|
||||
elif current_state in [HOST_STATES.DISABLED, HOST_STATES.OFFLINE]:
|
||||
if one.host.status(host.ID, HOST_STATUS.ENABLED):
|
||||
self.wait_for_host_state(host, [HOST_STATES.MONITORED])
|
||||
result['changed'] = True
|
||||
else:
|
||||
self.fail(msg="could not enable host")
|
||||
elif current_state in [HOST_STATES.MONITORED]:
|
||||
pass
|
||||
else:
|
||||
self.fail(msg="unknown host state %s, cowardly refusing to change state to enable" % current_state_name)
|
||||
|
||||
elif desired_state == 'disabled':
|
||||
if current_state == HOST_ABSENT:
|
||||
self.fail(msg='absent host cannot be put in disabled state')
|
||||
elif current_state in [HOST_STATES.MONITORED, HOST_STATES.OFFLINE]:
|
||||
if one.host.status(host.ID, HOST_STATUS.DISABLED):
|
||||
self.wait_for_host_state(host, [HOST_STATES.DISABLED])
|
||||
result['changed'] = True
|
||||
else:
|
||||
self.fail(msg="could not disable host")
|
||||
elif current_state in [HOST_STATES.DISABLED]:
|
||||
pass
|
||||
else:
|
||||
self.fail(msg="unknown host state %s, cowardly refusing to change state to disable" % current_state_name)
|
||||
|
||||
elif desired_state == 'offline':
|
||||
if current_state == HOST_ABSENT:
|
||||
self.fail(msg='absent host cannot be placed in offline state')
|
||||
elif current_state in [HOST_STATES.MONITORED, HOST_STATES.DISABLED]:
|
||||
if one.host.status(host.ID, HOST_STATUS.OFFLINE):
|
||||
self.wait_for_host_state(host, [HOST_STATES.OFFLINE])
|
||||
result['changed'] = True
|
||||
else:
|
||||
self.fail(msg="could not set host offline")
|
||||
elif current_state in [HOST_STATES.OFFLINE]:
|
||||
pass
|
||||
else:
|
||||
self.fail(msg="unknown host state %s, cowardly refusing to change state to offline" % current_state_name)
|
||||
|
||||
elif desired_state == 'absent':
|
||||
if current_state != HOST_ABSENT:
|
||||
if one.host.delete(host.ID):
|
||||
result['changed'] = True
|
||||
else:
|
||||
self.fail(msg="could not delete host from cluster")
|
||||
|
||||
# if we reach this point we can assume that the host was taken to the desired state
|
||||
|
||||
if desired_state != "absent":
|
||||
# manipulate or modify the template
|
||||
desired_template_changes = self.get_parameter('template')
|
||||
|
||||
if desired_template_changes is None:
|
||||
desired_template_changes = dict()
|
||||
|
||||
# complete the template with speficic ansible parameters
|
||||
if self.is_parameter('labels'):
|
||||
desired_template_changes['LABELS'] = self.get_parameter('labels')
|
||||
|
||||
if self.requires_template_update(host.TEMPLATE, desired_template_changes):
|
||||
# setup the root element so that pyone will generate XML instead of attribute vector
|
||||
desired_template_changes = {"TEMPLATE": desired_template_changes}
|
||||
if one.host.update(host.ID, desired_template_changes, 1): # merge the template
|
||||
result['changed'] = True
|
||||
else:
|
||||
self.fail(msg="failed to update the host template")
|
||||
|
||||
# the cluster
|
||||
if host.CLUSTER_ID != self.get_parameter('cluster_id'):
|
||||
if one.cluster.addhost(self.get_parameter('cluster_id'), host.ID):
|
||||
result['changed'] = True
|
||||
else:
|
||||
self.fail(msg="failed to update the host cluster")
|
||||
|
||||
# return
|
||||
self.exit()
|
||||
|
||||
|
||||
def main():
|
||||
HostModule().run_module()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2018 www.privaz.io Valletech AB
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
# OpenNebula common documentation
|
||||
DOCUMENTATION = '''
|
||||
options:
|
||||
api_url:
|
||||
description:
|
||||
- The ENDPOINT URL of the XMLRPC server.
|
||||
If not specified then the value of the ONE_URL environment variable, if any, is used.
|
||||
aliases:
|
||||
- api_endpoint
|
||||
api_username:
|
||||
description:
|
||||
- The name of the user for XMLRPC authentication.
|
||||
If not specified then the value of the ONE_USERNAME environment variable, if any, is used.
|
||||
api_password:
|
||||
description:
|
||||
- The password or token for XMLRPC authentication.
|
||||
aliases:
|
||||
- api_token
|
||||
validate_certs:
|
||||
description:
|
||||
- Whether to validate the SSL certificates or not.
|
||||
This parameter is ignored if PYTHONHTTPSVERIFY environment variable is used.
|
||||
type: bool
|
||||
default: true
|
||||
wait_timeout:
|
||||
description:
|
||||
- time to wait for the desired state to be reached before timeout, in seconds.
|
||||
default: 300
|
||||
'''
|
@ -0,0 +1,20 @@
|
||||
# This is the configuration template for ansible-test OpenNebula integration tests.
|
||||
#
|
||||
# You do not need this template if you are:
|
||||
#
|
||||
# 1) Running integration tests without using ansible-test.
|
||||
# 2) Running integration tests against previously recorded XMLRPC fixtures
|
||||
#
|
||||
# If you want to test against a Live OpenNebula platform,
|
||||
# fill in the values below and save this file without the .template extension.
|
||||
# This will cause ansible-test to use the given configuration.
|
||||
#
|
||||
# If you run with @FIXTURES enabled (true) then you can decide if you want to
|
||||
# run in @REPLAY mode (true) or, record mode (false).
|
||||
|
||||
|
||||
opennebula_url: @URL
|
||||
opennebula_username: @USERNAME
|
||||
opennebula_password: @PASSWORD
|
||||
opennebula_test_fixture: @FIXTURES
|
||||
opennebula_test_fixture_replay: @REPLAY
|
@ -0,0 +1,2 @@
|
||||
cloud/opennebula
|
||||
posix/ci/cloud/group4/opennebula
|
Binary file not shown.
@ -0,0 +1,2 @@
|
||||
dependencies:
|
||||
- setup_opennebula
|
@ -0,0 +1,235 @@
|
||||
# test code for the one_host module
|
||||
|
||||
|
||||
# ENVIRONENT PREPARACTION
|
||||
|
||||
- set_fact: test_number= 0
|
||||
|
||||
- name: "test_{{test_number}}: copy fixtures to test host"
|
||||
copy:
|
||||
src: testhost/tmp/opennebula-fixtures.json.gz
|
||||
dest: /tmp
|
||||
when:
|
||||
- opennebula_test_fixture
|
||||
- opennebula_test_fixture_replay
|
||||
|
||||
|
||||
# SETUP INITIAL TESTING CONDITION
|
||||
|
||||
- set_fact: test_number={{ test_number | int + 1 }}
|
||||
|
||||
- name: "test_{{test_number}}: ensure the tests hosts are absent"
|
||||
one_host:
|
||||
name: "{{ item }}"
|
||||
state: absent
|
||||
api_endpoint: "{{ opennebula_url }}"
|
||||
api_username: "{{ opennebula_username }}"
|
||||
api_token: "{{ opennebula_password }}"
|
||||
validate_certs: false
|
||||
environment:
|
||||
ONE_TEST_FIXTURE: "{{ opennebula_test_fixture }}"
|
||||
ONE_TEST_FIXTURE_FILE: /tmp/opennebula-fixtures.json.gz
|
||||
ONE_TEST_FIXTURE_REPLAY: "{{ opennebula_test_fixture_replay }}"
|
||||
ONE_TEST_FIXTURE_UNIT: "test_{{test_number}}_{{ item }}"
|
||||
with_items: "{{opennebula_test.hosts}}"
|
||||
register: result
|
||||
|
||||
# NOT EXISTING HOSTS
|
||||
|
||||
- set_fact: test_number={{ test_number | int + 1 }}
|
||||
|
||||
- name: "test_{{test_number}}: attempt to enable a host that does not exists"
|
||||
one_host:
|
||||
name: badhost
|
||||
state: "{{item}}"
|
||||
api_url: "{{ opennebula_url }}"
|
||||
api_username: "{{ opennebula_username }}"
|
||||
api_password: "{{ opennebula_password }}"
|
||||
validate_certs: false
|
||||
environment:
|
||||
ONE_TEST_FIXTURE: "{{ opennebula_test_fixture }}"
|
||||
ONE_TEST_FIXTURE_FILE: /tmp/opennebula-fixtures.json.gz
|
||||
ONE_TEST_FIXTURE_REPLAY: "{{ opennebula_test_fixture_replay }}"
|
||||
ONE_TEST_FIXTURE_UNIT: "test_{{test_number}}_{{item}}"
|
||||
ignore_errors: true
|
||||
register: result
|
||||
with_items:
|
||||
- enabled
|
||||
- disabled
|
||||
- offline
|
||||
|
||||
- name: "assert test_{{test_number}} failed"
|
||||
assert:
|
||||
that:
|
||||
- result is failed
|
||||
- result.results[0].msg == 'invalid host state ERROR'
|
||||
|
||||
# ---
|
||||
|
||||
- set_fact: test_number={{ test_number | int + 1 }}
|
||||
|
||||
- name: "test_{{test_number}}: delete an unexisting host"
|
||||
one_host:
|
||||
name: badhost
|
||||
state: absent
|
||||
api_url: "{{ opennebula_url }}"
|
||||
api_username: "{{ opennebula_username }}"
|
||||
api_password: "{{ opennebula_password }}"
|
||||
validate_certs: false
|
||||
environment:
|
||||
ONE_TEST_FIXTURE: "{{ opennebula_test_fixture }}"
|
||||
ONE_TEST_FIXTURE_FILE: /tmp/opennebula-fixtures.json.gz
|
||||
ONE_TEST_FIXTURE_REPLAY: "{{ opennebula_test_fixture_replay }}"
|
||||
ONE_TEST_FIXTURE_UNIT: "test_{{test_number}}"
|
||||
register: result
|
||||
|
||||
- name: "assert test_{{test_number}} worked"
|
||||
assert:
|
||||
that:
|
||||
- result.changed
|
||||
|
||||
# HOST ENABLEMENT
|
||||
|
||||
- set_fact: test_number={{ test_number | int + 1 }}
|
||||
|
||||
|
||||
- name: "test_{{test_number}}: enable the test hosts"
|
||||
one_host:
|
||||
name: "{{ item }}"
|
||||
state: enabled
|
||||
api_url: "{{ opennebula_url }}"
|
||||
api_username: "{{ opennebula_username }}"
|
||||
api_password: "{{ opennebula_password }}"
|
||||
validate_certs: false
|
||||
environment:
|
||||
ONE_TEST_FIXTURE: "{{ opennebula_test_fixture }}"
|
||||
ONE_TEST_FIXTURE_FILE: /tmp/opennebula-fixtures.json.gz
|
||||
ONE_TEST_FIXTURE_REPLAY: "{{ opennebula_test_fixture_replay }}"
|
||||
ONE_TEST_FIXTURE_UNIT: "test_{{test_number}}_{{ item }}"
|
||||
with_items: "{{opennebula_test.hosts}}"
|
||||
register: result
|
||||
|
||||
- name: "assert test_{{test_number}} worked"
|
||||
assert:
|
||||
that:
|
||||
- result.changed
|
||||
|
||||
# TEMPLATE MANAGEMENT
|
||||
|
||||
- set_fact: test_number={{ test_number | int + 1 }}
|
||||
|
||||
- name: "test_{{test_number}}: setup template values on hosts"
|
||||
one_host:
|
||||
name: "{{ item }}"
|
||||
state: enabled
|
||||
api_url: "{{ opennebula_url }}"
|
||||
api_username: "{{ opennebula_username }}"
|
||||
api_password: "{{ opennebula_password }}"
|
||||
validate_certs: false
|
||||
template:
|
||||
LABELS:
|
||||
- test
|
||||
- custom
|
||||
TEST_VALUE: 2
|
||||
environment:
|
||||
ONE_TEST_FIXTURE: "{{ opennebula_test_fixture }}"
|
||||
ONE_TEST_FIXTURE_FILE: /tmp/opennebula-fixtures.json.gz
|
||||
ONE_TEST_FIXTURE_REPLAY: "{{ opennebula_test_fixture_replay }}"
|
||||
ONE_TEST_FIXTURE_UNIT: "test_{{test_number}}_{{ item }}"
|
||||
with_items: "{{opennebula_test.hosts}}"
|
||||
register: result
|
||||
|
||||
- name: "assert test_{{test_number}} worked"
|
||||
assert:
|
||||
that:
|
||||
- result.changed
|
||||
|
||||
# ---
|
||||
|
||||
- set_fact: test_number={{ test_number | int + 1 }}
|
||||
|
||||
- name: "test_{{test_number}}: setup equivalent template values on hosts"
|
||||
one_host:
|
||||
name: "{{ item }}"
|
||||
state: enabled
|
||||
api_url: "{{ opennebula_url }}"
|
||||
api_username: "{{ opennebula_username }}"
|
||||
api_password: "{{ opennebula_password }}"
|
||||
validate_certs: false
|
||||
labels:
|
||||
- test
|
||||
- custom
|
||||
attributes:
|
||||
TEST_VALUE: "2"
|
||||
environment:
|
||||
ONE_TEST_FIXTURE: "{{ opennebula_test_fixture }}"
|
||||
ONE_TEST_FIXTURE_FILE: /tmp/opennebula-fixtures.json.gz
|
||||
ONE_TEST_FIXTURE_REPLAY: "{{ opennebula_test_fixture_replay }}"
|
||||
ONE_TEST_FIXTURE_UNIT: "test_{{test_number}}_{{ item }}"
|
||||
with_items: "{{opennebula_test.hosts}}"
|
||||
register: result
|
||||
|
||||
- name: "assert test_{{test_number}} worked"
|
||||
assert:
|
||||
that:
|
||||
- result.changed == false
|
||||
|
||||
# HOST DISABLEMENT
|
||||
|
||||
- set_fact: test_number={{ test_number | int + 1 }}
|
||||
|
||||
- name: "test_{{test_number}}: disable the test hosts"
|
||||
one_host:
|
||||
name: "{{ item }}"
|
||||
state: disabled
|
||||
api_url: "{{ opennebula_url }}"
|
||||
api_username: "{{ opennebula_username }}"
|
||||
api_password: "{{ opennebula_password }}"
|
||||
validate_certs: false
|
||||
environment:
|
||||
ONE_TEST_FIXTURE: "{{ opennebula_test_fixture }}"
|
||||
ONE_TEST_FIXTURE_FILE: /tmp/opennebula-fixtures.json.gz
|
||||
ONE_TEST_FIXTURE_REPLAY: "{{ opennebula_test_fixture_replay }}"
|
||||
ONE_TEST_FIXTURE_UNIT: "test_{{test_number}}_{{ item }}"
|
||||
with_items: "{{opennebula_test.hosts}}"
|
||||
register: result
|
||||
|
||||
- name: "assert test_{{test_number}} worked"
|
||||
assert:
|
||||
that:
|
||||
- result.changed
|
||||
|
||||
# HOST OFFLINE
|
||||
|
||||
- set_fact: test_number={{ test_number | int + 1 }}
|
||||
|
||||
- name: "test_{{test_number}}: offline the test hosts"
|
||||
one_host:
|
||||
name: "{{ item }}"
|
||||
state: offline
|
||||
api_url: "{{ opennebula_url }}"
|
||||
api_username: "{{ opennebula_username }}"
|
||||
api_password: "{{ opennebula_password }}"
|
||||
validate_certs: false
|
||||
environment:
|
||||
ONE_TEST_FIXTURE: "{{ opennebula_test_fixture }}"
|
||||
ONE_TEST_FIXTURE_FILE: /tmp/opennebula-fixtures.json.gz
|
||||
ONE_TEST_FIXTURE_REPLAY: "{{ opennebula_test_fixture_replay }}"
|
||||
ONE_TEST_FIXTURE_UNIT: "test_{{test_number}}_{{ item }}"
|
||||
with_items: "{{opennebula_test.hosts}}"
|
||||
register: result
|
||||
|
||||
- name: "assert test_{{test_number}} worked"
|
||||
assert:
|
||||
that:
|
||||
- result.changed
|
||||
|
||||
# TEARDOWN
|
||||
|
||||
- name: fetch fixtures
|
||||
fetch:
|
||||
src: /tmp/opennebula-fixtures.json.gz
|
||||
dest: targets/one_host/files
|
||||
when:
|
||||
- opennebula_test_fixture
|
||||
- not opennebula_test_fixture_replay
|
@ -0,0 +1,6 @@
|
||||
---
|
||||
|
||||
opennebula_test:
|
||||
hosts:
|
||||
- hv1
|
||||
- hv2
|
@ -0,0 +1,61 @@
|
||||
"""OpenNebula plugin for integration tests."""
|
||||
|
||||
import os
|
||||
|
||||
from lib.cloud import (
|
||||
CloudProvider,
|
||||
CloudEnvironment
|
||||
)
|
||||
|
||||
from lib.util import (
|
||||
find_executable,
|
||||
ApplicationError,
|
||||
display,
|
||||
is_shippable,
|
||||
)
|
||||
|
||||
|
||||
class OpenNebulaCloudProvider(CloudProvider):
|
||||
"""Checks if a configuration file has been passed or fixtures are going to be used for testing"""
|
||||
|
||||
def filter(self, targets, exclude):
|
||||
""" no need to filter modules, they can either run from config file or from fixtures"""
|
||||
pass
|
||||
|
||||
def setup(self):
|
||||
"""Setup the cloud resource before delegation and register a cleanup callback."""
|
||||
super(OpenNebulaCloudProvider, self).setup()
|
||||
|
||||
if not self._use_static_config():
|
||||
self._setup_dynamic()
|
||||
|
||||
def _setup_dynamic(self):
|
||||
display.info('No config file provided, will run test from fixtures')
|
||||
|
||||
config = self._read_config_template()
|
||||
values = dict(
|
||||
URL="http://localhost/RPC2",
|
||||
USERNAME='oneadmin',
|
||||
PASSWORD='onepass',
|
||||
FIXTURES='true',
|
||||
REPLAY='true',
|
||||
)
|
||||
config = self._populate_config_template(config, values)
|
||||
self._write_config(config)
|
||||
|
||||
|
||||
class OpenNebulaCloudEnvironment(CloudEnvironment):
|
||||
"""
|
||||
Updates integration test environment after delegation. Will setup the config file as parameter.
|
||||
"""
|
||||
|
||||
def configure_environment(self, env, cmd):
|
||||
"""
|
||||
:type env: dict[str, str]
|
||||
:type cmd: list[str]
|
||||
"""
|
||||
cmd.append('-e')
|
||||
cmd.append('@%s' % self.config_path)
|
||||
|
||||
cmd.append('-e')
|
||||
cmd.append('resource_prefix=%s' % self.resource_prefix)
|
@ -0,0 +1 @@
|
||||
pyone
|
Loading…
Reference in New Issue