使用 Kubevirt¶
下面您可以看到一个使用Kubevirt 虚拟机作为测试主机的场景。为了使 Ansible 连接到 KubeVirt 虚拟机中的 SSH,它将通过 Service NodePort 变得可访问。当您运行molecule test --scenario-name kubevirt
时,create
、converge
和destroy
步骤将一个接一个地运行。
此示例使用 Ansible 剧本,不需要任何 molecule 插件即可运行。您可以完全控制需要安装哪些测试需求。
先决条件¶
create.yml
和destroy.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 命名空间。
创建剧本¶
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