An initial Approach to deliver an X11 Application as a Web Service
By: Florian Held
Tags: Simulation, Ansible, X11, Openstack, Web

An initial Approach to deliver an X11 Application as a Web Service Florian Held Follow Jun 8 · 8 min read

Photo by Markus Spiske on Unsplash

In my previous blog post, I talked about bringing a complex hydromechanical simulation onto a laptop for the purpose of presenting it to pupils visiting 11th grade in a technical grammar school in Germany. Though I came up with a technically feasible solution, it was clear from the beginning that this wasn’t the best fit for distributing this software to pupils willing to experiment and conducting their own simulations. The option to redundantly install this software “bare-metal” on machines used by the pupils cannot be seen as best practice since at least a decade due to various problems like incompatible library dependencies or an a priori impossibility to port it to a certain operating system. Another option, to install a container runtime, bundle this application and its dependencies into an image and then somehow connecting the graphical display to the running container also seemed more like a bulky hack than an elegant solution. Clearly, a better solution would be to provide this simulation application as some sort of an easily deployable and manageable web service, accessible from a webbrowser and rolled out at e.g. a webhoster or cloud.

However, implementing such a solution, is far away from being straight-forward and comes with various challenges. In the following, I want to elaborate on first ideas and steps to overcome those and present an initial solution.

Challenges

The biggest challenge was to find a tool to port a rather complex X11 application, which:

I forked and customized

is based on the cmake build-system

uses specialized libraries like gomp (i.e. an OpenMP implementation for C++)

to a web service. This raised the question to go for a typical solution with underlying webserver technology like nginx and wrapping the simulation code into e.g. webassembly bytecode for delivery in a webbrowser or simply go for some sort of remote-display server. Another indirect restriction was, that development effort should not be too big, as I worked on this project on my own and only part time.

Another point to consider was, that this solution should be scalable to some degree. As a rough estimator, 15 pupils at the same time should be able to use this application without disturbance.

The solution should also be constructed in such a way that it would be usable by the said pupils without presuming some deep technical background.

Eventually, for me as the operator, ease of deployment and maintainability of the solution should be strived for.

Used Tools

As a technology for porting the application, webassembly and the associated emscripten toolchain seemed like a promising fit at first, as native c++ code could be provided as binary-code to run in modern webbrowsers overcoming javascript limitations for cpu-intensive tasks like performing a fluid simulation. And with emscripten it is principally possible to execute the associated cmake build chain, the whole application is based on. However, the foundation of webassembly was not built for rendering X11 applications in webbrowsers, though there seem to be some hacky workarounds possible, copying X11 headers and manually linking X11 libraries. However, building such a “solution” for my fluid simulation was everything but straight-forward as additional conflicts like the unability to cross-compile OpenMP libraries occurred which eventually led to the conclusion that this approach was too tedious and not expedient. And what was more, a lacking first impression of the rendering experience in the browser and the prospect of work for creating a fancy front-end strengthened this initial rejection for my use case.

So I went on looking for a solution that relieved me off those obstacles and provide some sort of a “cloned experience” of the terminal controlled simulation approach used on my laptop. Eventually, my research brought me to a solution providing a remote display (and thus the ability to interact with the application with a console like xterm) called xpra. On its web page it is defined as: Xpra is an open-source multi-platform persistent remote display server and client for forwarding applications and desktop screens. Together with the tool xpra-html5 it was possible to forward the xterm console running on a machine on the web to a webbrowser connected to it. Summarized, this approach gave me a one-to-one match with my original case developed for use on my laptop.

To tackle challenges like providing a scalable solution, ease of deployment and maintainability, I went for the cloud solution openstack. Reasons for this were, that cloud solutions per-se support those concepts by means of virtualization or providing APIs to smoothly interact with deployments and also because of an openstack infrastructure being available at our institute. For automated provisioning of the xpra server instances I went for ansible due to own experiences with this tool and its tight integration with openstack. Moreover due to the automation capabilities of ansible it was possible to provide resources on the xpra servers to give the pupils a better user experience and make the application easier to handle and comprehend.

Implementation Approach

To be able to provision xpra server instances, first some prerequisites had to be met. First, authorized access to relevant resources was needed, that is the source code of the forked original hydromechanical fluid simulation at the moment residing at our institute’s gitlab instance and also resources helping pupils to understand and execute the simulation (simulation-manual, theoretical background, plots, …) also residing at the institute’s gitlab.

To map the requirements for application delivery to ansible’s ecosystem, following project structure was chosen:

ansible/

├── ansible.cfg

├── ansible-secrets

│ └── secrets.yml

├── inventories

│ ├── development

│ │ ├── group_vars

│ │ │ └── all

│ │ └── hosts

│ ├── integration

│ │ ...

│ │

│ └── production

│ ...

│

├── pa-token

├── playbooks

│ ├── delete_full_stack.yml

│ ├── deploy_full_stack.yml

│ ├── roles

│ │ └── requirements.yml

│ └── tasks

│ ├── add_sim_folder_to_sim_group.yml

│ ├── create_meta_content_for_sim_user.yml

│ ├── install_app_apt_part.yml

│ ├── install_app_raw_part.yml

│ ├── make_sim_folder_accessible_to_sim_user.yml

│ ├── setup_vm.yml

│ ├── start_xpra.yml

│ ├── stop_server.yml

│ ├── uninstall_app_apt.yml

│ └── uninstall_app_raw.yml

├── requirements.txt

├── roles

│ ├── clone_repo

│ │ ├── defaults

│ │ │ └── main.yml

│ │ ├── handlers

│ │ │ └── main.yml

│ │ ├── meta

│ │ │ └── main.yml

│ │ ├── README.md

│ │ ├── tasks

│ │ │ └── main.yml

│ │ ├── tests

│ │ │ ├── inventory

│ │ │ └── test.yml

│ │ └── vars

│ │ └── main.yml

│ ├── create_group

│ │ │ ...

│ │

│ ├── create_network

│ │ │ ...

│ │

│ ├── create_router

│ │ │ ...

│ │

│ ├── create_rule

│ │ │ ...

│ │

│ ├── create_security_group

│ │ │ ...

│ │

│ ├── create_subnet

│ │ │ ...

│ │

│ ├── create_user

│ │ │ ...

│ │

│ ├── delete_vm

│ │ │ ...

│ │

│ ├── deploy_vm

│ │ │ ...

│ │

│ └── wait_for_port1_ready

│ │ ...

│

└── site.yml

The provisioning flow is controlled in the deploy_full_stack.yml playbook:

---

- hosts: localhost

gather_facts: false

vars:

register_upstream: []

tasks:

- name: create network

vars:

nw_name: "{{ nw_name_par }}"

include_role:

name: create_network

- name: create subnet

vars:

subnet_name: "{{ subnet_name_par }}"

nw_id: "{{ hostvars['localhost']['bodensee_private_net'].id }}"

include_role:

name: create_subnet

- name: create router

vars:

router_name: "{{ router_name_par }}"

include_role:

name: create_router

- name: create security group

vars:

sec_group_name: "{{ sec_group_name_bodensee }}"

include_role:

name: create_security_group

- name: create security group rule

vars:

sec_group_name: "{{ sec_group_name_bodensee }}"

port_nmbr: "{{ port_nmbr_ssh }}"

include_role:

name: create_rule

- name: Deploy vm

vars:

sec_group_list: ["{{ sec_group_1 }}","{{ sec_group_2 }}"]

include_role:

name: deploy_vm

register: app_host

- name: Add vm to inventory

add_host: name="{{ item.server.name }}" groups=bodensee_hosts ansible_ssh_host="{{ item.server.accessIPv4 }}" ansible_all_ipv4_addresses=["{{ item.server.private_v4 }}"] ansible_ssh_private_key_file="{{ key_file_path }}"

with_items:

- "{{ register_upstream }}"



- hosts: bodensee

remote_user: ubuntu

gather_facts: false

tasks:

- name: wait for ssh port to be ready

include_role:

name: wait_for_port1_ready

- hosts: bodensee

remote_user: ubuntu

become: yes

gather_facts: false

tasks:

- import_tasks: tasks/setup_vm.yml

- hosts: bodensee

remote_user: ubuntu

become: yes

gather_facts: false

vars:

personal_access_token: "token"

repo_name: "SPlishSplash"

repo_uri: https://gitlab-ci-token:{{ personal_access_token }}@{{ gitlab-server-uri }}/simulierte-welten/projekte/wasserschicht_dynamik/SPlisHSPlasH_mod_1.git

dest_folder: "{{ sim_folder_su }}"

tasks:

- name: clone SPlishSplash

include_role:

name: clone_repo

- hosts: bodensee

remote_user: ubuntu

become: yes

gather_facts: false

vars:

repo_name: "xpra-html5"

repo_uri: https://github.com/Xpra-org/xpra-html5

dest_folder: "{{ xpra_html5_folder_su }}"

tasks:

- name: clone xpra-html5

include_role:

name: clone_repo

- hosts: bodensee

remote_user: ubuntu

become: yes

gather_facts: false

pre_tasks:

- import_tasks: tasks/install_app_apt_part.yml

tasks:

- import_tasks: tasks/install_app_raw_part.yml

- hosts: bodensee

remote_user: ubuntu

become: yes

gather_facts: false

vars:

append_user: "ubuntu"

tasks:

- name: "create group {{ group_name }}"

include_role:

name: create_group

- name: "create user {{ user_name }} under group {{ group_name }}"

include_role:

name: create_user

- name: "append {{ append_user }} to group {{ group_name }}"

user:

name: "{{ append_user }}"

groups: "{{ group_name }}"

append: yes

- import_tasks: tasks/add_sim_folder_to_sim_group.yml

- import_tasks: tasks/make_sim_folder_accessible_to_sim_user.yml

- hosts: bodensee

remote_user: ubuntu

become: yes

tasks:

- import_tasks: tasks/create_meta_content_for_sim_user.yml

- import_tasks: tasks/start_xpra.yml

What is basically done here is:

Setup a tailor-made virtual machine accessible from the public internet and waiting for its ssh-port to become ready.

Cloning relevant git-repositories and building and installing them later alongside dependent other software packages

Creating a non-privileged sim-user able to access content relevant for simulation execution

Copying meta-content having educational purposes for the pupils onto the machine.

Starting the xpra server

The provisioning process can be triggered with a command similar to this:

$ export ANSIBLE_HOST_KEY_CHECKING=False && ansible-playbook -vvv site.yml --extra-vars "inventories/development/group_vars/all deploy=1 personal_access_token=xxx key_file_path=xxx"

At the moment, the virtual machine is up and running in openstack, the user can authenticate with valid credentials over a webbrowser (replace localhost with the respective public IP):

Login mask for xpra server

Given access to the machine, the user can now work on an xterm console browsing through the simulation-manual (a pdf readable through the pre-installed reader evince) or looking at temperature plots (png images viewable through the pre-installed viewer qiv) . However, most importantly, a simulation run can be started, shown here exemplarily:

$ whoami

sim-user

$ pwd

/home/sim-user/simulation/bin

$ cat example-run

MESA_GL_VERSION_OVERRIDE=3.3 ./SPHSimulator --stopAt 42.0 ../data/Scenes/simu_bodensee/bodensee_scene_7_succeed.json

$ bash example-run

and should look similar to this screenshot from my personal Firefox 89.0 browser:

Simulation run in a webbrowser

Open Issues

Though this approach yielded a running and usable solution, it is far from having a production-like quality. What is to follow is a small excerpt of open issues still to overcome: