Automation Blog

from Stefan Schnell

WebAssembly (Wasm) is a binary instruction format and it is designed as a portable compilation for different targets. This means different architectures, different operating systems and different execution environments like browser, desktop or server - one for all. This makes Wasm as a very interesting approach - also in the context of automation projects.

Wasmtime is a standalone fast and secure runtime for WebAssembly, WASI and the Component Model by the Bytecode Alliance. It allows to execute Wasm modules without further dependencies.

Ansible is an IT automation engine that automates IT processes like provisioning, configuration management, application deployment, orchestration and many other. It is widely used in many many companies.

This post describes an approach how to embed and use a WebAssembly module via Wasmtime with Ansible.

Execute Wasm with Ansible

Role

An Ansible role is used to implement this approach. We begin with the structure of the role and the files it contains.
The necessary Python packages and the compiled WebAssembly module are stored in the files subdirectory.

.
├── roles
│   └── wasmHello
│       ├── README.md
│       ├── files
│       │   ├── hello.rs.wasm
│       │   ├── importlib_resources-6.5.2-py3-none-any.whl
│       │   ├── pip-25.2-py3-none-any.whl
│       │   └── wasmtime-36.0.0-py3-none-manylinux1_x86_64.whl
│       ├── tasks
│       │   └── main.yml
│       └── vars
│           └── main.yml
└── wasmHello.yml

WebAssembly Module

Here the Rust source code hello.rs, which is compiled to hello.rs.wasm - the WebAssembly module.
It contains three functions. The hello function delivers a Hello World message, depending on whether a name is passed as a parameter. If a name is passed it is used, otherwise a standard text. Also there are a malloc and free function, to allocate memory and to free it. These functions are necessary to enable data transfer to the WebAssembly module. To compile it call rustc --target wasm32-unknown-unknown -O --crate-type=cdylib hello.rs -o hello.rs.wasm

use std::*;

#[no_mangle]
pub extern "C" fn malloc(len: u32) -> *mut u8 {
  let mut buf = Vec::with_capacity(len as usize);
  let ptr = buf.as_mut_ptr();
  mem::forget(buf);
  ptr
}

#[no_mangle]
pub unsafe extern "C" fn free(ptr: &mut u8, len: u32) {
  let _ = Vec::from_raw_parts(ptr, 0, len as usize);
}

#[no_mangle]
pub extern "C" fn hello(name: u8, len: u32, result: *mut u8) {
  let bytes = unsafe {
    slice::from_raw_parts(name as *const u8, len as usize)
  };
  let str_name = str::from_utf8(bytes).unwrap().trim();
  let mut out_text = "".to_string();
  if len == 0 {
    out_text = "Hello World from WebAssembly in Rust Language".to_string();
  } else {
    out_text = "Hello ".to_string() + str_name +
      " from WebAssembly in Rust Language";
  }
  let out = out_text.as_bytes();
  unsafe {
    std::ptr::copy(out.as_ptr().cast(), result, out.len());
  }
}

Variables

The file main.yml in the vars subdirectory contains the Python source code. It is assigned to a variable so that it can later be passed to the Python interpreter.
First the WebAssembly module is instantiated, then functions and memory are detected. The variables for the name and for the return value are created in memory. Then the function is executed and the return value is read from memory. Finally the allocated memory is freed and the return value is output.

---
# vars/main.yml

hello_wasm_python_code: |
  import os
  import tempfile
  import wasmtime
  
  def main():
  
      try:
      
          wasmFileName: str = tempfile.gettempdir() + "/hello.rs.wasm"
      
          wasmStore = wasmtime.Store()
          wasmModule = wasmtime.Module.from_file(
              wasmStore.engine,
              wasmFileName
          )
          wasmInstance = wasmtime.Instance(wasmStore, wasmModule, [])
      
          wasmFuncHello = wasmInstance.exports(wasmStore)["hello"]
          wasmFuncMalloc = wasmInstance.exports(wasmStore)["malloc"]
          wasmFuncFree = wasmInstance.exports(wasmStore)["free"]
          wasmMemory = wasmInstance.exports(wasmStore)["memory"]
      
          wasmVarName = bytearray("{{ var_name }}".encode("utf-8"))
          wasmVarPtrName = wasmFuncMalloc(wasmStore, len(wasmVarName))
          wasmMemory.write(wasmStore, wasmVarName, wasmVarPtrName)
      
          wasmVarPtrResult = wasmFuncMalloc(wasmStore, 128)
      
          wasmFuncHello(
              wasmStore,
              wasmVarPtrName,
              len(wasmVarName),
              wasmVarPtrResult
          )
      
          wasmResult = wasmMemory.read(
              wasmStore,
              wasmVarPtrResult,
              wasmVarPtrResult + 128
          )
      
          wasmFuncFree(wasmStore, wasmVarPtrResult, 128)
          wasmFuncFree(wasmStore, wasmVarPtrName, len(wasmVarName))
      
          print(wasmResult.decode().rstrip("\x00"))
      
      except Exception as ex:
          print("An error occured:", repr(ex))
  
  if __name__ == "__main__":
      main()

Tasks

The file main.yml in the tasks subdirectory contains list of tasks that the role provides to the play for execution.
At the init block the Python packages are copied to the temporary directory and installed. Also it copies the WebAssembly module to the temporary directory. At the main block the WebAssembly function is executed and the return value is displayed. In the Done block, the Python packages are uninstalled and all files are deleted.

---
# tasks/main.yml

- name: Init # ---------------------------------------------------------
  any_errors_fatal: true
  block:

    - name: Write pip wheel
      ansible.builtin.copy:
        src: "{{ pip }}"
        dest: "/tmp/{{ pip }}"
        mode: "0644"
        force: false

    - name: Write importlib_resources wheel
      ansible.builtin.copy:
        src: "{{ importlib_resources }}"
        dest: "/tmp/{{ importlib_resources }}"
        mode: "0644"
        force: false

    - name: Write wasmtime wheel
      ansible.builtin.copy:
        src: "{{ wasmtime }}"
        dest: "/tmp/{{ wasmtime }}"
        mode: "0644"
        force: false

    - name: Install Wasmtime
      block:

        - name: Install importlib_resources
          ansible.builtin.command:
            cmd: >-
              /usr/bin/python3 /tmp/{{ pip }}/pip install --no-index
              /tmp/{{ importlib_resources }} --break-system-packages
          register: install_importlib_resources
          failed_when: install_importlib_resources.rc != 0

        - name: Show result of install importlib_resources
          ansible.builtin.debug:
            var: install_importlib_resources.stdout_lines

        - name: Install wasmtime
          ansible.builtin.command:
            cmd: >-
              /usr/bin/python3 /tmp/{{ pip }}/pip install --no-index
              /tmp/{{ wasmtime }} --break-system-packages
          register: install_wasmtime
          failed_when: install_wasmtime.rc != 0

        - name: Show result of install wasmtime
          ansible.builtin.debug:
            var: install_wasmtime.stdout_lines

        - name: Delete wasmtime wheel
          ansible.builtin.file:
            path: "/tmp/{{ wasmtime }}"
            state: absent
            force: true

        - name: Delete importlib_resources wheel
          ansible.builtin.file:
            path: "/tmp/{{ importlib_resources }}"
            state: absent
            force: true

    - name: Write WebAssembly module
      ansible.builtin.copy:
        src: hello.rs.wasm
        dest: /tmp/hello.rs.wasm
        mode: "0644"
        force: false

- name: Main # ---------------------------------------------------------
  any_errors_fatal: true
  block:

    - name: Execute Python Script
      ansible.builtin.command:
        cmd: "/usr/bin/python3 -c '{{ hello_wasm_python_code }}'"
      changed_when: execute_script.rc == 0
      register: execute_script

    - name: Show output
      ansible.builtin.debug:
        var: execute_script.stdout

  always:

    - name: Done # -----------------------------------------------------
      block:

        - name: Uninstall wasmtime, delete pip wheel and WebAssembly module
          block:

            - name: Uninstall wasmtime
              ansible.builtin.command:
                cmd: >-
                  /usr/bin/python3 /tmp/{{ pip }}/pip uninstall
                  wasmtime --yes --break-system-packages
              register: uninstall_wasmtime
              changed_when: uninstall_wasmtime.rc == 0

            - name: Show result of uninstall wasmtime
              ansible.builtin.debug:
                var: uninstall_wasmtime.stdout_lines

            - name: Uninstall importlib_resources
              ansible.builtin.command:
                cmd: >-
                  /usr/bin/python3 /tmp/{{ pip }}/pip uninstall
                  importlib_resources --yes --break-system-packages
              register: uninstall_importlib_resources
              changed_when: uninstall_importlib_resources.rc == 0

            - name: Show result of uninstall importlib_resources
              ansible.builtin.debug:
                var: uninstall_importlib_resources.stdout_lines

            - name: Delete pip wheel
              ansible.builtin.file:
                path: "/tmp/{{ pip }}"
                state: absent
                force: true

            - name: Delete WebAssembly module
              ansible.builtin.file:
                path: /tmp/hello.rs.wasm
                state: absent
                force: true

Playbook

The playbook wasmHello.yml contains a few variables, the name and the names for the Python packages - this makes it easier to use new releases. It loads and executes the role.

---
# wasmHello.yml

- name: Test playbook which install Wasmtime and use WebAssembly module
  hosts: localhost
  gather_facts: false

  vars:

    pip: pip-25.2-py3-none-any.whl
    importlib_resources: importlib_resources-6.5.2-py3-none-any.whl
    wasmtime: wasmtime-36.0.0-py3-none-manylinux1_x86_64.whl

    var_name: Stefan

  tasks:

    - name: Include wasm role
      ansible.builtin.include_role:
        name: wasmHello

Output

Here is the result as a detailed output of the playbook execution. Line 61 shows the result of the WebAssembly function.

stefan@ubuntu:~$ ansible-playbook wasmHello.yml

PLAY [Test playbook which install Wasmtime and use WebAssembly module] *

TASK [Include wasm role] ***********************************************
included: wasmHello for localhost

TASK [wasmHello : Write pip wheel] *************************************
changed: [localhost]

TASK [wasmHello : Write importlib_resources wheel] *********************
changed: [localhost]

TASK [wasmHello : Write wasmtime wheel] ********************************
changed: [localhost]

TASK [wasmHello : Install importlib_resources] *************************
changed: [localhost]

TASK [wasmHello : Show result of install importlib_resources] **********
ok: [localhost] => {
    "install_importlib_resources.stdout_lines": [
        "Defaulting to user installation because normal site-packages
         is not writeable",
        "Processing /tmp/importlib_resources-6.5.2-py3-none-any.whl",
        "Installing collected packages: importlib-resources",
        "Successfully installed importlib-resources-6.5.2"
    ]
}

TASK [wasmHello : Install wasmtime] ************************************
changed: [localhost]

TASK [wasmHello : Show result of install wasmtime] *********************
ok: [localhost] => {
    "install_wasmtime.stdout_lines": [
        "Defaulting to user installation because normal site-packages
         is not writeable",
        "Processing /tmp/wasmtime-36.0.0-py3-none-manylinux1_x86_64.whl",
        "Requirement already satisfied: importlib_resources>=5.10
         (from wasmtime==36.0.0) (6.5.2)",
        "Installing collected packages: wasmtime",
        "Successfully installed wasmtime-36.0.0"
    ]
}

TASK [wasmHello : Delete wasmtime wheel] *******************************
changed: [localhost]

TASK [wasmHello : Delete importlib_resources wheel] ********************
changed: [localhost]

TASK [wasmHello : Write WebAssembly module] ****************************
changed: [localhost]

TASK [wasmHello : Execute Python Script] *******************************
changed: [localhost]

TASK [wasmHello : Show output] *****************************************
ok: [localhost] => {
    "execute_script.stdout": "Hello Stefan from WebAssembly in Rust Language"
}

TASK [wasmHello : Uninstall wasmtime] **********************************
changed: [localhost]

TASK [wasmHello : Show result of uninstall wasmtime] *******************
ok: [localhost] => {
    "uninstall_wasmtime.stdout_lines": [
        "Found existing installation: wasmtime 36.0.0",
        "Uninstalling wasmtime-36.0.0:",
        "  Successfully uninstalled wasmtime-36.0.0"
    ]
}

TASK [wasmHello : Uninstall importlib_resources] ***********************
changed: [localhost]

TASK [wasmHello : Show result of uninstall importlib_resources] ********
ok: [localhost] => {
    "uninstall_importlib_resources.stdout_lines": [
        "Found existing installation: importlib_resources 6.5.2",
        "Uninstalling importlib_resources-6.5.2:",
        "  Successfully uninstalled importlib_resources-6.5.2"
    ]
}

TASK [wasmHello : Delete pip wheel] ************************************
changed: [localhost]

TASK [wasmHello : Delete WebAssembly module] ***************************
changed: [localhost]

PLAY RECAP *************************************************************
localhost:
ok=19  changed=13  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

Conclusion

In summary, it can be stated that it is possible to use WebAssembly with Ansible. The Ansible role concept makes it very easy to provision the necessary files. This opens up new integration possibilities for automation with Ansible.