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.