From a9c5560dcca18a0df93341289087a9585fbca29c Mon Sep 17 00:00:00 2001 From: Christian Pointner Date: Sat, 1 Jan 2022 17:13:26 +0100 Subject: add new openwrt-based testvm add new vm-deploy playbook and script --- roles/vm/guest/define/templates/libvirt-domain.xml.j2 | 4 +++- roles/vm/guest/install/tasks/main.yml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'roles/vm') diff --git a/roles/vm/guest/define/templates/libvirt-domain.xml.j2 b/roles/vm/guest/define/templates/libvirt-domain.xml.j2 index 0430229b..11b1be6e 100644 --- a/roles/vm/guest/define/templates/libvirt-domain.xml.j2 +++ b/roles/vm/guest/define/templates/libvirt-domain.xml.j2 @@ -56,7 +56,7 @@ {% endif %} {% for bus in ['virtio', 'scsi'] %} {% for device, src in (install.disks[bus] | default({})).items() %} - + {% if src.type == 'lvm' %} @@ -64,6 +64,8 @@ {% elif src.type == 'blockdev' %} +{% elif src.type == 'image' %} + {% endif %} diff --git a/roles/vm/guest/install/tasks/main.yml b/roles/vm/guest/install/tasks/main.yml index f2bd5362..d4a31929 100644 --- a/roles/vm/guest/install/tasks/main.yml +++ b/roles/vm/guest/install/tasks/main.yml @@ -10,7 +10,7 @@ size: "{{ item.value.size }}" state: present -- name: create zfs base datasets for vm +- name: create zfs-based datasets for vm loop: "{{ install.disks.virtio | default({}) | combine(install.disks.scsi | default({})) | dict2items | selectattr('value.type', 'eq', 'zfs') | map(attribute='value.backend') | map('default', 'default') | unique | list }}" delegate_to: "{{ vm_host.name }}" zfs: -- cgit v1.2.3 From 9a8c06e53273fb6fb35c16aeb2429320b421f6a8 Mon Sep 17 00:00:00 2001 From: Christian Pointner Date: Mon, 3 Jan 2022 22:42:20 +0100 Subject: first basic version of openwrt-based vms --- ansible.cfg | 1 + common/vm-deploy.yml | 12 +- inventory/host_vars/ch-testvm-openwrt.yml | 29 ++-- library/wait_for_virt.py | 179 ++++++++++++++++++++++++ roles/vm/guest/deploy/defaults/main.yml | 7 + roles/vm/guest/deploy/tasks/main.yml | 26 ++++ roles/vm/guest/install/library/wait_for_virt.py | 179 ------------------------ 7 files changed, 228 insertions(+), 205 deletions(-) create mode 100644 library/wait_for_virt.py create mode 100644 roles/vm/guest/deploy/defaults/main.yml create mode 100644 roles/vm/guest/deploy/tasks/main.yml delete mode 100644 roles/vm/guest/install/library/wait_for_virt.py (limited to 'roles/vm') diff --git a/ansible.cfg b/ansible.cfg index ef74d99c..e3b31768 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -7,6 +7,7 @@ log_path = ./log remote_tmp = /tmp/.ansible/tmp filter_plugins = ./filter_plugins +library = ./library gathering = smart fact_caching = jsonfile diff --git a/common/vm-deploy.yml b/common/vm-deploy.yml index e7c9ded7..bc2e8039 100644 --- a/common/vm-deploy.yml +++ b/common/vm-deploy.yml @@ -22,11 +22,9 @@ when: - (output_images | length) != 1 - - name: fetch infos from image file - stat: - path: "{{ output_images | first }}" - get_mime: yes - register: output_images_info - - debug: - var: output_images_info.stat +- name: deploy vm + hosts: "{{ install_hostname }}" + gather_facts: no + roles: + - role: vm/guest/deploy diff --git a/inventory/host_vars/ch-testvm-openwrt.yml b/inventory/host_vars/ch-testvm-openwrt.yml index f14bd548..5a208b55 100644 --- a/inventory/host_vars/ch-testvm-openwrt.yml +++ b/inventory/host_vars/ch-testvm-openwrt.yml @@ -8,9 +8,8 @@ install: primary: /dev/sda scsi: sda: - type: zfs - name: root - size: 15g + type: image + path: /srv/nvme/ch-testvm-openwrt.img interfaces: - bridge: br-svc name: svc0 @@ -98,20 +97,12 @@ openwrt_uci: proto: static ipaddr: "{{ network_zones.svc.prefix | ipaddr(network_zones.svc.offsets[inventory_hostname]) | ipaddr('address') }}" netmask: "{{ network_zones.svc.prefix | ipaddr('netmask') }}" + gateway: "{{ network_zones.svc.gateway }}" + dns: "{{ network_zones.svc.dns }}" - -virsh_domxml: | - - - /srv/ch-router/vmlinuz - console=ttyS0,115200n8 noinitrd root=/dev/vda - - - - - - - - - - + - name: route 'lan' + options: + interface: svc + target: "{{ network_zones.lan.prefix | ipaddr('network') }}" + netmask: "{{ network_zones.lan.prefix | ipaddr('netmask') }}" + gateway: "{{ network_zones.svc.prefix | ipaddr(network_zones.svc.offsets['ch-gw-lan']) | ipaddr('address') }}" diff --git a/library/wait_for_virt.py b/library/wait_for_virt.py new file mode 100644 index 00000000..6c49fae1 --- /dev/null +++ b/library/wait_for_virt.py @@ -0,0 +1,179 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import traceback +import time + +try: + import libvirt +except ImportError: + HAS_VIRT = False +else: + HAS_VIRT = True + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native + + +VIRT_FAILED = 1 +VIRT_SUCCESS = 0 +VIRT_UNAVAILABLE = 2 + +VIRT_STATE_NAME_MAP = { + 0: "running", + 1: "running", + 2: "running", + 3: "paused", + 4: "shutdown", + 5: "shutdown", + 6: "crashed" +} + + +class VMNotFound(Exception): + pass + + +class LibvirtConnection(object): + + def __init__(self, uri, module): + + self.module = module + + cmd = "uname -r" + rc, stdout, stderr = self.module.run_command(cmd) + + if "xen" in stdout: + conn = libvirt.open(None) + elif "esx" in uri: + auth = [[libvirt.VIR_CRED_AUTHNAME, libvirt.VIR_CRED_NOECHOPROMPT], [], None] + conn = libvirt.openAuth(uri, auth) + else: + conn = libvirt.open(uri) + + if not conn: + raise Exception("hypervisor connection failure") + + self.conn = conn + + def find_vm(self, vmid): + """ + Extra bonus feature: vmid = -1 returns a list of everything + """ + conn = self.conn + + vms = [] + + # this block of code borrowed from virt-manager: + # get working domain's name + ids = conn.listDomainsID() + for id in ids: + vm = conn.lookupByID(id) + vms.append(vm) + # get defined domain + names = conn.listDefinedDomains() + for name in names: + vm = conn.lookupByName(name) + vms.append(vm) + + if vmid == -1: + return vms + + for vm in vms: + if vm.name() == vmid: + return vm + + raise VMNotFound("virtual machine %s not found" % vmid) + + def get_status(self, vmid): + state = self.find_vm(vmid).info()[0] + return VIRT_STATE_NAME_MAP.get(state, "unknown") + + +class Virt(object): + + def __init__(self, uri, module): + self.module = module + self.uri = uri + + def __get_conn(self): + self.conn = LibvirtConnection(self.uri, self.module) + return self.conn + + def status(self, vmid): + """ + Return a state suitable for server consumption. Aka, codes.py values, not XM output. + """ + self.__get_conn() + return self.conn.get_status(vmid) + + +def core(module): + + states = module.params.get('states', None) + guest = module.params.get('name', None) + uri = module.params.get('uri', None) + delay = module.params.get('delay', None) + sleep = module.params.get('sleep', None) + timeout = module.params.get('timeout', None) + + v = Virt(uri, module) + res = {'changed': False, 'failed': True} + + if delay > 0: + time.sleep(delay) + + for _ in range(0, timeout, sleep): + state = v.status(guest) + if state in states: + res['state'] = state + res['failed'] = False + res['msg'] = "guest '%s' has reached state: %s" % (guest, state) + return VIRT_SUCCESS, res + + time.sleep(sleep) + + res['msg'] = "timeout waiting for guest '%s' to reach one of states: %s" % (guest, ', '.join(states)) + return VIRT_FAILED, res + + +def main(): + + module = AnsibleModule(argument_spec=dict( + name=dict(aliases=['guest'], required=True), + states=dict(type='list', required=True), + uri=dict(default='qemu:///system'), + delay=dict(type='int', default=0), + sleep=dict(type='int', default=1), + timeout=dict(type='int', default=300), + )) + + if not HAS_VIRT: + module.fail_json( + msg='The `libvirt` module is not importable. Check the requirements.' + ) + + for state in module.params.get('states', None): + if state not in set(VIRT_STATE_NAME_MAP.values()): + module.fail_json( + msg="states contains invalid state '%s', must be one of %s" % (state, ', '.join(set(VIRT_STATE_NAME_MAP.values()))) + ) + + rc = VIRT_SUCCESS + try: + rc, result = core(module) + except Exception as e: + module.fail_json(msg=to_native(e), exception=traceback.format_exc()) + + if rc != 0: # something went wrong emit the msg + module.fail_json(rc=rc, msg=result) + else: + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/roles/vm/guest/deploy/defaults/main.yml b/roles/vm/guest/deploy/defaults/main.yml new file mode 100644 index 00000000..c067ecbc --- /dev/null +++ b/roles/vm/guest/deploy/defaults/main.yml @@ -0,0 +1,7 @@ +--- +vm_deploy_decompressors: + "application/octet-stream": cat + "application/gzip": gunzip + "application/zstd": unzstd + "application/x-bzip2": bunzip2 + "application/x-xz": unxz diff --git a/roles/vm/guest/deploy/tasks/main.yml b/roles/vm/guest/deploy/tasks/main.yml new file mode 100644 index 00000000..cc9733ba --- /dev/null +++ b/roles/vm/guest/deploy/tasks/main.yml @@ -0,0 +1,26 @@ +--- +- name: fetch infos from image file + delegate_to: localhost + stat: + path: "{{ output_images | first }}" + get_mime: yes + register: output_image_info + + ## TODO: prepare directory for disk images + ## TODO: compute disk path based on install.disks.primary and install.disks.(scsi|virtio) + ## TODO: actually call this directly and make file replacement atomic!! +- name: copy disk image + pause: + prompt: | + Please copy the image to the vm-host i.e. by calling this: + + cat {{ output_image_info.stat.path }} | {{ vm_deploy_decompressors[output_image_info.stat.mimetype] }} | ssh {{ vm_host.name }} 'cat > {{ install.disks.scsi.sda.path }}' + + When done press ENTER to continue or CTRL-C then A to abort. + +- name: define vm + vars: + vm_define_installer: no + delegate_to: "{{ vm_host.name }}" + import_role: + name: vm/guest/define diff --git a/roles/vm/guest/install/library/wait_for_virt.py b/roles/vm/guest/install/library/wait_for_virt.py deleted file mode 100644 index 6c49fae1..00000000 --- a/roles/vm/guest/install/library/wait_for_virt.py +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -import traceback -import time - -try: - import libvirt -except ImportError: - HAS_VIRT = False -else: - HAS_VIRT = True - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native - - -VIRT_FAILED = 1 -VIRT_SUCCESS = 0 -VIRT_UNAVAILABLE = 2 - -VIRT_STATE_NAME_MAP = { - 0: "running", - 1: "running", - 2: "running", - 3: "paused", - 4: "shutdown", - 5: "shutdown", - 6: "crashed" -} - - -class VMNotFound(Exception): - pass - - -class LibvirtConnection(object): - - def __init__(self, uri, module): - - self.module = module - - cmd = "uname -r" - rc, stdout, stderr = self.module.run_command(cmd) - - if "xen" in stdout: - conn = libvirt.open(None) - elif "esx" in uri: - auth = [[libvirt.VIR_CRED_AUTHNAME, libvirt.VIR_CRED_NOECHOPROMPT], [], None] - conn = libvirt.openAuth(uri, auth) - else: - conn = libvirt.open(uri) - - if not conn: - raise Exception("hypervisor connection failure") - - self.conn = conn - - def find_vm(self, vmid): - """ - Extra bonus feature: vmid = -1 returns a list of everything - """ - conn = self.conn - - vms = [] - - # this block of code borrowed from virt-manager: - # get working domain's name - ids = conn.listDomainsID() - for id in ids: - vm = conn.lookupByID(id) - vms.append(vm) - # get defined domain - names = conn.listDefinedDomains() - for name in names: - vm = conn.lookupByName(name) - vms.append(vm) - - if vmid == -1: - return vms - - for vm in vms: - if vm.name() == vmid: - return vm - - raise VMNotFound("virtual machine %s not found" % vmid) - - def get_status(self, vmid): - state = self.find_vm(vmid).info()[0] - return VIRT_STATE_NAME_MAP.get(state, "unknown") - - -class Virt(object): - - def __init__(self, uri, module): - self.module = module - self.uri = uri - - def __get_conn(self): - self.conn = LibvirtConnection(self.uri, self.module) - return self.conn - - def status(self, vmid): - """ - Return a state suitable for server consumption. Aka, codes.py values, not XM output. - """ - self.__get_conn() - return self.conn.get_status(vmid) - - -def core(module): - - states = module.params.get('states', None) - guest = module.params.get('name', None) - uri = module.params.get('uri', None) - delay = module.params.get('delay', None) - sleep = module.params.get('sleep', None) - timeout = module.params.get('timeout', None) - - v = Virt(uri, module) - res = {'changed': False, 'failed': True} - - if delay > 0: - time.sleep(delay) - - for _ in range(0, timeout, sleep): - state = v.status(guest) - if state in states: - res['state'] = state - res['failed'] = False - res['msg'] = "guest '%s' has reached state: %s" % (guest, state) - return VIRT_SUCCESS, res - - time.sleep(sleep) - - res['msg'] = "timeout waiting for guest '%s' to reach one of states: %s" % (guest, ', '.join(states)) - return VIRT_FAILED, res - - -def main(): - - module = AnsibleModule(argument_spec=dict( - name=dict(aliases=['guest'], required=True), - states=dict(type='list', required=True), - uri=dict(default='qemu:///system'), - delay=dict(type='int', default=0), - sleep=dict(type='int', default=1), - timeout=dict(type='int', default=300), - )) - - if not HAS_VIRT: - module.fail_json( - msg='The `libvirt` module is not importable. Check the requirements.' - ) - - for state in module.params.get('states', None): - if state not in set(VIRT_STATE_NAME_MAP.values()): - module.fail_json( - msg="states contains invalid state '%s', must be one of %s" % (state, ', '.join(set(VIRT_STATE_NAME_MAP.values()))) - ) - - rc = VIRT_SUCCESS - try: - rc, result = core(module) - except Exception as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - if rc != 0: # something went wrong emit the msg - module.fail_json(rc=rc, msg=result) - else: - module.exit_json(**result) - - -if __name__ == '__main__': - main() -- cgit v1.2.3 From 50901c755fdf7db15dc2cc86671589fa1e1b58f5 Mon Sep 17 00:00:00 2001 From: Christian Pointner Date: Tue, 4 Jan 2022 12:24:18 +0100 Subject: add some todo docs --- roles/vm/guest/deploy/tasks/main.yml | 1 + 1 file changed, 1 insertion(+) (limited to 'roles/vm') diff --git a/roles/vm/guest/deploy/tasks/main.yml b/roles/vm/guest/deploy/tasks/main.yml index cc9733ba..f22af8af 100644 --- a/roles/vm/guest/deploy/tasks/main.yml +++ b/roles/vm/guest/deploy/tasks/main.yml @@ -9,6 +9,7 @@ ## TODO: prepare directory for disk images ## TODO: compute disk path based on install.disks.primary and install.disks.(scsi|virtio) ## TODO: actually call this directly and make file replacement atomic!! + ## Probably by writing and action plugin based on this: https://github.com/socratesx/Ansible-Decompress - name: copy disk image pause: prompt: | -- cgit v1.2.3