mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-05-08 14:22:46 +00:00
Initial commit
This commit is contained in:
473
plugins/modules/cloud/kubevirt/kubevirt_vm.py
Normal file
473
plugins/modules/cloud/kubevirt/kubevirt_vm.py
Normal file
@@ -0,0 +1,473 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2019, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: kubevirt_vm
|
||||
|
||||
short_description: Manage KubeVirt virtual machine
|
||||
|
||||
description:
|
||||
- Use Openshift Python SDK to manage the state of KubeVirt virtual machines.
|
||||
|
||||
|
||||
author: KubeVirt Team (@kubevirt)
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Set the virtual machine to either I(present), I(absent), I(running) or I(stopped).
|
||||
- "I(present) - Create or update a virtual machine. (And run it if it's ephemeral.)"
|
||||
- "I(absent) - Remove a virtual machine."
|
||||
- "I(running) - Create or update a virtual machine and run it."
|
||||
- "I(stopped) - Stop a virtual machine. (This deletes ephemeral VMs.)"
|
||||
default: "present"
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
- running
|
||||
- stopped
|
||||
type: str
|
||||
name:
|
||||
description:
|
||||
- Name of the virtual machine.
|
||||
required: true
|
||||
type: str
|
||||
namespace:
|
||||
description:
|
||||
- Namespace where the virtual machine exists.
|
||||
required: true
|
||||
type: str
|
||||
ephemeral:
|
||||
description:
|
||||
- If (true) ephemeral virtual machine will be created. When destroyed it won't be accessible again.
|
||||
- Works only with C(state) I(present) and I(absent).
|
||||
type: bool
|
||||
default: false
|
||||
datavolumes:
|
||||
description:
|
||||
- "DataVolumes are a way to automate importing virtual machine disks onto pvcs during the virtual machine's
|
||||
launch flow. Without using a DataVolume, users have to prepare a pvc with a disk image before assigning
|
||||
it to a VM or VMI manifest. With a DataVolume, both the pvc creation and import is automated on behalf of the user."
|
||||
type: list
|
||||
template:
|
||||
description:
|
||||
- "Name of Template to be used in creation of a virtual machine."
|
||||
type: str
|
||||
template_parameters:
|
||||
description:
|
||||
- "New values of parameters from Template."
|
||||
type: dict
|
||||
|
||||
extends_documentation_fragment:
|
||||
- community.kubernetes.k8s_auth_options
|
||||
- community.general.kubevirt_vm_options
|
||||
- community.general.kubevirt_common_options
|
||||
|
||||
|
||||
requirements:
|
||||
- python >= 2.7
|
||||
- openshift >= 0.8.2
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Start virtual machine 'myvm'
|
||||
kubevirt_vm:
|
||||
state: running
|
||||
name: myvm
|
||||
namespace: vms
|
||||
|
||||
- name: Create virtual machine 'myvm' and start it
|
||||
kubevirt_vm:
|
||||
state: running
|
||||
name: myvm
|
||||
namespace: vms
|
||||
memory: 64Mi
|
||||
cpu_cores: 1
|
||||
bootloader: efi
|
||||
smbios_uuid: 5d307ca9-b3ef-428c-8861-06e72d69f223
|
||||
cpu_model: Conroe
|
||||
headless: true
|
||||
hugepage_size: 2Mi
|
||||
tablets:
|
||||
- bus: virtio
|
||||
name: tablet1
|
||||
cpu_limit: 3
|
||||
cpu_shares: 2
|
||||
disks:
|
||||
- name: containerdisk
|
||||
volume:
|
||||
containerDisk:
|
||||
image: kubevirt/cirros-container-disk-demo:latest
|
||||
path: /custom-disk/cirros.img
|
||||
disk:
|
||||
bus: virtio
|
||||
|
||||
- name: Create virtual machine 'myvm' with multus network interface
|
||||
kubevirt_vm:
|
||||
name: myvm
|
||||
namespace: vms
|
||||
memory: 512M
|
||||
interfaces:
|
||||
- name: default
|
||||
bridge: {}
|
||||
network:
|
||||
pod: {}
|
||||
- name: mynet
|
||||
bridge: {}
|
||||
network:
|
||||
multus:
|
||||
networkName: mynetconf
|
||||
|
||||
- name: Combine inline definition with Ansible parameters
|
||||
kubevirt_vm:
|
||||
# Kubernetes specification:
|
||||
definition:
|
||||
metadata:
|
||||
labels:
|
||||
app: galaxy
|
||||
service: web
|
||||
origin: vmware
|
||||
|
||||
# Ansible parameters:
|
||||
state: running
|
||||
name: myvm
|
||||
namespace: vms
|
||||
memory: 64M
|
||||
disks:
|
||||
- name: containerdisk
|
||||
volume:
|
||||
containerDisk:
|
||||
image: kubevirt/cirros-container-disk-demo:latest
|
||||
path: /custom-disk/cirros.img
|
||||
disk:
|
||||
bus: virtio
|
||||
|
||||
- name: Start ephemeral virtual machine 'myvm' and wait to be running
|
||||
kubevirt_vm:
|
||||
ephemeral: true
|
||||
state: running
|
||||
wait: true
|
||||
wait_timeout: 180
|
||||
name: myvm
|
||||
namespace: vms
|
||||
memory: 64M
|
||||
labels:
|
||||
kubevirt.io/vm: myvm
|
||||
disks:
|
||||
- name: containerdisk
|
||||
volume:
|
||||
containerDisk:
|
||||
image: kubevirt/cirros-container-disk-demo:latest
|
||||
path: /custom-disk/cirros.img
|
||||
disk:
|
||||
bus: virtio
|
||||
|
||||
- name: Start fedora vm with cloud init
|
||||
kubevirt_vm:
|
||||
state: running
|
||||
wait: true
|
||||
name: myvm
|
||||
namespace: vms
|
||||
memory: 1024M
|
||||
cloud_init_nocloud:
|
||||
userData: |-
|
||||
#cloud-config
|
||||
password: fedora
|
||||
chpasswd: { expire: False }
|
||||
disks:
|
||||
- name: containerdisk
|
||||
volume:
|
||||
containerDisk:
|
||||
image: kubevirt/fedora-cloud-container-disk-demo:latest
|
||||
path: /disk/fedora.qcow2
|
||||
disk:
|
||||
bus: virtio
|
||||
node_affinity:
|
||||
soft:
|
||||
- weight: 1
|
||||
term:
|
||||
match_expressions:
|
||||
- key: security
|
||||
operator: In
|
||||
values:
|
||||
- S2
|
||||
|
||||
- name: Create virtual machine with datavolume and specify node affinity
|
||||
kubevirt_vm:
|
||||
name: myvm
|
||||
namespace: default
|
||||
memory: 1024Mi
|
||||
datavolumes:
|
||||
- name: mydv
|
||||
source:
|
||||
http:
|
||||
url: https://url/disk.qcow2
|
||||
pvc:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storage: 5Gi
|
||||
node_affinity:
|
||||
hard:
|
||||
- term:
|
||||
match_expressions:
|
||||
- key: security
|
||||
operator: In
|
||||
values:
|
||||
- S1
|
||||
|
||||
- name: Remove virtual machine 'myvm'
|
||||
kubevirt_vm:
|
||||
state: absent
|
||||
name: myvm
|
||||
namespace: vms
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
kubevirt_vm:
|
||||
description:
|
||||
- The virtual machine dictionary specification returned by the API.
|
||||
- "This dictionary contains all values returned by the KubeVirt API all options
|
||||
are described here U(https://kubevirt.io/api-reference/master/definitions.html#_v1_virtualmachine)"
|
||||
returned: success
|
||||
type: complex
|
||||
contains: {}
|
||||
'''
|
||||
|
||||
|
||||
import copy
|
||||
import traceback
|
||||
|
||||
from ansible_collections.community.kubernetes.plugins.module_utils.k8s.common import AUTH_ARG_SPEC
|
||||
from ansible_collections.community.general.plugins.module_utils.kubevirt import (
|
||||
virtdict,
|
||||
KubeVirtRawModule,
|
||||
VM_COMMON_ARG_SPEC,
|
||||
VM_SPEC_DEF_ARG_SPEC
|
||||
)
|
||||
|
||||
VM_ARG_SPEC = {
|
||||
'ephemeral': {'type': 'bool', 'default': False},
|
||||
'state': {
|
||||
'type': 'str',
|
||||
'choices': [
|
||||
'present', 'absent', 'running', 'stopped'
|
||||
],
|
||||
'default': 'present'
|
||||
},
|
||||
'datavolumes': {'type': 'list'},
|
||||
'template': {'type': 'str'},
|
||||
'template_parameters': {'type': 'dict'},
|
||||
}
|
||||
|
||||
# Which params (can) modify 'spec:' contents of a VM:
|
||||
VM_SPEC_PARAMS = list(VM_SPEC_DEF_ARG_SPEC.keys()) + ['datavolumes', 'template', 'template_parameters']
|
||||
|
||||
|
||||
class KubeVirtVM(KubeVirtRawModule):
|
||||
|
||||
@property
|
||||
def argspec(self):
|
||||
""" argspec property builder """
|
||||
argument_spec = copy.deepcopy(AUTH_ARG_SPEC)
|
||||
argument_spec.update(VM_COMMON_ARG_SPEC)
|
||||
argument_spec.update(VM_ARG_SPEC)
|
||||
return argument_spec
|
||||
|
||||
@staticmethod
|
||||
def fix_serialization(obj):
|
||||
if obj and hasattr(obj, 'to_dict'):
|
||||
return obj.to_dict()
|
||||
return obj
|
||||
|
||||
def _wait_for_vmi_running(self):
|
||||
for event in self._kind_resource.watch(namespace=self.namespace, timeout=self.params.get('wait_timeout')):
|
||||
entity = event['object']
|
||||
if entity.metadata.name != self.name:
|
||||
continue
|
||||
status = entity.get('status', {})
|
||||
phase = status.get('phase', None)
|
||||
if phase == 'Running':
|
||||
return entity
|
||||
|
||||
self.fail("Timeout occurred while waiting for virtual machine to start. Maybe try a higher wait_timeout value?")
|
||||
|
||||
def _wait_for_vm_state(self, new_state):
|
||||
if new_state == 'running':
|
||||
want_created = want_ready = True
|
||||
else:
|
||||
want_created = want_ready = False
|
||||
|
||||
for event in self._kind_resource.watch(namespace=self.namespace, timeout=self.params.get('wait_timeout')):
|
||||
entity = event['object']
|
||||
if entity.metadata.name != self.name:
|
||||
continue
|
||||
status = entity.get('status', {})
|
||||
created = status.get('created', False)
|
||||
ready = status.get('ready', False)
|
||||
if (created, ready) == (want_created, want_ready):
|
||||
return entity
|
||||
|
||||
self.fail("Timeout occurred while waiting for virtual machine to achieve '{0}' state. "
|
||||
"Maybe try a higher wait_timeout value?".format(new_state))
|
||||
|
||||
def manage_vm_state(self, new_state, already_changed):
|
||||
new_running = True if new_state == 'running' else False
|
||||
changed = False
|
||||
k8s_obj = {}
|
||||
|
||||
if not already_changed:
|
||||
k8s_obj = self.get_resource(self._kind_resource)
|
||||
if not k8s_obj:
|
||||
self.fail("VirtualMachine object disappeared during module operation, aborting.")
|
||||
if k8s_obj.spec.get('running', False) == new_running:
|
||||
return False, k8s_obj
|
||||
|
||||
newdef = dict(metadata=dict(name=self.name, namespace=self.namespace), spec=dict(running=new_running))
|
||||
k8s_obj, err = self.patch_resource(self._kind_resource, newdef, k8s_obj,
|
||||
self.name, self.namespace, merge_type='merge')
|
||||
if err:
|
||||
self.fail_json(**err)
|
||||
else:
|
||||
changed = True
|
||||
|
||||
if self.params.get('wait'):
|
||||
k8s_obj = self._wait_for_vm_state(new_state)
|
||||
|
||||
return changed, k8s_obj
|
||||
|
||||
def _process_template_defaults(self, proccess_template, processedtemplate, defaults):
|
||||
def set_template_default(default_name, default_name_index, definition_spec):
|
||||
default_value = proccess_template['metadata']['annotations'][default_name]
|
||||
if default_value:
|
||||
values = definition_spec[default_name_index]
|
||||
default_values = [d for d in values if d.get('name') == default_value]
|
||||
defaults[default_name_index] = default_values
|
||||
if definition_spec[default_name_index] is None:
|
||||
definition_spec[default_name_index] = []
|
||||
definition_spec[default_name_index].extend([d for d in values if d.get('name') != default_value])
|
||||
|
||||
devices = processedtemplate['spec']['template']['spec']['domain']['devices']
|
||||
spec = processedtemplate['spec']['template']['spec']
|
||||
|
||||
set_template_default('defaults.template.cnv.io/disk', 'disks', devices)
|
||||
set_template_default('defaults.template.cnv.io/volume', 'volumes', spec)
|
||||
set_template_default('defaults.template.cnv.io/nic', 'interfaces', devices)
|
||||
set_template_default('defaults.template.cnv.io/network', 'networks', spec)
|
||||
|
||||
def construct_definition(self, kind, our_state, ephemeral):
|
||||
definition = virtdict()
|
||||
processedtemplate = {}
|
||||
|
||||
# Construct the API object definition:
|
||||
defaults = {'disks': [], 'volumes': [], 'interfaces': [], 'networks': []}
|
||||
vm_template = self.params.get('template')
|
||||
if vm_template:
|
||||
# Find the template the VM should be created from:
|
||||
template_resource = self.client.resources.get(api_version='template.openshift.io/v1', kind='Template', name='templates')
|
||||
proccess_template = template_resource.get(name=vm_template, namespace=self.params.get('namespace'))
|
||||
|
||||
# Set proper template values taken from module option 'template_parameters':
|
||||
for k, v in self.params.get('template_parameters', {}).items():
|
||||
for parameter in proccess_template.parameters:
|
||||
if parameter.name == k:
|
||||
parameter.value = v
|
||||
|
||||
# Proccess the template:
|
||||
processedtemplates_res = self.client.resources.get(api_version='template.openshift.io/v1', kind='Template', name='processedtemplates')
|
||||
processedtemplate = processedtemplates_res.create(proccess_template.to_dict()).to_dict()['objects'][0]
|
||||
|
||||
# Process defaults of the template:
|
||||
self._process_template_defaults(proccess_template, processedtemplate, defaults)
|
||||
|
||||
if not ephemeral:
|
||||
definition['spec']['running'] = our_state == 'running'
|
||||
template = definition if ephemeral else definition['spec']['template']
|
||||
template['metadata']['labels']['vm.cnv.io/name'] = self.params.get('name')
|
||||
dummy, definition = self.construct_vm_definition(kind, definition, template, defaults)
|
||||
|
||||
return self.merge_dicts(definition, processedtemplate)
|
||||
|
||||
def execute_module(self):
|
||||
# Parse parameters specific to this module:
|
||||
ephemeral = self.params.get('ephemeral')
|
||||
k8s_state = our_state = self.params.get('state')
|
||||
kind = 'VirtualMachineInstance' if ephemeral else 'VirtualMachine'
|
||||
_used_params = [name for name in self.params if self.params[name] is not None]
|
||||
# Is 'spec:' getting changed?
|
||||
vm_spec_change = True if set(VM_SPEC_PARAMS).intersection(_used_params) else False
|
||||
changed = False
|
||||
crud_executed = False
|
||||
method = ''
|
||||
|
||||
# Underlying module_utils/k8s/* code knows only of state == present/absent; let's make sure not to confuse it
|
||||
if ephemeral:
|
||||
# Ephemerals don't actually support running/stopped; we treat those as aliases for present/absent instead
|
||||
if our_state == 'running':
|
||||
self.params['state'] = k8s_state = 'present'
|
||||
elif our_state == 'stopped':
|
||||
self.params['state'] = k8s_state = 'absent'
|
||||
else:
|
||||
if our_state != 'absent':
|
||||
self.params['state'] = k8s_state = 'present'
|
||||
|
||||
# Start with fetching the current object to make sure it exists
|
||||
# If it does, but we end up not performing any operations on it, at least we'll be able to return
|
||||
# its current contents as part of the final json
|
||||
self.client = self.get_api_client()
|
||||
self._kind_resource = self.find_supported_resource(kind)
|
||||
k8s_obj = self.get_resource(self._kind_resource)
|
||||
if not self.check_mode and not vm_spec_change and k8s_state != 'absent' and not k8s_obj:
|
||||
self.fail("It's impossible to create an empty VM or change state of a non-existent VM.")
|
||||
|
||||
# If there are (potential) changes to `spec:` or we want to delete the object, that warrants a full CRUD
|
||||
# Also check_mode always warrants a CRUD, as that'll produce a sane result
|
||||
if vm_spec_change or k8s_state == 'absent' or self.check_mode:
|
||||
definition = self.construct_definition(kind, our_state, ephemeral)
|
||||
result = self.execute_crud(kind, definition)
|
||||
changed = result['changed']
|
||||
k8s_obj = result['result']
|
||||
method = result['method']
|
||||
crud_executed = True
|
||||
|
||||
if ephemeral and self.params.get('wait') and k8s_state == 'present' and not self.check_mode:
|
||||
# Waiting for k8s_state==absent is handled inside execute_crud()
|
||||
k8s_obj = self._wait_for_vmi_running()
|
||||
|
||||
if not ephemeral and our_state in ['running', 'stopped'] and not self.check_mode:
|
||||
# State==present/absent doesn't involve any additional VMI state management and is fully
|
||||
# handled inside execute_crud() (including wait logic)
|
||||
patched, k8s_obj = self.manage_vm_state(our_state, crud_executed)
|
||||
changed = changed or patched
|
||||
if changed:
|
||||
method = method or 'patch'
|
||||
|
||||
# Return from the module:
|
||||
self.exit_json(**{
|
||||
'changed': changed,
|
||||
'kubevirt_vm': self.fix_serialization(k8s_obj),
|
||||
'method': method
|
||||
})
|
||||
|
||||
|
||||
def main():
|
||||
module = KubeVirtVM()
|
||||
try:
|
||||
module.execute_module()
|
||||
except Exception as e:
|
||||
module.fail_json(msg=str(e), exception=traceback.format_exc())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user