pabis.eu

Deploy SmartFox Server using Ansible - Part 3

01 February 2023

Previously we adapted our playbook to configure more aspects of SmartFox like ports, copying Zone files and extensions. In this post, let's make the playbook support more platforms, namely RedHat based systems, like AlmaLinux, and ARM CPU systems, like AWS Graviton. We will also fix an issue where SmartFox archive is downloaded again with each run of the playbook.

Previous post is available here.

Also check out the repository for this post.

Rules for downloading SmartFox archive

The first approach to this problem would be to check if any of the SmartFox files exist, as the case in unarchive.creates. However, that would be a blocker when we want to change the version that is installed for example from 2.15.0 to 2.17.0.

A better solution would be to detect currently installed version and compare it to the one in the variables. We can store the value ourselves in a file, like {{ sfs2x_directory }}/.version. But let's scan the archive for some matches (for version 2.18.0).

$ grep -r "2.18.0" | grep -v geo | grep -v svg
Binary file ./jre/lib/server/libjvm.so matches
Binary file ./jre/lib/modules matches
./.install4j/i4jparams.conf:  <general applicationName="SmartFoxServer 2X" applicationVersion="2.18.0" mediaSetId="156" applicationId="3273-5845-8099-7635" status="LCOK" mediaName="SFS2X_unix_2_18_0" jreVersion="11.0.13" minJavaVersion="1.8" publisherName="GOTOANDPLAY snc" publisherURL="www.smartfoxserver.com" jreSharingKey="" lzmaCompression="false" pack200Compression="false" installerType="1" addOnAppId="" suggestPreviousLocations="false" uninstallerFilename="uninstall" uninstallerDirectory="." />
./.install4j/i4jparams.conf:    <variable name="sys.version" value="2.18.0" />
./RELEASE-NOTES.html:       <em>Version 2.18.0</em><br>
[...]

The configuration for install4j seems like a good candidate for this task. For this we will use xml module with appropriate XPath and store the result in a temporary register: current_version. We need to match the first <general> node after <config> and attribute applicationVersion. But we also have to be sure that the target file exists, otherwise our check would fail.

---
- name: Check if .install4j/i4jparams.conf exists
  stat:
    path: "{{ smartfox_target_directory }}/SmartFoxServer_2X/.install4j/i4jparams.conf"
  register: i4j_stat
  changed_when: false

- name: Get current version of SmartFox from install4j
  xml:
    path: "{{ smartfox_target_directory }}/SmartFoxServer_2X/.install4j/i4jparams.conf"
    xpath: "/config/general"
    content: "attribute"
  when: i4j_stat.stat.exists
  register: i4j_conf

- name: Extract value from install4j
  set_fact:
    current_version: "{{ (i4j_conf.matches | first).general.applicationVersion }}"
  when: i4j_conf is defined and "count" in i4j_conf and i4j_conf.count > 0

We select XPath of /config/general and specify to capture only the attributes of the tag as opposed to its contents. In the next task we select the first match of the XPath search because what we get in return is an array. In XML there can be multiple <general> tags under <config>, even if for the application it would be incorrect. Filter | first returns a single object from the array and we use applicationVersion property to store the current version.

Because these tasks are complex, let's save them into a new file version_detect.yml and add them before import_tasks: download-smartfox in the playbook.

Next let's add inside download-smartfox.yml in both tasks a when clause and remove creates from unarchive:

[...]
  register: smartfox_archive_tmp
  when: current_version is not defined or current_version != smartfox_version
[...]
    mode: 0755
  when: current_version is not defined or current_version != smartfox_version

What is more to use xml module in Ansible we need lxml library on the remote host. We do this by ensuring that python3-pip is installed in the system and lxml is installed within pip. So in prepare-environment.yml:

- name: Install requirements (Debian/Ubuntu)
  apt:
    name:
      - unzip
      - acl
      - python3-pip
    state: present

- name: Install lxml
  pip:
    name: lxml
    state: present

Support for Debian-based and RedHat-based systems

First thing is to decouple all the tasks that are typical for Debian/Ubuntu based systems. To do this, we can utilize one of the facts exposed by Ansible - ansible_os_family. In file prepare-environment.yml we will change the task of installing package from apt to package - this will automatically select between apt on Debian-based systems and yum or dnf on RedHat-based systems. What is more with apt we have to update the package cache in a separate step. The rest of the file remains the same.

---
- name: Update apt cache (Debian/Ubuntu)
  apt:
    update_cache: yes
    cache_valid_time: 3600
  when: ansible_os_family == "Debian"

- name: Install required packages
  package:
    name:
      - unzip
      - acl
      - python3-pip
    state: present
[...]

Support for ARM64 processor

SmartFox by default is provided with x86_64 build of Java 11. However, it as Java slogan promises "Write once, run anywhere", we should be able to run it on ARM64. To do this we need to download ARM64 build of Java 11 either from archive or from the distribution package manager. We will use the latter approach for simplicity. Next we need to replace the default Java with the ARM64 one. We will also detect what is currently installed and replace it only if it is x86_64.

---
- name: Make sure Java is installed
  package:
    name: "{{ java_package_name }}"
    state: present

- name: Detect Java architecture
  shell:
    cmd: file -L {{ smartfox_target_directory }}/SmartFoxServer_2X/jre/bin/java
  register: java_arch

- name: Remove Java for x86_64
  file:
    path: "{{ smartfox_target_directory }}/SmartFoxServer_2X/jre"
    state: absent
  when: java_arch.stdout is defined and java_arch.stdout.find("x86-64") != -1

- name: Link Java for ARM
  file:
    src: "/usr/lib/jvm/{{ java_source }}"
    dest: "{{ smartfox_target_directory }}/SmartFoxServer_2X/jre"
    state: link
  when: java_arch.stdout is defined and java_arch.stdout.find("x86-64") != -1

For each distribution we will define Java package name and Java directory. For this we will create two files in vars - java-Debian.yml and java-RedHat.yml.

---
# RedHat
java_source: jre-11-openjdk
java_package_name: java-11-openjdk-headless
---
# Debian
java_source: java-11-openjdk-arm64
java_package_name: openjdk-11-jre-headless

Next we include the vars file in our playbook and add tasks for replacing Java but only when the architecture of the target is ARM64. We will do it just after downloading and extracting SmartFox.

[...]
  vars_files:
    - "vars/java-{{ ansible_os_family }}.yml"
    - vars/smartfox.yml

    [...]
    - import_tasks: tasks/download-smartfox.yml
    - import_tasks: tasks/replace-java.yml
      when: ansible_architecture == "aarch64"
    [...]

Some more prerequisites?

While testing the playbook on RockyLinux it turned out that lxml cannot be directly installed due to lacking packages. So let's specify another vars file packages-RedHat.yml and packages-Debian.yml, to make sure all the necessities are installed. Also we will move packages from prepare_environment.yml to packages-common.yml.

---
# RedHat
extra_packages:
  - libxml2-devel
  - libxslt-devel
  - gcc
  - python3-devel
# Debian
extra_packages:
  - libxml2-dev
  - libxslt-dev
# Common
common_packages:
  - unzip
  - acl
  - python3-pip

Then we will use list concatenation in the Install required packages task:

[...]
- name: Install required packages
  package:
    name: "{{ common_packages + extra_packages }}"
    state: present
[...]

And include in playbook new variable files: vars/packages-common.yml and "vars/packages-{{ ansible_os_family }}.yml".

What is more on RockyLinux I encountered another problem: building lxml required much more RAM than given and the task took a long time to complete. The first thing was to enable swap space - however this task should be out of scope of this playbook as it is tightly related to the specifications of the target. Another thing was that the build command took so long that Ansible was stuck forever waiting for the response (and connection through public internet might drop at any moment). To fix this, let's add async and poll to our task.

- name: Install lxml
  pip:
    name: lxml
    state: present
  poll: 10
  async: 900

This way, Ansible will verbosely report the status of the task every ten seconds or until 15 minutes have passed.

Testing

What is now left is to test things out. I modified my Tris client to allow me to type the server address. I also generated a zone file with a running SmartFox and deployed the file with Ansible. It's available in the repository as well.

First thing that we need to do is to allow unsafe TLS connections (over self-signed certificate) by visiting each SmartFox'es website and "accepting the risk". The browser will remember this decision also for our client.

Allow self-signed certificates

We can open two windows, connect to the same server and test the game.

Playing Tris