跳至内容

使用 Kubevirt

下面您可以看到一个使用Kubevirt 虚拟机作为测试主机的场景。为了使 Ansible 连接到 KubeVirt 虚拟机中的 SSH,它将通过 Service NodePort 变得可访问。当您运行molecule test --scenario-name kubevirt时,createconvergedestroy步骤将一个接一个地运行。

此示例使用 Ansible 剧本,不需要任何 molecule 插件即可运行。您可以完全控制需要安装哪些测试需求。

先决条件

create.ymldestroy.yml Ansible 剧本需要 Ansible 集合kubernetes.core。为了与 Kubernetes API 服务器进行无缝通信,该集合使用以下环境变量

  • K8S_AUTH_API_KEY:这是用于向 Kubernetes 集群进行身份验证的服务帐户令牌。

  • K8S_AUTH_HOST:这指向 Kubernetes 集群 API 的 URL。

  • K8S_AUTH_VERIFY_SSL:如果设置为false,这将禁用 SSL/TLS 证书的验证,这可能会带来安全风险。它主要用于测试环境,尤其是在处理自签名证书时。

此外,为了使剧本正常工作,Kubernetes 服务帐户需要特定的角色和角色绑定才能在特定命名空间中运行。这确保了剧本具有足够的权限来执行 Kubernetes 资源上的命令。这些角色包括获取、列出、监视、创建、删除和编辑虚拟机和服务。

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: <Molecule Kubernetes Serviceaccount>
  namespace: <Kubernetes VM Namespace>
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: <Kubernetes VM Namespace>
  name: <Molecule Kubernetes Role>
rules:
  - apiGroups: ["kubevirt.io"]
    resources: ["virtualmachines"]
    verbs: ["get", "list", "watch", "create", "delete", "patch", "edit"]
  - apiGroups: [""]
    resources: ["services"]
    verbs: ["get", "list", "watch", "create", "delete", "patch", "edit"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: <Molecule Kubernetes Rolebinding>
  namespace: <Kubernetes VM Namespace>
subjects:
  - kind: ServiceAccount
    name: <Molecule Kubernetes Serviceaccount>
    namespace: <Kubernetes VM Namespace>
roleRef:
  kind: Role
  name: <Molecule Kubernetes Role>
  apiGroup: rbac.authorization.k8s.io

您需要替换以下占位符

  • <Molecule Kubernetes Serviceaccount>:这是 molecule 测试用来创建 KubeVirt 虚拟机的 Kubernetes 服务帐户的名称。
  • <Kubernetes VM Namespace>:这是将实例化虚拟机的 Kubernetes 命名空间的名称。
  • <Molecule Kubernetes Role>:这是 Kubernetes 角色的名称,它封装了 molecule 测试正常运行所需的必要权限。
  • <Molecule Kubernetes Rolebinding>:这是 Kubernetes 角色绑定的名称,它将角色<Molecule Kubernetes Role>与服务帐户<Molecule Kubernetes Serviceaccount>关联。

注意事项

  • 此示例使用短暂虚拟机,这可以提高虚拟机创建和清理的速度。但是,需要注意的是,如果重新启动虚拟机,系统中的任何数据都不会保留。
  • 您无需担心设置 SSH 密钥。create.yml Ansible 剧本负责配置临时的 SSH 密钥。
  • 用于连接到虚拟机的 SSH 主机名是动态获取的,它是虚拟机正在运行的 K8S 节点。

配置剧本

molecule.yml
---
dependency:
  name: galaxy
  options:
    requirements-file: requirements.yml
    role-file: requirements.yml
platforms:
  - name: rhel9
    image: registry.redhat.io/rhel9/rhel-guest-image
    namespace: <Kubernetes VM Namespace>
    ssh_service:
      type: NodePort
    ansible_user: cloud-user
    memory: 1Gi
  - name: rhel8
    image: registry.redhat.io/rhel8/rhel-guest-image
    namespace: <Kubernetes VM Namespace>
    ssh_service:
      type: NodePort
    ansible_user: cloud-user
    memory: 1Gi
provisioner:
  name: ansible
  config_options:
    defaults:
      interpreter_python: auto_silent
      callback_whitelist: profile_tasks, timer, yaml
    ssh_connection:
      pipelining: false
  log: true
verifier:
  name: ansible
scenario:
  test_sequence:
    - dependency
    - destroy
    - syntax
    - create
    - converge
    - idempotence
    - side_effect
    - verify
    - destroy

请替换以下参数

  • <Kubernetes VM Namespace>:这应替换为要在其中创建 KubeVirt 虚拟机的 Kubernetes 命名空间。
requirements.yml
---
collections:
  - name: kubernetes.core
  - name: community.crypto

创建剧本

create.yml
- name: Create
  hosts: localhost
  connection: local
  gather_facts: false
  vars:
    temporary_ssh_key_size: 2048 # Variable for the size of the SSH key
  tasks:
    - name: Set default SSH key path # Sets the path of the SSH key
      ansible.builtin.set_fact:
        temporary_ssh_key_path: "{{ molecule_ephemeral_directory }}/identity_file"

    - name: Generate SSH key pair # Generates a new SSH key pair
      community.crypto.openssh_keypair:
        path: "{{ temporary_ssh_key_path }}"
        size: "{{ temporary_ssh_key_size }}"
      register: temporary_ssh_keypair # Stores the output of this task in a variable

    - name: Set SSH public key # Sets the SSH public key from the key pair
      ansible.builtin.set_fact:
        temporary_ssh_public_key: "{{ temporary_ssh_keypair.public_key }}"

    - name: Create VM in KubeVirt # Calls another file to create the VM in KubeVirt
      ansible.builtin.include_tasks: tasks/create_vm.yml
      loop: "{{ molecule_yml.platforms }}" # Loops over all platforms defined in molecule_yml
      loop_control:
        loop_var: vm # Sets the variable for the current item in the loop

    - name: Create Nodeport service if ssh_type is set to NodePort # Conditional block, executes if vm.ssh_service.type is NodePort
      when: "vm.ssh_service.type == 'NodePort'" # The block is executed when this condition is met
      block:
        - name: Create ssh NodePort Kubernetes Services # Creates a new NodePort service in Kubernetes
          kubernetes.core.k8s:
            state: present
            definition:
              apiVersion: v1
              kind: Service
              metadata:
                name: "{{ vm.name }}"
                namespace: "{{ vm.namespace }}"
              spec:
                ports:
                  - port: 22
                    protocol: TCP
                    targetPort: 22
                selector:
                  kubevirt.io/domain: "{{ vm.name }}"
                type: NodePort
          loop: "{{ molecule_yml.platforms }}" # Loops over all platforms defined in molecule_yml
          loop_control:
            loop_var: vm # Sets the variable for the current item in the loop

        - name: Retrieve Service Info # Retrieves information about the service
          kubernetes.core.k8s_info:
            api_version: v1
            kind: Service
            name: "{{ vm.name }}"
            namespace: "{{ vm.namespace }}"
          loop: "{{ molecule_yml.platforms }}" # Loops over all platforms defined in molecule_yml
          loop_control:
            loop_var: vm # Sets the variable for the current item in the loop
          register: node_port_services # Stores the output of this task in a variable

    - name: Create VM dictionary # Calls another file to create a dictionary with information about the VM
      ansible.builtin.include_tasks: tasks/create_vm_dictionary.yml
      loop: "{{ molecule_yml.platforms }}" # Loops over all platforms defined in molecule_yml
      loop_control:
        loop_var: vm # Sets the variable for the current item in the loop

    - name: Create ansible inventory from dictionary # Creates an Ansible inventory file from the dictionary
      vars:
        molecule_inventory:
          all:
            children:
              molecule:
                hosts: "{{ molecule_systems }}"
      ansible.builtin.copy:
        content: "{{ molecule_inventory | to_nice_yaml }}"
        dest: "{{ molecule_ephemeral_directory }}/inventory/molecule_inventory.yml"
        mode: "0600" # Sets the permissions of the file to -rw-------

    - name: Refresh inventory # Refreshes the inventory
      ansible.builtin.meta: refresh_inventory

    - name: Assert molecule group exists # Checks if the 'molecule' group exists in the inventory
      ansible.builtin.assert:
        that: "'molecule' in groups"
        fail_msg: "Molecule group was not found in inventory groups: {{ groups }}"
      run_once: true # noqa: run-once

- name: Validate that inventory was refreshed # New playbook to validate the inventory
  hosts: molecule # Runs on hosts in the 'molecule' group
  gather_facts: false # Disables fact gathering
  tasks:
    - name: Wait for the host to be reachable # Waits for the host to become reachable
      ansible.builtin.wait_for_connection:
        timeout: 120 # Waits for up to 120 seconds
tasks/create_vm.yml
---
- name: Create VM in KubeVirt
  kubernetes.core.k8s: # Uses the k8s module from the kubernetes.core Ansible collection
    state: present # Ensures the VM exists. If it doesn't, it will be created.
    definition:
      apiVersion: kubevirt.io/v1 # KubeVirt's API version
      kind: VirtualMachine # The type of Kubernetes resource to create
      metadata:
        labels:
          kubevirt.io/domain: "{{ vm.name }}" # Labels for the VM
        name: "{{ vm.name }}" # Name of the VM
        namespace: "{{ vm.namespace }}" # Namespace where the VM will be created
      spec:
        running: true # Starts the VM after creation
        template:
          metadata:
            labels:
              kubevirt.io/domain: "{{ vm.name }}" # Labels for the VM's template
          spec:
            domain:
              devices:
                disks:
                  - disk:
                      bus: virtio # Type of disk bus
                    name: containerdisk # Name of the container disk
                  - disk:
                      bus: virtio # Type of disk bus
                    name: cloudinitdisk # Name of the cloud-init disk
                  - name: emptydisk # Name of the empty disk
                    disk:
                      bus: virtio # Type of disk bus
              resources:
                requests:
                  memory: "{{ vm.memory | default('1Gi') }}" # Amount of memory requested for the VM
            volumes:
              - name: emptydisk
                emptyDisk:
                  capacity: "{{ vm.capacity | default('2Gi') }}" # Capacity of the empty ephemeral disk
              - containerDisk:
                  image: "{{ vm.image }}" # The image used for the container disk
                name: containerdisk
              - cloudInitNoCloud: # Cloud-init configuration
                  userData: | # User-data script
                    #cloud-config
                    preserve_hostname: true
                    hostname: "{{ vm.name }}"  # Sets the hostname
                    fqdn: "{{ vm.name }}"      # Fully Qualified Domain Name
                    prefer_fqdn_over_hostname: true
                    users:
                      - default
                      - name: {{ vm.ansible_user }}
                        lock_passwd: true   # Locks the password
                        ssh_authorized_keys:
                          - "{{ temporary_ssh_public_key }}"  # SSH public key
                    runcmd:
                      - [ sh, -c, "hostnamectl set-hostname {{ vm.name }}" ]  # Sets the hostname
                      - [ sudo, yum, install, -y, qemu-guest-agent ]  # Installs qemu-guest-agent
                      - [ sudo, systemctl, start, qemu-guest-agent ]  # Starts qemu-guest-agent
                name: cloudinitdisk

- name: Fetch VM pod info
  kubernetes.core.k8s_info:
    api_version: v1
    kind: Pod
    label_selectors:
      - "vm.kubevirt.io/name={{ vm.name }}"
    namespace: "{{ vm.namespace }}"
  register: vm_pod_info

- name: Extract the nodename from the VM pod info
  ansible.builtin.set_fact:
    nodeport_host: "{{ vm_pod_info.resources | map(attribute='spec.nodeName') | list | first }}"
tasks/create_vm_dictionary.yml
---
- name: Create VM dictionary
  vars:
    # This variable block is setting the `ssh_service_address` variable.
    # It first checks if the service type of the SSH service is 'NodePort'.
    # If it is, it retrieves the 'nodePort' from the services results.
    ssh_service_address: >-
      {%- set svc_type = vm.ssh_service.type | default(None) -%}
      {%- if svc_type == 'NodePort' -%}
        {{ (node_port_services.results | selectattr('vm.name', '==', vm.name) | first)['resources'][0]['spec']['ports'][0]['nodePort'] }}
      {%- endif -%}
  ansible.builtin.set_fact:
    # Here, the task is updating the `molecule_systems` dictionary with new VM information.
    # If `molecule_systems` doesn't exist, it is created as an empty dictionary.
    # Then it is combined with a new dictionary for the current VM, containing ansible connection details.
    molecule_systems: >-
      {{
        molecule_systems | default({}) | combine({
          vm.name: {
            'ansible_user': vm.ansible_user,
            'ansible_host': nodeport_host,
            'ansible_ssh_port': ssh_service_address,
            'ansible_ssh_private_key_file': temporary_ssh_key_path
          }
        })
      }}

收敛剧本

converge.yml
- name: Fail if molecule group is missing
  hosts: localhost
  tasks:
    - name: Print some info
      ansible.builtin.debug:
        msg: "{{ groups }}"

    - name: Assert group existence
      ansible.builtin.assert:
        that: "'molecule' in groups"
        fail_msg: |
          molecule group was not found inside inventory groups: {{ groups }}

- name: Converge
  hosts: molecule
  # We disable gather facts because it would fail due to our container not
  # having python installed. This will not prevent use from running 'raw'
  # commands. Most molecule users are expected to use containers that already
  # have python installed in order to avoid notable delays installing it.
  gather_facts: false
  tasks:
    - name: Check uname
      ansible.builtin.raw: uname -a
      register: result
      changed_when: false

    - name: Print some info
      ansible.builtin.assert:
        that: result.stdout | regex_search("^Linux")

销毁剧本

destroy.yml
---
- name: Destroy
  hosts: localhost
  connection: local
  gather_facts: false
  tasks:
    - name: Delete VM Instance in KubeVirt
      kubernetes.core.k8s:
        state: absent
        kind: VirtualMachine
        name: "{{ vm.name }}"
        namespace: "{{ vm.namespace }}"
      loop: "{{ molecule_yml.platforms }}"
      loop_control:
        loop_var: vm

    - name: Delete NodePort Service in KubeVirt
      kubernetes.core.k8s:
        state: absent
        kind: Service
        name: "{{ vm.name }}"
        namespace: "{{ vm.namespace }}"
      loop: "{{ molecule_yml.platforms }}"
      loop_control:
        loop_var: vm