From: Christian Pointner Date: Sat, 2 Feb 2019 01:47:24 +0000 (+0100) Subject: Merge pull request #56 from realraum/update-vm-config X-Git-Url: https://git.realraum.at/?p=noc.git;a=commitdiff_plain;h=b17088ecb1fa96b3fb3903df0cf7d9f20c6963cb;hp=da2e17ab536fed1335a4d93b171c255e6f78514c Merge pull request #56 from realraum/update-vm-config Playbook for updating a VM's configuration --- diff --git a/ansible/host_vars/alfred/main.yml b/ansible/host_vars/alfred/main.yml index ab6ad85..ed73d0c 100644 --- a/ansible/host_vars/alfred/main.yml +++ b/ansible/host_vars/alfred/main.yml @@ -9,4 +9,5 @@ vm_host: gateway: "{{ net.mgmt.gw }}" nameservers: "{{ net.mgmt.dns }}" indices: + metrics: 74 testvm: 99 diff --git a/ansible/host_vars/metrics/main.yml b/ansible/host_vars/metrics/main.yml index 31b6b26..c1d5edf 100644 --- a/ansible/host_vars/metrics/main.yml +++ b/ansible/host_vars/metrics/main.yml @@ -1,2 +1,35 @@ --- localconfig_ssh_config_user: root + +vm_host: alfred + +install: + host: "{{ vm_host }}" + mem: 1024 + numcpu: 2 + disks: + primary: /dev/vda + virtio: + vda: + vg: "{{ vm_host }}" + lv: "{{ inventory_hostname }}" + size: 10g + vdb: + vg: "{{ vm_host }}" + lv: "{{ inventory_hostname }}-data" + + interfaces: + - bridge: "{{ hostvars[vm_host].vm_host.network.interface }}" + name: mgmt0 + autostart: True + +network: + nameservers: "{{ hostvars[vm_host].vm_host.network.nameservers }}" + domain: realraum.at + systemd_link: + interfaces: "{{ install.interfaces }}" + primary: + interface: mgmt0 + ip: "{{ (hostvars[vm_host].vm_host.network.ip+'/'+hostvars[vm_host].vm_host.network.mask) | ipaddr(hostvars[vm_host].vm_host.network.indices[inventory_hostname]) | ipaddr('address') }}" + mask: "{{ hostvars[vm_host].vm_host.network.mask }}" + gateway: "{{ hostvars[vm_host].vm_host.network.gateway | default(hostvars[vm_host].vm_host.network.ip) }}" diff --git a/ansible/library/wait_for_virt.py b/ansible/library/wait_for_virt.py new file mode 100644 index 0000000..6c49fae --- /dev/null +++ b/ansible/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/ansible/roles/vm/define/defaults/main.yml b/ansible/roles/vm/define/defaults/main.yml new file mode 100644 index 0000000..302256a --- /dev/null +++ b/ansible/roles/vm/define/defaults/main.yml @@ -0,0 +1,4 @@ +vm_define_autostart: "{{ not vm_define_installer and hostvars[hostname].install_cooked.autostart | default(False) }}" +vm_define_start: yes + +vm_define_installer: no diff --git a/ansible/roles/vm/define/tasks/main.yml b/ansible/roles/vm/define/tasks/main.yml new file mode 100644 index 0000000..5667658 --- /dev/null +++ b/ansible/roles/vm/define/tasks/main.yml @@ -0,0 +1,51 @@ +--- +- name: check if vm already exists + virt: + name: "{{ hostname }}" + command: info + register: vmhost_info + +- when: hostname in vmhost_info + block: + - name: destroy exisiting vm + virt: + name: "{{ hostname }}" + state: destroyed + + - name: wait for vm to be destroyed + wait_for_virt: + name: "{{ hostname }}" + states: shutdown,crashed + timeout: 5 + + - name: undefining exisiting vm + virt: + name: "{{ hostname }}" + command: undefine + + +- name: redefine vm + virt: + name: "{{ hostname }}" + command: define + xml: "{{ lookup('template', 'libvirt-domain.xml.j2') }}" + +- when: vm_define_start + block: + - name: start vm + virt: + name: "{{ hostname }}" + state: running + + - name: wait for VM to start + wait_for_virt: + name: "{{ hostname }}" + states: running + timeout: 10 + +- name: mark vm as autostarted + when: vm_define_autostart + virt: + name: "{{ hostname }}" + autostart: yes + command: info ## virt module needs either command or state diff --git a/ansible/roles/vm/define/templates/libvirt-domain.xml.j2 b/ansible/roles/vm/define/templates/libvirt-domain.xml.j2 new file mode 100644 index 0000000..152ef51 --- /dev/null +++ b/ansible/roles/vm/define/templates/libvirt-domain.xml.j2 @@ -0,0 +1,76 @@ + + {{ hostname }} + {{ hostvars[hostname].install_cooked.mem * 1024 }} + {{ hostvars[hostname].install_cooked.mem * 1024 }} + {{ hostvars[hostname].install_cooked.numcpu }} + + hvm +{% if vm_define_installer %} + {{ debian_installer_path }}/{{ install_distro }}-{{ install_codename }}/{{ hostvars[hostname].install_cooked.arch | default('amd64') }}/linux + {{ preseed_tmpdir }}/initrd.preseed.gz + console=ttyS0,115200n8 +{% endif %} + + + + + + + + + destroy +{% if vm_define_installer %} + destroy + destroy +{% else %} + restart + restart +{% endif %} + + /usr/bin/kvm + + + + + /dev/urandom + + +{% if 'virtio' in hostvars[hostname].install_cooked.disks %} +{% for device, lv in hostvars[hostname].install_cooked.disks.virtio.items() %} + + + + + +{% endfor %} +{% endif %} + +{% if 'scsi' in hostvars[hostname].install_cooked.disks %} + +{% for device, lv in hostvars[hostname].install_cooked.disks.scsi.items() %} + + + + + +{% endfor %} +{% endif %} + +{% if hostvars[hostname].install_cooked.interfaces %} +{% for if in hostvars[hostname].install_cooked.interfaces %} + + + +
+ +{% endfor %} +{% endif %} + + + + + + + + + diff --git a/ansible/roles/vm/install/library/wait_for_virt.py b/ansible/roles/vm/install/library/wait_for_virt.py deleted file mode 100644 index 6c49fae..0000000 --- a/ansible/roles/vm/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() diff --git a/ansible/roles/vm/install/tasks/main.yml b/ansible/roles/vm/install/tasks/main.yml index 5043fcc..e669fa3 100644 --- a/ansible/roles/vm/install/tasks/main.yml +++ b/ansible/roles/vm/install/tasks/main.yml @@ -6,30 +6,6 @@ lv: "{{ item.value.lv }}" size: "{{ item.value.size }}" -- name: check if vm already exists - virt: - name: "{{ hostname }}" - command: info - register: vmhost_info - -- block: - - name: destroy exisiting vm - virt: - name: "{{ hostname }}" - state: destroyed - - - name: wait for vm to be destroyed - wait_for_virt: - name: "{{ hostname }}" - states: shutdown,crashed - timeout: 5 - - - name: undefining exisiting vm - virt: - name: "{{ hostname }}" - command: undefine - - when: hostname in vmhost_info - block: - name: create a temporary workdir @@ -52,26 +28,12 @@ etype: user permissions: rx - - name: define new installer vm - virt: - name: "{{ hostname }}" - command: define - xml: "{{ lookup('template', 'libvirt-domain.xml.j2') }}" + - import_role: + name: vm/define vars: - run_installer: yes + vm_define_installer: yes preseed_tmpdir: "{{ tmpdir.path }}" - - name: start vm - virt: - name: "{{ hostname }}" - state: running - - - name: wait for installer to start - wait_for_virt: - name: "{{ hostname }}" - states: running - timeout: 10 - - debug: msg: "you can check on the status of the installer running this command 'virsh console {{ hostname }}' on host {{ inventory_hostname }}." @@ -83,33 +45,13 @@ register: installer_result failed_when: installer_result.failed or installer_result.state == "crashed" - - name: undefining installer vm - virt: - name: "{{ hostname }}" - command: undefine - always: - name: cleanup temporary workdir file: path: "{{ tmpdir.path }}" state: absent -- name: define new production vm - virt: - name: "{{ hostname }}" - command: define - xml: "{{ lookup('template', 'libvirt-domain.xml.j2') }}" +- import_role: + name: vm/define vars: - run_installer: no - -- name: start vm - virt: - name: "{{ hostname }}" - state: running - -- name: mark vm as autostarted - virt: - name: "{{ hostname }}" - autostart: "{{ hostvars[hostname].install_cooked.autostart }}" - command: info ## virt module needs either command or state - when: hostvars[hostname].install_cooked.autostart is defined + vm_define_installer: no diff --git a/ansible/roles/vm/install/templates/libvirt-domain.xml.j2 b/ansible/roles/vm/install/templates/libvirt-domain.xml.j2 deleted file mode 100644 index 9119f64..0000000 --- a/ansible/roles/vm/install/templates/libvirt-domain.xml.j2 +++ /dev/null @@ -1,76 +0,0 @@ - - {{ hostname }} - {{ hostvars[hostname].install_cooked.mem * 1024 }} - {{ hostvars[hostname].install_cooked.mem * 1024 }} - {{ hostvars[hostname].install_cooked.numcpu }} - - hvm -{% if run_installer %} - {{ debian_installer_path }}/{{ install_distro }}-{{ install_codename }}/{{ hostvars[hostname].install_cooked.arch | default('amd64') }}/linux - {{ preseed_tmpdir }}/initrd.preseed.gz - console=ttyS0,115200n8 -{% endif %} - - - - - - - - - destroy -{% if run_installer %} - destroy - destroy -{% else %} - restart - restart -{% endif %} - - /usr/bin/kvm - - - - - /dev/urandom - - -{% if 'virtio' in hostvars[hostname].install_cooked.disks %} -{% for device, lv in hostvars[hostname].install_cooked.disks.virtio.items() %} - - - - - -{% endfor %} -{% endif %} - -{% if 'scsi' in hostvars[hostname].install_cooked.disks %} - -{% for device, lv in hostvars[hostname].install_cooked.disks.scsi.items() %} - - - - - -{% endfor %} -{% endif %} - -{% if hostvars[hostname].install_cooked.interfaces %} -{% for if in hostvars[hostname].install_cooked.interfaces %} - - - -
- -{% endfor %} -{% endif %} - - - - - - - - - diff --git a/ansible/vm-update.yml b/ansible/vm-update.yml new file mode 100644 index 0000000..b40e06d --- /dev/null +++ b/ansible/vm-update.yml @@ -0,0 +1,51 @@ +--- +- name: preperations and sanity checks + hosts: "{{ hostname }}" + gather_facts: no + tasks: + - name: setup variables + set_fact: + network_cooked: "{{ network }}" + install_cooked: "{{ install }}" + + - name: create temporary host group for vm host + changed_when: no + add_host: + name: "{{ install_cooked.host }}" + inventory_dir: "{{inventory_dir}}" + group: _vmhost_ + # TODO: add some sanity checks + +- name: Update network configuration & shutdown VM + hosts: "{{ hostname }}" + roles: + - vm/network + + post_tasks: + - name: Shutdown VM + async: 1 + poll: 0 + ignore_errors: true + shell: sleep 2 && poweroff + +- name: Update VM definition + hosts: _vmhost_ + pre_tasks: + - name: Wait for VM to shut down + wait_for_virt: + name: "{{ hostname }}" + states: shutdown + timeout: 30 + + roles: + - vm/define + +- name: Apply the base vm/guest role + hosts: "{{ hostname }}" + pre_tasks: + - name: Wait for the VM to be up and running + wait_for_connection: + timeout: 120 + + roles: + - vm/guest