#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (c) 2016 Red Hat, Inc. # # 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 . # ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} DOCUMENTATION = ''' --- module: ovirt_cluster short_description: Module to manage clusters in oVirt/RHV version_added: "2.3" author: "Ondra Machacek (@machacekondra)" description: - "Module to manage clusters in oVirt/RHV" options: name: description: - "Name of the cluster to manage." required: true state: description: - "Should the cluster be present or absent" choices: ['present', 'absent'] default: present data_center: description: - "Datacenter name where cluster reside." description: description: - "Description of the cluster." comment: description: - "Comment of the cluster." network: description: - "Management network of cluster to access cluster hosts." ballooning: description: - "If I(True) enable memory balloon optimization. Memory balloon is used to re-distribute / reclaim the host memory based on VM needs in a dynamic way." virt: description: - "If I(True), hosts in this cluster will be used to run virtual machines." gluster: description: - "If I(True), hosts in this cluster will be used as Gluster Storage server nodes, and not for running virtual machines." - "By default the cluster is created for virtual machine hosts." threads_as_cores: description: - "If I(True) the exposed host threads would be treated as cores which can be utilized by virtual machines." ksm: description: - "I I(True) MoM enables to run Kernel Same-page Merging I(KSM) when necessary and when it can yield a memory saving benefit that outweighs its CPU cost." ksm_numa: description: - "If I(True) enables KSM C(ksm) for best berformance inside NUMA nodes." ha_reservation: description: - "If I(True) enable the oVirt/RHV to monitor cluster capacity for highly available virtual machines." trusted_service: description: - "If (True) enable integration with an OpenAttestation server." vm_reason: description: - "If I(True) enable an optional reason field when a virtual machine is shut down from the Manager, allowing the administrator to provide an explanation for the maintenance." host_reason: description: - "If I(True) enable an optional reason field when a host is placed into maintenance mode from the Manager, allowing the administrator to provide an explanation for the maintenance." memory_policy: description: - "I(disabled) - Disables memory page sharing." - "I(server) - Sets the memory page sharing threshold to 150% of the system memory on each host." - "I(desktop) - Sets the memory page sharing threshold to 200% of the system memory on each host." choices: ['disabled', 'server', 'desktop'] rng_sources: description: - "List that specify the random number generator devices that all hosts in the cluster will use." - "Supported generators are: I(hwrng) and I(random)." spice_proxy: description: - "The proxy by which the SPICE client will connect to virtual machines." - "The address must be in the following format: I(protocol://[host]:[port])" fence_enabled: description: - "If I(True) enables fencing on the cluster." - "Fencing is enabled by default." fence_skip_if_sd_active: description: - "If I(True) any hosts in the cluster that are Non Responsive and still connected to storage will not be fenced." fence_skip_if_connectivity_broken: description: - "If I(True) fencing will be temporarily disabled if the percentage of hosts in the cluster that are experiencing connectivity issues is greater than or equal to the defined threshold." - "The threshold can be specified by C(fence_connectivity_threshold)." fence_connectivity_threshold: description: - "The threshold used by C(fence_skip_if_connectivity_broken)." resilience_policy: description: - "The resilience policy defines how the virtual machines are prioritized in the migration." - "Following values are supported:" - "C(do_not_migrate) - Prevents virtual machines from being migrated. " - "C(migrate) - Migrates all virtual machines in order of their defined priority." - "C(migrate_highly_available) - Migrates only highly available virtual machines to prevent overloading other hosts." choices: ['do_not_migrate', 'migrate', 'migrate_highly_available'] migration_bandwidth: description: - "The bandwidth settings define the maximum bandwidth of both outgoing and incoming migrations per host." - "Following bandwidth options are supported:" - "C(auto) - Bandwidth is copied from the I(rate limit) [Mbps] setting in the data center host network QoS." - "C(hypervisor_default) - Bandwidth is controlled by local VDSM setting on sending host." - "C(custom) - Defined by user (in Mbps)." choices: ['auto', 'hypervisor_default', 'custom'] migration_bandwidth_limit: description: - "Set the I(custom) migration bandwidth limit." - "This parameter is used only when C(migration_bandwidth) is I(custom)." migration_auto_converge: description: - "If I(True) auto-convergence is used during live migration of virtual machines." - "Used only when C(migration_policy) is set to I(legacy)." - "Following options are supported:" - "C(true) - Override the global setting to I(true)." - "C(false) - Override the global setting to I(false)." - "C(inherit) - Use value which is set globally." choices: ['true', 'false', 'inherit'] migration_compressed: description: - "If I(True) compression is used during live migration of the virtual machine." - "Used only when C(migration_policy) is set to I(legacy)." - "Following options are supported:" - "C(true) - Override the global setting to I(true)." - "C(false) - Override the global setting to I(false)." - "C(inherit) - Use value which is set globally." choices: ['true', 'false', 'inherit'] migration_policy: description: - "A migration policy defines the conditions for live migrating virtual machines in the event of host failure." - "Following policies are supported:" - "C(legacy) - Legacy behavior of 3.6 version." - "C(minimal_downtime) - Virtual machines should not experience any significant downtime." - "C(suspend_workload) - Virtual machines may experience a more significant downtime." - "C(post_copy) - Virtual machines should not experience any significant downtime. If the VM migration is not converging for a long time, the migration will be switched to post-copy. Added in version I(2.4)." choices: ['legacy', 'minimal_downtime', 'suspend_workload', 'post_copy'] serial_policy: description: - "Specify a serial number policy for the virtual machines in the cluster." - "Following options are supported:" - "C(vm) - Sets the virtual machine's UUID as its serial number." - "C(host) - Sets the host's UUID as the virtual machine's serial number." - "C(custom) - Allows you to specify a custom serial number in C(serial_policy_value)." serial_policy_value: description: - "Allows you to specify a custom serial number." - "This parameter is used only when C(serial_policy) is I(custom)." scheduling_policy: description: - "Name of the scheduling policy to be used for cluster." cpu_arch: description: - "CPU architecture of cluster." choices: ['x86_64', 'ppc64', 'undefined'] cpu_type: description: - "CPU codename. For example I(Intel SandyBridge Family)." switch_type: description: - "Type of switch to be used by all networks in given cluster. Either I(legacy) which is using linux brigde or I(ovs) using Open vSwitch." choices: ['legacy', 'ovs'] compatibility_version: description: - "The compatibility version of the cluster. All hosts in this cluster must support at least this compatibility version." mac_pool: description: - "MAC pool to be used by this cluster." - "C(Note:)" - "This is supported since oVirt version 4.1." version_added: 2.4 external_network_providers: description: - "List of references to the external network providers available in the cluster. If the automatic deployment of the external network provider is supported, the networks of the referenced network provider are available on every host in the cluster." - "External network provider is described by following dictionary:" - "C(name) - Name of the external network provider. Either C(name) or C(id) is required." - "C(id) - ID of the external network provider. Either C(name) or C(id) is required." - "This is supported since oVirt version 4.2." version_added: 2.5 extends_documentation_fragment: ovirt ''' EXAMPLES = ''' # Examples don't contain auth parameter for simplicity, # look at ovirt_auth module to see how to reuse authentication: # Create cluster - ovirt_cluster: data_center: mydatacenter name: mycluster cpu_type: Intel SandyBridge Family description: mycluster compatibility_version: 4.0 # Create virt service cluster: - ovirt_cluster: data_center: mydatacenter name: mycluster cpu_type: Intel Nehalem Family description: mycluster switch_type: legacy compatibility_version: 4.0 ballooning: true gluster: false threads_as_cores: true ha_reservation: true trusted_service: false host_reason: false vm_reason: true ksm_numa: true memory_policy: server rng_sources: - hwrng - random # Create cluster with default network provider - ovirt_cluster: name: mycluster data_center: Default cpu_type: Intel SandyBridge Family external_network_providers: - name: ovirt-provider-ovn # Remove cluster - ovirt_cluster: state: absent name: mycluster ''' RETURN = ''' id: description: ID of the cluster which is managed returned: On success if cluster is found. type: str sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c cluster: description: "Dictionary of all the cluster attributes. Cluster attributes can be found on your oVirt/RHV instance at following url: http://ovirt.github.io/ovirt-engine-api-model/master/#types/cluster." type: dict returned: On success if cluster is found. ''' import traceback try: import ovirtsdk4.types as otypes except ImportError: pass from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.ovirt import ( BaseModule, check_sdk, create_connection, equal, ovirt_full_argument_spec, search_by_name, get_id_by_name, ) class ClustersModule(BaseModule): def __get_major(self, full_version): if full_version is None: return None if isinstance(full_version, otypes.Version): return full_version.major return int(full_version.split('.')[0]) def __get_minor(self, full_version): if full_version is None: return None if isinstance(full_version, otypes.Version): return full_version.minor return int(full_version.split('.')[1]) def param(self, name, default=None): return self._module.params.get(name, default) def _get_memory_policy(self): memory_policy = self.param('memory_policy') if memory_policy == 'desktop': return 200 elif memory_policy == 'server': return 150 elif memory_policy == 'disabled': return 100 def _get_policy_id(self): # These are hardcoded IDs, once there is API, please fix this. # legacy - 00000000-0000-0000-0000-000000000000 # minimal downtime - 80554327-0569-496b-bdeb-fcbbf52b827b # suspend workload if needed - 80554327-0569-496b-bdeb-fcbbf52b827c # post copy - a7aeedb2-8d66-4e51-bb22-32595027ce71 migration_policy = self.param('migration_policy') if migration_policy == 'legacy': return '00000000-0000-0000-0000-000000000000' elif migration_policy == 'minimal_downtime': return '80554327-0569-496b-bdeb-fcbbf52b827b' elif migration_policy == 'suspend_workload': return '80554327-0569-496b-bdeb-fcbbf52b827c' elif migration_policy == 'post_copy': return 'a7aeedb2-8d66-4e51-bb22-32595027ce71' def _get_sched_policy(self): sched_policy = None if self.param('scheduling_policy'): sched_policies_service = self._connection.system_service().scheduling_policies_service() sched_policy = search_by_name(sched_policies_service, self.param('scheduling_policy')) if not sched_policy: raise Exception("Scheduling policy '%s' was not found" % self.param('scheduling_policy')) return sched_policy def _get_mac_pool(self): mac_pool = None if self._module.params.get('mac_pool'): mac_pool = search_by_name( self._connection.system_service().mac_pools_service(), self._module.params.get('mac_pool'), ) return mac_pool def _get_external_network_providers(self): return self.param('external_network_providers') or [] def _get_external_network_provider_id(self, external_provider): return external_provider.get('id') or get_id_by_name( self._connection.system_service().openstack_network_providers_service(), external_provider.get('name') ) def _get_external_network_providers_entity(self): if self.param('external_network_providers') is not None: return [otypes.ExternalProvider(id=self._get_external_network_provider_id(external_provider)) for external_provider in self.param('external_network_providers')] def build_entity(self): sched_policy = self._get_sched_policy() return otypes.Cluster( name=self.param('name'), comment=self.param('comment'), description=self.param('description'), ballooning_enabled=self.param('ballooning'), gluster_service=self.param('gluster'), virt_service=self.param('virt'), threads_as_cores=self.param('threads_as_cores'), ha_reservation=self.param('ha_reservation'), trusted_service=self.param('trusted_service'), optional_reason=self.param('vm_reason'), maintenance_reason_required=self.param('host_reason'), scheduling_policy=otypes.SchedulingPolicy( id=sched_policy.id, ) if sched_policy else None, serial_number=otypes.SerialNumber( policy=otypes.SerialNumberPolicy(self.param('serial_policy')), value=self.param('serial_policy_value'), ) if ( self.param('serial_policy') is not None or self.param('serial_policy_value') is not None ) else None, migration=otypes.MigrationOptions( auto_converge=otypes.InheritableBoolean( self.param('migration_auto_converge'), ) if self.param('migration_auto_converge') else None, bandwidth=otypes.MigrationBandwidth( assignment_method=otypes.MigrationBandwidthAssignmentMethod( self.param('migration_bandwidth'), ) if self.param('migration_bandwidth') else None, custom_value=self.param('migration_bandwidth_limit'), ) if ( self.param('migration_bandwidth') or self.param('migration_bandwidth_limit') ) else None, compressed=otypes.InheritableBoolean( self.param('migration_compressed'), ) if self.param('migration_compressed') else None, policy=otypes.MigrationPolicy( id=self._get_policy_id() ) if self.param('migration_policy') else None, ) if ( self.param('migration_bandwidth') is not None or self.param('migration_bandwidth_limit') is not None or self.param('migration_auto_converge') is not None or self.param('migration_compressed') is not None or self.param('migration_policy') is not None ) else None, error_handling=otypes.ErrorHandling( on_error=otypes.MigrateOnError( self.param('resilience_policy') ), ) if self.param('resilience_policy') else None, fencing_policy=otypes.FencingPolicy( enabled=self.param('fence_enabled'), skip_if_connectivity_broken=otypes.SkipIfConnectivityBroken( enabled=self.param('fence_skip_if_connectivity_broken'), threshold=self.param('fence_connectivity_threshold'), ) if ( self.param('fence_skip_if_connectivity_broken') is not None or self.param('fence_connectivity_threshold') is not None ) else None, skip_if_sd_active=otypes.SkipIfSdActive( enabled=self.param('fence_skip_if_sd_active'), ) if self.param('fence_skip_if_sd_active') is not None else None, ) if ( self.param('fence_enabled') is not None or self.param('fence_skip_if_sd_active') is not None or self.param('fence_skip_if_connectivity_broken') is not None or self.param('fence_connectivity_threshold') is not None ) else None, display=otypes.Display( proxy=self.param('spice_proxy'), ) if self.param('spice_proxy') else None, required_rng_sources=[ otypes.RngSource(rng) for rng in self.param('rng_sources') ] if self.param('rng_sources') else None, memory_policy=otypes.MemoryPolicy( over_commit=otypes.MemoryOverCommit( percent=self._get_memory_policy(), ), ) if self.param('memory_policy') else None, ksm=otypes.Ksm( enabled=self.param('ksm'), merge_across_nodes=not self.param('ksm_numa'), ) if ( self.param('ksm_numa') is not None or self.param('ksm') is not None ) else None, data_center=otypes.DataCenter( name=self.param('data_center'), ) if self.param('data_center') else None, management_network=otypes.Network( name=self.param('network'), ) if self.param('network') else None, cpu=otypes.Cpu( architecture=self.param('cpu_arch'), type=self.param('cpu_type'), ) if ( self.param('cpu_arch') or self.param('cpu_type') ) else None, version=otypes.Version( major=self.__get_major(self.param('compatibility_version')), minor=self.__get_minor(self.param('compatibility_version')), ) if self.param('compatibility_version') else None, switch_type=otypes.SwitchType( self.param('switch_type') ) if self.param('switch_type') else None, mac_pool=otypes.MacPool( id=get_id_by_name(self._connection.system_service().mac_pools_service(), self.param('mac_pool')) ) if self.param('mac_pool') else None, external_network_providers=self._get_external_network_providers_entity(), ) def _matches_entity(self, item, entity): return equal(item.get('id'), entity.id) and equal(item.get('name'), entity.name) def _update_check_external_network_providers(self, entity): if self.param('external_network_providers') is None: return True if entity.external_network_providers is None: return not self.param('external_network_providers') entity_providers = self._connection.follow_link(entity.external_network_providers) entity_provider_ids = [provider.id for provider in entity_providers] entity_provider_names = [provider.name for provider in entity_providers] for provider in self._get_external_network_providers(): if provider.get('id'): if provider.get('id') not in entity_provider_ids: return False elif provider.get('name') and provider.get('name') not in entity_provider_names: return False for entity_provider in entity_providers: if not any([self._matches_entity(provider, entity_provider) for provider in self._get_external_network_providers()]): return False return True def update_check(self, entity): sched_policy = self._get_sched_policy() migration_policy = getattr(entity.migration, 'policy', None) cluster_cpu = getattr(entity, 'cpu', dict()) return ( equal(self.param('comment'), entity.comment) and equal(self.param('description'), entity.description) and equal(self.param('switch_type'), str(entity.switch_type)) and equal(self.param('cpu_arch'), str(getattr(cluster_cpu, 'architecture', None))) and equal(self.param('cpu_type'), getattr(cluster_cpu, 'type', None)) and equal(self.param('ballooning'), entity.ballooning_enabled) and equal(self.param('gluster'), entity.gluster_service) and equal(self.param('virt'), entity.virt_service) and equal(self.param('threads_as_cores'), entity.threads_as_cores) and equal(self.param('ksm_numa'), not entity.ksm.merge_across_nodes) and equal(self.param('ksm'), entity.ksm.enabled) and equal(self.param('ha_reservation'), entity.ha_reservation) and equal(self.param('trusted_service'), entity.trusted_service) and equal(self.param('host_reason'), entity.maintenance_reason_required) and equal(self.param('vm_reason'), entity.optional_reason) and equal(self.param('spice_proxy'), getattr(entity.display, 'proxy', None)) and equal(self.param('fence_enabled'), entity.fencing_policy.enabled) and equal(self.param('fence_skip_if_sd_active'), entity.fencing_policy.skip_if_sd_active.enabled) and equal(self.param('fence_skip_if_connectivity_broken'), entity.fencing_policy.skip_if_connectivity_broken.enabled) and equal(self.param('fence_connectivity_threshold'), entity.fencing_policy.skip_if_connectivity_broken.threshold) and equal(self.param('resilience_policy'), str(entity.error_handling.on_error)) and equal(self.param('migration_bandwidth'), str(entity.migration.bandwidth.assignment_method)) and equal(self.param('migration_auto_converge'), str(entity.migration.auto_converge)) and equal(self.param('migration_compressed'), str(entity.migration.compressed)) and equal(self.param('serial_policy'), str(getattr(entity.serial_number, 'policy', None))) and equal(self.param('serial_policy_value'), getattr(entity.serial_number, 'value', None)) and equal(self.param('scheduling_policy'), getattr(self._connection.follow_link(entity.scheduling_policy), 'name', None)) and equal(self._get_policy_id(), getattr(migration_policy, 'id', None)) and equal(self._get_memory_policy(), entity.memory_policy.over_commit.percent) and equal(self.__get_minor(self.param('compatibility_version')), self.__get_minor(entity.version)) and equal(self.__get_major(self.param('compatibility_version')), self.__get_major(entity.version)) and equal( self.param('migration_bandwidth_limit') if self.param('migration_bandwidth') == 'custom' else None, entity.migration.bandwidth.custom_value ) and equal( sorted(self.param('rng_sources')) if self.param('rng_sources') else None, sorted([ str(source) for source in entity.required_rng_sources ]) ) and equal( get_id_by_name(self._connection.system_service().mac_pools_service(), self.param('mac_pool'), raise_error=False), entity.mac_pool.id ) and self._update_check_external_network_providers(entity) ) def main(): argument_spec = ovirt_full_argument_spec( state=dict( choices=['present', 'absent'], default='present', ), name=dict(default=None, required=True), ballooning=dict(default=None, type='bool', aliases=['balloon']), gluster=dict(default=None, type='bool'), virt=dict(default=None, type='bool'), threads_as_cores=dict(default=None, type='bool'), ksm_numa=dict(default=None, type='bool'), ksm=dict(default=None, type='bool'), ha_reservation=dict(default=None, type='bool'), trusted_service=dict(default=None, type='bool'), vm_reason=dict(default=None, type='bool'), host_reason=dict(default=None, type='bool'), memory_policy=dict(default=None, choices=['disabled', 'server', 'desktop'], aliases=['performance_preset']), rng_sources=dict(default=None, type='list'), spice_proxy=dict(default=None), fence_enabled=dict(default=None, type='bool'), fence_skip_if_sd_active=dict(default=None, type='bool'), fence_skip_if_connectivity_broken=dict(default=None, type='bool'), fence_connectivity_threshold=dict(default=None, type='int'), resilience_policy=dict(default=None, choices=['migrate_highly_available', 'migrate', 'do_not_migrate']), migration_bandwidth=dict(default=None, choices=['auto', 'hypervisor_default', 'custom']), migration_bandwidth_limit=dict(default=None, type='int'), migration_auto_converge=dict(default=None, choices=['true', 'false', 'inherit']), migration_compressed=dict(default=None, choices=['true', 'false', 'inherit']), migration_policy=dict( default=None, choices=['legacy', 'minimal_downtime', 'suspend_workload', 'post_copy'] ), serial_policy=dict(default=None, choices=['vm', 'host', 'custom']), serial_policy_value=dict(default=None), scheduling_policy=dict(default=None), data_center=dict(default=None), description=dict(default=None), comment=dict(default=None), network=dict(default=None), cpu_arch=dict(default=None, choices=['ppc64', 'undefined', 'x86_64']), cpu_type=dict(default=None), switch_type=dict(default=None, choices=['legacy', 'ovs']), compatibility_version=dict(default=None), mac_pool=dict(default=None), external_network_providers=dict(default=None, type='list'), ) module = AnsibleModule( argument_spec=argument_spec, supports_check_mode=True, ) if module._name == 'ovirt_clusters': module.deprecate("The 'ovirt_clusters' module is being renamed 'ovirt_cluster'", version=2.8) check_sdk(module) try: auth = module.params.pop('auth') connection = create_connection(auth) clusters_service = connection.system_service().clusters_service() clusters_module = ClustersModule( connection=connection, module=module, service=clusters_service, ) state = module.params['state'] if state == 'present': ret = clusters_module.create() elif state == 'absent': ret = clusters_module.remove() module.exit_json(**ret) except Exception as e: module.fail_json(msg=str(e), exception=traceback.format_exc()) finally: connection.close(logout=auth.get('token') is None) if __name__ == "__main__": main()