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.
We can open two windows, connect to the same server and test the game.