<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://leo.leung.xyz/wiki/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Leo</id>
	<title>Leo&#039;s Notes - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://leo.leung.xyz/wiki/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Leo"/>
	<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/Special:Contributions/Leo"/>
	<updated>2026-04-29T23:52:31Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.43.6</generator>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=FreeIPA&amp;diff=7728</id>
		<title>FreeIPA</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=FreeIPA&amp;diff=7728"/>
		<updated>2026-04-07T06:00:20Z</updated>

		<summary type="html">&lt;p&gt;Leo: /* Configure the Samba server */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Installation ==&lt;br /&gt;
Here are my notes as I fumble my way setting up FreeIPA.&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
There is an official Docker container that has a complete FreeIPA installation. This container uses systemd to start up FreeIPA along with the other related services such as OpenLDAP, Bind, and Kerberos. See more at: https://github.com/freeipa/freeipa-container&lt;br /&gt;
&lt;br /&gt;
If you are using Docker, you &#039;&#039;&#039;must disable cgroup v2&#039;&#039;&#039; (this is enabled by default on RHEL9 and above). More about this in the Troubleshooting section below.&lt;br /&gt;
&lt;br /&gt;
Use the following &amp;lt;code&amp;gt;docker-compose.yml&amp;lt;/code&amp;gt; stack to quickly get started with FreeIPA:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = version: &#039;3.3&#039;&lt;br /&gt;
&lt;br /&gt;
services:&lt;br /&gt;
&lt;br /&gt;
  freeipa:&lt;br /&gt;
    image: freeipa/freeipa-server:rocky-8&lt;br /&gt;
    restart: unless-stopped&lt;br /&gt;
    tty: true&lt;br /&gt;
    stdin_open: true&lt;br /&gt;
    hostname: ipa&lt;br /&gt;
    domainname: home.steamr.com&lt;br /&gt;
    extra_hosts:&lt;br /&gt;
      - &amp;quot;ipa.home.steamr.com:10.1.2.12&amp;quot;&lt;br /&gt;
    environment:&lt;br /&gt;
      - IPA_SERVER_HOSTNAME=ipa.home.steamr.com&lt;br /&gt;
      - IPA_SERVER_IP=10.1.2.12&lt;br /&gt;
      - DNS=10.1.0.8&lt;br /&gt;
      - TZ=America/Edmonton&lt;br /&gt;
    command:&lt;br /&gt;
      - ipa-server-install&lt;br /&gt;
      - --realm=home.steamr.com&lt;br /&gt;
      - --domain=home.steamr.com&lt;br /&gt;
      - --ds-password=xxxxxxxxxx&lt;br /&gt;
      - --admin-password=xxxxxxxxxx&lt;br /&gt;
      - --no-host-dns&lt;br /&gt;
      - --setup-dns&lt;br /&gt;
      - --auto-forwarders&lt;br /&gt;
      - --allow-zone-overlap&lt;br /&gt;
      - --no-dnssec-validation&lt;br /&gt;
      - --unattended&lt;br /&gt;
    sysctls:&lt;br /&gt;
      - net.ipv6.conf.all.disable_ipv6=0&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ./data:/data&lt;br /&gt;
      - ./logs:/var/logs&lt;br /&gt;
      - /sys/fs/cgroup:/sys/fs/cgroup:ro&lt;br /&gt;
    tmpfs:&lt;br /&gt;
      - /run&lt;br /&gt;
      - /var/cache&lt;br /&gt;
      - /tmp&lt;br /&gt;
    cap_add:&lt;br /&gt;
      - SYS_TIME&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;10.1.2.12:80:80/tcp&amp;quot;&lt;br /&gt;
      - &amp;quot;10.1.2.12:443:443/tcp&amp;quot;&lt;br /&gt;
      # DNS&lt;br /&gt;
      - &amp;quot;10.1.2.12:53:53/tcp&amp;quot;&lt;br /&gt;
      - &amp;quot;10.1.2.12:53:53/udp&amp;quot;&lt;br /&gt;
      # LDAP(S)&lt;br /&gt;
      - &amp;quot;10.1.2.12:389:389/tcp&amp;quot;&lt;br /&gt;
      - &amp;quot;10.1.2.12:636:636/tcp&amp;quot;&lt;br /&gt;
      # Kerberos&lt;br /&gt;
      - &amp;quot;10.1.2.12:88:88/tcp&amp;quot;&lt;br /&gt;
      - &amp;quot;10.1.2.12:464:464/tcp&amp;quot;&lt;br /&gt;
      - &amp;quot;10.1.2.12:88:88/udp&amp;quot;&lt;br /&gt;
      - &amp;quot;10.1.2.12:464:464/udp&amp;quot;&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Samba integration ==&lt;br /&gt;
There are 3 methods to using FreeIPA with Samba. I eventually settled on method #2.&lt;br /&gt;
&lt;br /&gt;
# Configure Samba to use FreeIPA as a simple LDAP server, using &#039;&#039;&#039;ldapsam&#039;&#039;&#039; as the passdb backend. This requires a schema change to include the sambaSAMAccount and sambaGroupMapping, and sambaSID object classes. There is a DNA (distributed numeric assignment) plugin that can be used to update these fields.&lt;br /&gt;
# Configure Samba to use FreeIPA using &#039;&#039;&#039;ipasam&#039;&#039;&#039; as the passdb backend. This requires the &amp;lt;code&amp;gt;ipasam.so&amp;lt;/code&amp;gt; module installed on the samba servers.&lt;br /&gt;
# Configure Samba to use &#039;&#039;&#039;Kerberos&#039;&#039;&#039;. Does not seem to allow users to use password authentication. &lt;br /&gt;
&lt;br /&gt;
=== Method 1: ldapsam ===&lt;br /&gt;
{{Warning|I didn&#039;t get this to work|I wasn&#039;t able to fully get this method to work. If you are using OpenLDAP only, this way of integrating Samba does work. The issue I had was getting the DNA plugin to work as advertised. &lt;br /&gt;
&lt;br /&gt;
I eventually settled on the ipasam method in the section below.}}&lt;br /&gt;
To use ldapsam, we need to make some changes to the FreeIPA LDAP server by adding sambaSAMAccount and sambaGroupMapping as a default user object class and group object class. &lt;br /&gt;
&lt;br /&gt;
You can either set this in the FreeIPA web interface under configuration, or run:{{Highlight&lt;br /&gt;
| code = # ldapmodify &amp;lt;&amp;lt;EOF&lt;br /&gt;
dn: cn=ipaConfig,cn=etc,dc=home,dc=steamr,dc=com&lt;br /&gt;
changetype: modify&lt;br /&gt;
add: ipaUserObjectClasses&lt;br /&gt;
ipaUserObjectClasses: sambaSAMAccount&lt;br /&gt;
-&lt;br /&gt;
add: ipaGroupObjectClasses&lt;br /&gt;
ipaGroupObjectClasses: sambaGroupMapping&lt;br /&gt;
EOF&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}We will then need to make a custom DNA (distributed numeric assignment) plugin to update these attributes whenever something related to the object (such as the password) is changed. This can be done by adding a DNA object into LDAP.{{Highlight&lt;br /&gt;
| code = ldapadd &amp;lt;&amp;lt;EOF&lt;br /&gt;
dn: cn=SambaSid,cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config&lt;br /&gt;
objectClass: top&lt;br /&gt;
objectClass: extensibleObject&lt;br /&gt;
dnatype: sambaSID&lt;br /&gt;
dnaprefix: S-1-5-21-2049073866-1371207509-1214748462&lt;br /&gt;
dnainterval: 1&lt;br /&gt;
dnamagicregen: assign&lt;br /&gt;
dnafilter: ({{!}}(objectclass=sambasamaccount)(objectclass=sambagroupmapping))&lt;br /&gt;
dnascope: dc=home,dc=steamr,dc=com&lt;br /&gt;
cn: SambaSid&lt;br /&gt;
dnanextvalue: 2&lt;br /&gt;
&lt;br /&gt;
dn: cn=sambaGroupType,cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config&lt;br /&gt;
objectClass: top&lt;br /&gt;
objectClass: extensibleObject&lt;br /&gt;
cn: sambaGroupType&lt;br /&gt;
dnatype: sambaGroupType&lt;br /&gt;
dnainterval: 1&lt;br /&gt;
dnamagicregen: assign&lt;br /&gt;
dnafilter: (objectClass=sambagroupmapping)&lt;br /&gt;
dnascope: dc=home,dc=steamr,dc=com&lt;br /&gt;
dnanextvalue: 2&lt;br /&gt;
EOF&lt;br /&gt;
| lang = bash&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
A ldapsam user entry should have these fields. I don&#039;t see these though.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = dn: uid=guest2, ou=People,dc=quenya,dc=org&lt;br /&gt;
sambaLMPassword: 878D8014606CDA29677A44EFA1353FC7&lt;br /&gt;
sambaPwdMustChange: 2147483647&lt;br /&gt;
sambaPrimaryGroupSID: S-1-5-21-2447931902-1787058256-3961074038-513&lt;br /&gt;
sambaNTPassword: 552902031BEDE9EFAAD3B435B51404EE&lt;br /&gt;
sambaPwdLastSet: 1010179124&lt;br /&gt;
sambaLogonTime: 0&lt;br /&gt;
objectClass: sambaSamAccount&lt;br /&gt;
uid: guest2&lt;br /&gt;
sambaKickoffTime: 2147483647&lt;br /&gt;
sambaAcctFlags: [UX         ]&lt;br /&gt;
sambaLogoffTime: 2147483647&lt;br /&gt;
sambaSID: S-1-5-21-2447931902-1787058256-3961074038-5006&lt;br /&gt;
sambaPwdCanChange: 0&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Configure the Samba server ====&lt;br /&gt;
You can either use a specific binding credential that&#039;s shared across all your samba servers, or use the machine&#039;s cifs service account to authenticate to the LDAP server.&lt;br /&gt;
&lt;br /&gt;
I tried to do the following using the admin account as the bind DN: (&#039;&#039;&#039;using the admin account like this is probably a bad idea, I&#039;m just testing&#039;&#039;&#039;)&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = [global]&lt;br /&gt;
	# freeipa configurations&lt;br /&gt;
	passdb backend = ipasam:ldap://home.steamr.com&lt;br /&gt;
	ldap admin dn = uid=admin,cn=users,cn=accounts,dc=home,dc=steamr,dc=com&lt;br /&gt;
	ldapsam:trusted = yes&lt;br /&gt;
	ldap suffix = cn=accounts,dc=home,dc=steamr,dc=com&lt;br /&gt;
	ldap user suffix = cn=users,cn=accounts&lt;br /&gt;
	ldap machine suffix = cn=computers,cn=accounts&lt;br /&gt;
	ldap group suffix = cn=groups,cn=accounts&lt;br /&gt;
	ldap passwd sync = only&lt;br /&gt;
	ldap ssl = no&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
Run &amp;lt;code&amp;gt;smbpasswd -w password&amp;lt;/code&amp;gt; to set your bind credential passwords.  &lt;br /&gt;
&lt;br /&gt;
=== Method 2: ipasam ===&lt;br /&gt;
See: https://bgstack15.wordpress.com/2017/05/10/samba-share-with-freeipa-auth/&lt;br /&gt;
&lt;br /&gt;
Install the adtrust components on the FreeIPA server. Install &amp;lt;code&amp;gt;ipa-server-trust-ad&amp;lt;/code&amp;gt; and run &amp;lt;code&amp;gt;ipa-adtrust-install --add-sids&amp;lt;/code&amp;gt;. This will add the additional IPASAM attributes such as ipaNtPassword in user objects.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ipa-adtrust-install --add-sids&lt;br /&gt;
## Answer yes to overwrite smb.conf&lt;br /&gt;
## Answer yes to install slapi-nis&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Ensure that your hostname is set to the FQDN of the hostname otherwise this process will fail. If you are using an external DNS server, ensure that the additional service records are present. If you are using a Docker container, set the hostname to the full FQDN (eg. ipa.example.com, rather than just &#039;ipa&#039;).&lt;br /&gt;
&lt;br /&gt;
Next, create a new user and then change the user&#039;s password. This should populate the &amp;lt;code&amp;gt;ipaNTPassword&amp;lt;/code&amp;gt; attribute. {{Highlight&lt;br /&gt;
| code = # ipa user-add leo --first=Leo --last=Leung&lt;br /&gt;
# ipa group-add-member smbgrp --users=leo&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
When modifying smb.conf, the service account or bind DN must have access to these new ipasam attributes. You need to create a new set of privilege and role and grant the account access. Create the role and permissions in the web interface or run:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ipa permission-add &amp;quot;CIFS server can read user passwords&amp;quot;  --attrs={ipaNTHash,ipaNTSecurityIdentifier} --type=user --right={read,search,compare} --bindtype=permission&lt;br /&gt;
# ipa privilege-add &amp;quot;CIFS server privilege&amp;quot;&lt;br /&gt;
# ipa privilege-add-permission &amp;quot;CIFS server privilege&amp;quot; --permission=&amp;quot;CIFS server can read user passwords&amp;quot;&lt;br /&gt;
# ipa role-add &amp;quot;CIFS server&amp;quot;&lt;br /&gt;
# ipa role-add-privilege &amp;quot;CIFS server&amp;quot; --privilege=&amp;quot;CIFS server privilege&amp;quot;&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Then, add your service account or bind DN to the &#039;CIFS server&#039; role. For example:&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ipa service-add cifs/dnas.home.steamr.com&lt;br /&gt;
# ipa role-add-member &amp;quot;CIFS server&amp;quot; --services=cifs/dnas.home.steamr.com&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Configure the Samba server ====&lt;br /&gt;
Generate a keytab file for samba.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # kinit -kt /etc/krb5.keytab&lt;br /&gt;
## Note: we ran this in the previous step.&lt;br /&gt;
## ipa service-add cifs/dnas.home.steamr.com&lt;br /&gt;
# ipa-getkeytab -s ipa.home.steamr.com -p cifs/dnas.home.steamr.com -k /etc/samba/samba.keytab&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Then, tweak smb.conf.{{Highlight&lt;br /&gt;
| code = [global]&lt;br /&gt;
passdb backend = ipasam:ldap://ipa.home.steamr.com&lt;br /&gt;
ldapsam:trusted = yes&lt;br /&gt;
ldap suffix = dc=home,dc=steamr,dc=com&lt;br /&gt;
ldap user suffix = cn=users,cn=accounts&lt;br /&gt;
ldap machine suffix = cn=computers,cn=accounts&lt;br /&gt;
ldap group suffix = cn=groups,cn=accounts&lt;br /&gt;
ldap ssl = no&lt;br /&gt;
idmap config * : backend = tdb  &lt;br /&gt;
create krb5 conf = No &lt;br /&gt;
dedicated keytab file = FILE:/etc/samba/samba.keytab&lt;br /&gt;
kerberos method = dedicated keytab&lt;br /&gt;
| lang = text&lt;br /&gt;
}}If you get: &amp;lt;code&amp;gt;NT_STATUS_BAD_TOKEN_TYPE&amp;lt;/code&amp;gt;, you need to disable MS-PAC in the FreeIPA settings or disable it specifically for this cifs service account.&lt;br /&gt;
&lt;br /&gt;
The ipasam passdb provider is available from the &amp;lt;code&amp;gt;ipa-server-trust-ad&amp;lt;/code&amp;gt; package. However, this package also pulls in a ton of other IPA dependencies which aren&#039;t needed if you just want to run Samba that talks to IPA and not the entire FreeIPA server. If you just want the provider to work on a bare minimal samba server, you can simply just copy (or extract from the &amp;lt;code&amp;gt;ipa-server-trust-ad&amp;lt;/code&amp;gt; package) the &amp;lt;code&amp;gt;ipasam.so&amp;lt;/code&amp;gt; file to &amp;lt;code&amp;gt;/usr/lib64/samba/pdb/ipasam.so&amp;lt;/code&amp;gt; with this set of commands:&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # yum download ipa-server-trust-ad&lt;br /&gt;
# mkdir x &amp;amp;&amp;amp; cd x&lt;br /&gt;
# rpm2cpio ../ipa-server-trust-ad*rpm {{!}} cpio -id ./usr/lib64/samba/pdb/ipasam.so&lt;br /&gt;
# cp ./usr/lib64/samba/pdb/ipasam.so /usr/lib64/samba/pdb/ipasam.so&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Method 3: Kerberos ===&lt;br /&gt;
&lt;br /&gt;
This method is similar to the ipasam method above and you will need to set up the server in the same way. However, the way you configure Samba is different. &lt;br /&gt;
&lt;br /&gt;
==== On the Samba server ====&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ## Join the samba server to FreeIPA&lt;br /&gt;
# ipa-client-install&lt;br /&gt;
&lt;br /&gt;
## Then add the client to samba.&lt;br /&gt;
# ipa-client-samba&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Note that when you try adding the samba client, the IPA server has to be able to resolve the A record for the Samba server you&#039;re adding or else it will fail.&lt;br /&gt;
&lt;br /&gt;
This should automatically set up the cifs service accounts for this particular samba server, get the samba keytab file in &amp;lt;code&amp;gt;/etc/samba/samba.keytab&amp;lt;/code&amp;gt;, and then tweak the smb.conf file to use this keytab file. &lt;br /&gt;
&lt;br /&gt;
The smb.conf file now looks like:&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = [global]&lt;br /&gt;
    # Limit number of forked processes to avoid SMBLoris attack&lt;br /&gt;
    max smbd processes = 1000&lt;br /&gt;
    # Use dedicated Samba keytab. The key there must be synchronized&lt;br /&gt;
    # with Samba tdb databases or nothing will work&lt;br /&gt;
    dedicated keytab file = FILE:/etc/samba/samba.keytab&lt;br /&gt;
    kerberos method = dedicated keytab&lt;br /&gt;
    # Set up logging per machine and Samba process&lt;br /&gt;
    log file = /var/log/samba/log.%m&lt;br /&gt;
    log level = 1&lt;br /&gt;
    # We force &#039;member server&#039; role to allow winbind automatically&lt;br /&gt;
    # discover what is supported by the domain controller side&lt;br /&gt;
    server role = member server&lt;br /&gt;
    realm = HOME.STEAMR.COM&lt;br /&gt;
    netbios name = DNAS&lt;br /&gt;
    workgroup = HOME&lt;br /&gt;
    # Local writable range for IDs not coming from IPA or trusted domains&lt;br /&gt;
    idmap config * : range = 0 - 0&lt;br /&gt;
    idmap config * : backend = tdb&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    idmap config HOME : range = 100000 - 299999&lt;br /&gt;
    idmap config HOME : backend = sss&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Default homes share&lt;br /&gt;
[homes]&lt;br /&gt;
    read only = no&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
You need to then start winbind and smb.  For some reason, this method doesn&#039;t seem to work as winbind is stuck with &amp;quot;&amp;lt;code&amp;gt;wb_parent_idmap_setup_lookupname_done: Lookup domain name &#039;home&#039; failed &#039;NT_STATUS_DOMAIN_CONTROLLER_NOT_FOUND&#039;&amp;lt;/code&amp;gt;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
=== Debug samba issues ===&lt;br /&gt;
Use &amp;lt;code&amp;gt;smbclient&amp;lt;/code&amp;gt; to help debug issues. This utility is provided by the &amp;lt;code&amp;gt;samba-client&amp;lt;/code&amp;gt; package. You can then test authentication by running: &amp;lt;code&amp;gt;smbclient -d 10 -U leo //dnas/home&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Tasks ==&lt;br /&gt;
&lt;br /&gt;
=== Join a computer to a FreeIPA ===&lt;br /&gt;
Use the &amp;lt;code&amp;gt;ipa-client-install&amp;lt;/code&amp;gt; command to add a computer to a FreeIPA server. This should also automatically add a computer account, generate a keytab file, and tweak sssd to use FreeIPA as an authentication mechanism.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ipa-client-install -U -p admin -w $Password --server ipa.home.steamr.com --domain home.steamr.com --force-join --no-ntp --fixed-primary&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Error: did not receive Kerberos credentials ===&lt;br /&gt;
Tools such as &#039;ipa&#039; uses your session&#039;s Kerberos tickets for authentication. If you don&#039;t have any tickets or if your tickets expired, you may get an &amp;lt;code&amp;gt;ipa: ERROR: did not receive Kerberos credentials&amp;lt;/code&amp;gt; error. Fix this by running:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ## Renew/obtain Kerberos tickets for &#039;admin&#039;&lt;br /&gt;
# kinit admin&lt;br /&gt;
Password for admin@HOME.STEAMR.COM:  ****&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Verify if your tickets are available with &amp;lt;code&amp;gt;klist&amp;lt;/code&amp;gt;:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # klist&lt;br /&gt;
Ticket cache: FILE:/tmp/krb5cc_0&lt;br /&gt;
Default principal: admin@STEAMR.COM&lt;br /&gt;
&lt;br /&gt;
Valid starting     Expires            Service principal&lt;br /&gt;
03/06/22 14:44:35  03/07/22 14:39:47  krbtgt/STEAMR.COM@STEAMR.COM&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Container issues ===&lt;br /&gt;
&lt;br /&gt;
* Don&#039;t mount /var/log because the container image symlinks everything into /data. If you do mount /var/log, make sure you make the expected directories or else the installer will fail.&lt;br /&gt;
* Error with &amp;lt;code&amp;gt;AssertionError: Another instance named &#039;HOME-STEAMR-COM&#039; may already exist&amp;lt;/code&amp;gt;. I can&#039;t figure out what&#039;s causing lib389 to think there&#039;s another instance. I built a container image on top of this image with the assertion patched out. This seemed to have fixed the issue. &lt;br /&gt;
* {{Highlight&lt;br /&gt;
| code = FROM freeipa/freeipa-server:rocky-8&lt;br /&gt;
RUN sed &#039;s/assert_c(len(insts)/# assert_c(len(insts)/&#039; -i /usr/lib/python3.6/site-packages/lib389/instance/setup.py&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Can&#039;t find the ipa-adtrust-install package ====&lt;br /&gt;
The FreeIPA packages are under a different app stream repo. Enable it by running &amp;lt;code&amp;gt;dnf -y module enable idm:DL1&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== sssd: Decrypt integrity check failed ===&lt;br /&gt;
After recreating the FreeIPA server, I uninstalled and reinstalled the FreeIPA client on a machine. Kinit works as expected, but sssd authentication fails with the following error in /var/log/sssd/.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;[krb5_child[29691]] [get_and_save_tgt] (0x0020): [RID#6] 1725: [-1765328353][Decrypt integrity check failed]&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The fix here is to wipe out all the caches. &amp;lt;code&amp;gt;sss_cache -E&amp;lt;/code&amp;gt; isn&#039;t sufficient. You have to stop sssd and delete all the databases:&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # systemctl stop sssd&lt;br /&gt;
# rm -rf /var/lib/sss/db/*&lt;br /&gt;
# systemctl start sssd&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== File permission issues ===&lt;br /&gt;
I ran into issues starting FreeIPA in a Docker container. Symptoms include the following issues in the subsections below.&lt;br /&gt;
&lt;br /&gt;
==== Bind / named doesn&#039;t start: ====&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ipa named-pkcs11[5407]: LDAP error: Invalid credentials: bind to LDAP server failed&lt;br /&gt;
ipa named-pkcs11[5407]: couldn&#039;t establish connection in LDAP connection pool: permission denied&lt;br /&gt;
ipa named-pkcs11[5407]: dynamic database &#039;ipa&#039; configuration failed: permission denied&lt;br /&gt;
ipa named-pkcs11[5407]: loading configuration: permission denied&lt;br /&gt;
ipa named-pkcs11[5407]: exiting (due to fatal error)&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
Ignore this for now. You have other issues, likely.&lt;br /&gt;
&lt;br /&gt;
==== Samba doesn&#039;t start:  Error: Invalid credentials ====&lt;br /&gt;
When trying to start smb, I get the following:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = [2023/01/24 05:17:46.170883,  0, pid=5124] ipa_sam.c:4945(bind_callback)&lt;br /&gt;
  bind_callback: cannot perform interactive SASL bind with GSSAPI. LDAP security error is 49&lt;br /&gt;
[2023/01/24 05:17:46.171155,  0, pid=5124] ../../source3/lib/smbldap.c:1059(smbldap_connect_system)&lt;br /&gt;
  failed to bind to server ldapi://%2fvar%2frun%2fslapd-HOME-STEAMR-COM.socket with dn=&amp;quot;[Anonymous bind]&amp;quot; Error: Invalid credentials&lt;br /&gt;
        (unknown)&lt;br /&gt;
[2023/01/24 05:17:46.171419,  1, pid=5124] ../../source3/lib/smbldap.c:1272(get_cached_ldap_connect)&lt;br /&gt;
  Connection to LDAP server failed for the 1 try!&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
The fix was to stop FreeIPA with &amp;lt;code&amp;gt;ipactl stop&amp;lt;/code&amp;gt;, then &amp;lt;code&amp;gt;rm /run/samba/krb5cc_samba&amp;lt;/code&amp;gt;, and restarting FreeIPA with &amp;lt;code&amp;gt;ipactl start&amp;lt;/code&amp;gt;. If the error recurrs after restarting FreeIPA, then you have other issues that&#039;s preventing Samba from starting.&lt;br /&gt;
&lt;br /&gt;
==== Tomcat doesn&#039;t start: status=5/NOTINSTALLED ====&lt;br /&gt;
When trying to start Tomcat, you get a 5 exit code, as reported by systemd:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = pki-tomcatd@pki-tomcat.service: Control process exited, code=exited, status=5/NOTINSTALLED&lt;br /&gt;
pki-tomcatd@pki-tomcat.service: Failed with result &#039;exit-code&#039;.&lt;br /&gt;
Failed to start PKI Tomcat Server pki-tomcat.&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
The exit code 5 being &#039;NOTINSTALLED&#039; is a red herring. You likely have permission issues that&#039;s preventing the user running tomcat (pkiuser) from accessing some certs or configs. Go through your Docker volumes and chown anything directory called &#039;pki&#039; to the pkiuser.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # chown -R 17:17 etc/pki etc/sysconfig/pki var/lib/pki var/lib/ipa/pki-ca&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Tomcat still doesn&#039;t start: start-post operation timed out. Terminating. ====&lt;br /&gt;
Tomcat doesn&#039;t start as it times out.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = pki-tomcatd@pki-tomcat.service: start-post operation timed out. Terminating.&lt;br /&gt;
pki-tomcatd@pki-tomcat.service: Control process exited, code=killed, status=15/TERM&lt;br /&gt;
pki-tomcatd@pki-tomcat.service: Failed with result &#039;timeout&#039;.&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
There&#039;s likely still something tomcat can&#039;t write to.&lt;br /&gt;
&lt;br /&gt;
It seemed to get better when made the ownerships for /data/var/lib/pki/pki-tomcat/logs/ and /var/log/pki/pki-tomcat to pkiuser.&lt;br /&gt;
&lt;br /&gt;
To help troubleshoot, su as pkiuser and try running tomcat as per the service file and see if you get any stack traces.&lt;br /&gt;
&lt;br /&gt;
==== IPA Web GUI reports &amp;quot;Your session has expired. Please re-login&amp;quot; ====&lt;br /&gt;
Review the logs for dirsrv and see if there are any errors. &lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # tail -f /var/log/dirsrv/slapd-*/access&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
For this instance, this was caused when I accidentally removed &amp;lt;code&amp;gt;/etc/dirsrv/ds.keytab.&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Configuring Samba issues ===&lt;br /&gt;
When running # ipa-adtrust-install --add-sids, you get:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ipa-adtrust-install --add-sids&lt;br /&gt;
...&lt;br /&gt;
Configuring CIFS&lt;br /&gt;
  [1/25]: validate server hostname&lt;br /&gt;
  [error] ValueError: Host reports different name than configured: &#039;ipa&#039; versus &#039;ipa.home.steamr.com&#039;. Samba requires to have the same hostname or Kerberos principal &#039;cifs/ipa.home.steamr.com&#039; will not be found in Samba keytab.&lt;br /&gt;
Unexpected error - see /var/log/ipaserver-adtrust-install.log for details:&lt;br /&gt;
ValueError: Host reports different name than configured: &#039;ipa&#039; versus &#039;ipa.home.steamr.com&#039;. Samba requires to have the same hostname or Kerberos principal &#039;cifs/ipa.home.steamr.com&#039; will not be found in Samba keytab.&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Ensure that your hostname is the full FQDN.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # hostname&lt;br /&gt;
ipa&lt;br /&gt;
# hostname -f&lt;br /&gt;
ipa.home.steamr.com&lt;br /&gt;
&lt;br /&gt;
## You need to fix the hostname so that this is what you get:&lt;br /&gt;
# hostname ipa.home.steamr.com&lt;br /&gt;
# hostname&lt;br /&gt;
ipa.home.steamr.com&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Once the hostname is fixed, try the command again.&lt;br /&gt;
&lt;br /&gt;
=== DNS is missing A/AAAA entries for hosts ===&lt;br /&gt;
When trying to add a new host, the DNS silently gets ignored. Possible issues:&lt;br /&gt;
&lt;br /&gt;
* I think this is related to this error when running ipa-client-install: &amp;lt;code&amp;gt;Could not update DNS SSHFP records.&amp;lt;/code&amp;gt;. &lt;br /&gt;
* Possibly bad DNS entries during install was detected and it skipped the DNS tasks? {{Highlight&lt;br /&gt;
| code = Hostname (fc37.home.steamr.com) does not have A/AAAA record.&lt;br /&gt;
Failed to update DNS records.&lt;br /&gt;
Missing A/AAAA record(s) for host fc37.home.steamr.com: 10.1.2.32.&lt;br /&gt;
Incorrect reverse record(s):&lt;br /&gt;
10.1.2.32 is pointing to fc35.home.steamr.com. instead of fc37.home.steamr.com.&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Without this DNS entry set, other issues will crop up later on:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = [root@ipa /]# ipa service-add cifs/dnas.home.steamr.com&lt;br /&gt;
ipa: ERROR: Host &#039;dnas.home.steamr.com&#039; does not have corresponding DNS A/AAAA record&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
The quick work-around would be to add the DNS entry manually and try again.&lt;br /&gt;
&lt;br /&gt;
=== FreeIPA doesn&#039;t start under Docker: Failed to allocate manager object ===&lt;br /&gt;
When trying to run FreeIPA under Docker, you get the following message almost immediately on startup:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = Detected virtualization docker.&lt;br /&gt;
Detected architecture x86-64.&lt;br /&gt;
Failed to create /init.scope control group: Read-only file system&lt;br /&gt;
Failed to allocate manager object: Read-only file system&lt;br /&gt;
[!!!!!!] Failed to allocate manager object.&lt;br /&gt;
Exiting PID 1...&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
You&#039;re likely running this under a Docker host that has cgroup v2 enabled. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Possible workaround&#039;&#039;&#039; (though I couldn&#039;t get it to work before I reverted to Rocky Linux 8, but I realized after downgrading FreeIPA takes a ton of time before it appears to be functional): You will have to disable this by adding &amp;lt;code&amp;gt;systemd.unified_cgroup_hierarchy=0&amp;lt;/code&amp;gt; as a kernel arguments to &amp;lt;code&amp;gt;/etc/default/grub&amp;lt;/code&amp;gt;: . Rebuild grub configs &amp;lt;code&amp;gt;grub2-mkconfig -o /boot/grub2/grub.cfg&amp;lt;/code&amp;gt; and reboot. Bring the container up as usual.&lt;br /&gt;
&lt;br /&gt;
Alternatively, use Podman (except that I can&#039;t because I use docker-compose for all my setups).&lt;br /&gt;
&lt;br /&gt;
=== Failed to authenticate to CA REST API ===&lt;br /&gt;
After suffering a brief power outage, my FreeIPA stack stopped working. A contributing factor might have been my auto-update mechanism which pulled in the most recent version of the freeipa/freeipa:rocky-9 container image.  Looking at the container logs, I see that the container beings to shutdown after the upgrade command fails. Looking at the &amp;lt;code&amp;gt;/var/log/ipaupgrade.log&amp;lt;/code&amp;gt; log file, I see the following error:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = 2025-06-10T03:54:37Z DEBUG The ipa-server-upgrade command failed, exception: RemoteRetrieveError: Failed to authenticate to CA REST API&lt;br /&gt;
2025-06-10T03:54:37Z ERROR Unexpected error - see /var/log/ipaupgrade.log for details:&lt;br /&gt;
RemoteRetrieveError: Failed to authenticate to CA REST API&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
My first step was to try downgrading. The second oldest container image was FreeIPA 4.11 and attempting to downgrade to this version didn&#039;t work as the data I had already had migrated to 4.12.&lt;br /&gt;
&lt;br /&gt;
Some searching turned up a [https://access.redhat.com/solutions/7122683 Red Hat knowledgebase article] which states that this is a bug with ipa-server 4.12.2-14. The fix is to change the following files:&lt;br /&gt;
&lt;br /&gt;
* Add &amp;lt;code&amp;gt;/etc/pki/pki-tomcat/Catalina/localhost/rewrite.config&amp;lt;/code&amp;gt; (copy it from &amp;lt;code&amp;gt;/usr/share/pki/server/conf/Catalina/localhost/rewrite.config&amp;lt;/code&amp;gt;)&lt;br /&gt;
* Edit &amp;lt;code&amp;gt;/etc/pki/pki-tomcat/server.xml&amp;lt;/code&amp;gt; to include before the closing &amp;lt;code&amp;gt;&amp;lt;/Host&amp;gt;&amp;lt;/code&amp;gt; tag near the bottom of the file: {{Highlight&lt;br /&gt;
| code = &amp;lt;Valve className=&amp;quot;org.apache.catalina.valves.rewrite.RewriteValve&amp;quot;/&amp;gt;&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Because both files are in &amp;lt;code&amp;gt;/etc&amp;lt;/code&amp;gt;, these two files should be in the data volume that&#039;s mounted into the FreeIPA container. Edit both files from the data volume and then try restarting the container. It should start properly.&lt;br /&gt;
&lt;br /&gt;
Enterprise software? FreeIPA feels like it was put together with duct tape.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* https://bgstack15.wordpress.com/2017/05/10/samba-share-with-freeipa-auth/  - Older guide walking how to set up FreeIPA and Samba&lt;br /&gt;
* https://blog.cubieserver.de/2018/synology-nas-samba-nfs-and-kerberos-with-freeipa-ldap/&lt;br /&gt;
* https://www.freeipa.org/page/Howto/Integrating_a_Samba_File_Server_With_IPA - Samba integration using kerberos (not ipasam)&lt;br /&gt;
* https://freeipa-users.redhat.narkive.com/ez2uKpFS/authenticate-samba-3-or-4-with-freeipa&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{{Navbox Linux}}&lt;br /&gt;
[[Category:Linux]]&lt;br /&gt;
[[Category:Networking]]&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Dreame_L40_Ultra&amp;diff=7727</id>
		<title>Dreame L40 Ultra</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Dreame_L40_Ultra&amp;diff=7727"/>
		<updated>2025-12-29T04:06:57Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The Dreame L40 Ultra is a robot vacuum and mop cleaner. I purchased this off Amazon in early November 2025 for about $580 CAD.&lt;br /&gt;
&lt;br /&gt;
== Running Valetudo ==&lt;br /&gt;
The Dreame L40 Ultra robot will need the Dreame App to function. This requires the robot to have a constant internet network connection to Dreame&#039;s servers. If this doesn&#039;t sound appleaing, you can root and then install Valetudo on the robot. Valetudo serves a web-interface right from the robot itself and offers most of the functionality of the Dreame App. It is able to make the necessary calls to the underlying firmware such that you do not need to rely on Dreame&#039;s cloud services for the robot to function, thereby making the robot completely self-sufficient on your home network.&lt;br /&gt;
&lt;br /&gt;
Be aware of the downsides with Valetudo:&lt;br /&gt;
* Controlling it from outside your home network or sharing it with multiple people requires more work (a reverse proxy or tailscale should do the trick). If you&#039;re using HomeAssistant, this might not be a huge issue as there&#039;s a HA integration with Valetudo.&lt;br /&gt;
* Some of the more niche features from Dreame is not available (CleanGenius is not available, so things like  auto-recleaning, or auto-rewashing won&#039;t be available. There is no way to do the washboard base cleaning in Valetudo either)&lt;br /&gt;
* No multi-level / multi-map support (you can sort of hack this by replacing the map directory when you switch the robot)&lt;br /&gt;
* No camera view (but you can install rtc2go in addition to Valetudo and watch the camera stream it that way)&lt;br /&gt;
* Map view is good, but there&#039;s no way to hide/delete rooms.&lt;br /&gt;
* Because you&#039;ve rooted the device and divorced it from the cloud, there won&#039;t be any more firmware / security updates or new (anti-)features.&lt;br /&gt;
&lt;br /&gt;
== Rooting and installing Valetudo ==&lt;br /&gt;
You can root and install Valetudo on the Dreame L40 Ultra without needing to disassemble the vacuum cleaner by using the exposed debug headers. The steps on how to do this is documented on Valetudo&#039;s installation page (https://valetudo.cloud/pages/installation/dreame.html) and ideally requires the Dreame breakout PCB because you will need to use the FEL / fastboot installation method for this model. The UART Shell method is not available for this model due to secure boot. The installation instructions is somewhat scattered across multiple different pages and websites and I am documenting everything here for this specific model so it&#039;s all together.&lt;br /&gt;
&lt;br /&gt;
{{Warning|Trusting random people on the internet|Pretty much everything below requires trusting that Dennis Giese (the person behind rooting the robot) isn&#039;t doing something malicous.&lt;br /&gt;
&lt;br /&gt;
The FEL boot images that we&#039;ll be running is entirely generated by the dustbuilder service that Dennis is providing and it isn&#039;t completely open source at the time of writing.&lt;br /&gt;
&lt;br /&gt;
Once you gain root access, you can look around for anything suspicious. At the time of writing, I was able to get a robot that, after changing the NTP server to one hosted locally, was absolutely network-silent.&lt;br /&gt;
&lt;br /&gt;
But then again... you did get a Chinese robot vacuum cleaner that requires constant Internet access...}}&lt;br /&gt;
&lt;br /&gt;
=== Prerequsites ===&lt;br /&gt;
Things you need to root and install Valetudo:&lt;br /&gt;
&lt;br /&gt;
* Dreame Breakout PCB - https://github.com/Hypfer/valetudo-dreameadapter&lt;br /&gt;
** You can order the PCB from JLCPCB and it&#039;ll come in about 2 weeks (~$7 CAD).&lt;br /&gt;
** You will need to make sure you have 2mm headers ($2 CAD), some 2.4mm female headers ($1 CAD), a DIP button, a vertical micro USB header that fits the PCB layout ($2 CAD for 10), and a USB 2.0 type A female port ($2 CAD for 20, optional for this device/installation process).&lt;br /&gt;
* A 3.3v USB serial adapter and some dupont wires (optional, but is nice to have if you want to connect to the robot via serial)&lt;br /&gt;
* A computer running Linux (Ubuntu or Debian is preferable because we need to run Hypfer&#039;s fork of the LiveSuit program which was targeted for those distros). Ideally, this computer shouldn&#039;t be used for something important because you&#039;ll need to download/install a kernel module (which I don&#039;t really know where the source is from)&lt;br /&gt;
** Install Livesuit (https://github.com/Hypfer/valetudo-sunxi-livesuit). Follow the instructions in the README&lt;br /&gt;
* A rooted FEL firmware from https://builder.dontvacuum.me/_dreame_r2492.html (more on this in the instructions below).&lt;br /&gt;
** You will need to use the dustbuilder tool to generate FEL image that allows us to run the robot without secure boot. The tools to do this doesn&#039;t seem to be fully open source, possibly because Dennis (the author) wants to delay Dreame from patching this issue. &lt;br /&gt;
&lt;br /&gt;
=== Installation ===&lt;br /&gt;
&lt;br /&gt;
# Pry off the front of the robot to expose the debug headers. Your Dreame Breakout PCB with the 2mm headers should fit and the top of the breakout board should be facing the LIDAR.&lt;br /&gt;
# Download https://builder.dontvacuum.me/nextgen/dust-livesuit-mr813-ddr4.img and then run LiveSuit. Set the image to dust-livesuit-mr813-ddr4.img&lt;br /&gt;
# Factory reset the vacuum cleaner by pressing the reset button next to the wifi light for 10 seconds (it&#039;s next to the dust compartment). Once it comes back up, press and hold the power button until the vacuum is off&lt;br /&gt;
# While pressing the boot sel button the breakout PCB, press and hold power button on the vacuum cleaner for 5 seconds until the vacuum indicator lights are flashing. You can release the boot sel button once this happens.&lt;br /&gt;
# Plug a USB micro cable from your computer to the breakout port&lt;br /&gt;
# LiveSuit will ask to format the partition. Press No.&lt;br /&gt;
# In a terminal, you should be able to run: {{Highlight&lt;br /&gt;
| code = # fastboot devices&lt;br /&gt;
# fastboot getvar dustversion&lt;br /&gt;
# fastboot getvar config&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Go to the dustbuilder page (https://builder.dontvacuum.me/_dreame_r2492.html) and enter your device serial number (the QR code next to the dust compartment or the serial number under the dust compartment), the config value from the command above, and ensure you check off &#039;Create FEL image&#039;. The dustbuilder took about 20 minutes for me before it generated a FEL image I can download. While that&#039;s being generated, we can continue...&lt;br /&gt;
# Backup the boot stages in case we need it. Keep the bin files somewhere safe. They should all be approximately 400MB.{{Highlight&lt;br /&gt;
| code = # fastboot get_staged dustx100.bin&lt;br /&gt;
# fastboot oem stage1&lt;br /&gt;
# fastboot get_staged dustx101.bin&lt;br /&gt;
# fastboot oem stage2&lt;br /&gt;
# fastboot get_staged dustx102.bin&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Download the firmware image from the dustbuilder. You should have received a dreame.vacuum.r2492_1680_fel.zip file in the package. Extract the contents.&lt;br /&gt;
# Open LiveSuit and set the image to the _dreame.vacuum.r2492_phoenixsuit.img image file.&lt;br /&gt;
# Make a note of the contents in check.txt. You need this ready for the next step.&lt;br /&gt;
# Reboot the vacuum cleaner and re-enter fastboot again. We do this because there is a 160 second watchdog that reboots the robot and we want as much time as we can get to avoid having it reboot on us before we&#039;re done.&lt;br /&gt;
# Run: {{Highlight&lt;br /&gt;
| code = # fastboot getvar config   ## Confirm fastboot works&lt;br /&gt;
# fastboot oem dust xxxxxxx   ## Replace xxxx with the contents in check.txt&lt;br /&gt;
# fastboot oem prep   ## If this fails, don&#039;t proceed. Ensure LiveSuit has the correct img file set&lt;br /&gt;
## Ensure that everything below can run before the watchdog resets the device. If you took too long above, reset and try again.&lt;br /&gt;
# fastboot flash toc1 toc1.img   ## Should return OKAY. Stop otherwise and re-assess.&lt;br /&gt;
# fastboot flash boot1 boot.img&lt;br /&gt;
# fastboot flash rootfs1 rootfs.img&lt;br /&gt;
# fastboot flash boot2 boot.img&lt;br /&gt;
# fastboot flash rootfs2 rootfs.img&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Once all the flash commands succeed, you can run &amp;lt;code&amp;gt;fastboot reboot&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Press the outer buttons together for 3 seconds so that the robot starts its WiFi access point. Connect your computer to the robot&#039;s WiFi network.&lt;br /&gt;
# Confirm that you can SSH to the robot at root@192.168.5.1 using the SSH key that you selected (or were provided) in dustbuilder. If you don&#039;t have a working key, you can always connect to the robot via serial (there&#039;s a root shell running there)&lt;br /&gt;
# Backup all the calibration data from the robot. {{Highlight&lt;br /&gt;
| code = robot# tar -czf /tmp/calibration.tar.gz /mnt&lt;br /&gt;
&lt;br /&gt;
## on your computer, scp it out, or if dropbear utils aren&#039;t available, you can just do:&lt;br /&gt;
computer# ssh -i *rsa root@192.168.5.1 cat /tmp/calibration.tar.gz &amp;gt; calibration.tar.gz&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Download Valetudo&#039;s latest release from https://github.com/Hypfer/Valetudo and copy it into the robot to &amp;lt;code&amp;gt;/data/valetudo&amp;lt;/code&amp;gt;. {{Highlight&lt;br /&gt;
| code = robot# scp leo@computer:~/valetudo /data/valetudo&lt;br /&gt;
robot# chmod +x /data/valetudo&lt;br /&gt;
robot# cp /misc/_root_postboot.sh.tpl /data/_root_postboot.sh&lt;br /&gt;
robot# chmod +x /data/_root_postboot.sh&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Reboot and once the robot comes back up, you should see the Valetudo web interface on the robot at http://192.168.5.1. Complete the robot setup using Valetudo (such as connecting it to your home&#039;s WiFi network).&lt;br /&gt;
&lt;br /&gt;
=== Camera Streaming ===&lt;br /&gt;
The camera on the vacuum cleaner can be seen via go2rtc. Anthony Zhang&#039;s blog talks more about this (https://anthony-zhang.me/blog/offline-robot-vacuum/) and it requires installing tihmstar&#039;s vacuumstreamer repo and the official go2rtc binary:&lt;br /&gt;
* https://github.com/tihmstar/vacuumstreamer&lt;br /&gt;
* https://github.com/AlexxIT/go2rtc&lt;br /&gt;
{{Warning|Trusting random people on the internet... part 2|The vacuumstreamer repo has a video_monitor binary which seems to have come from another AVA powered vacuum cleaner. The source of this binary isn&#039;t clear. You&#039;re running someone&#039;s binary blob at this point.}}{{Highlight&lt;br /&gt;
| code = ## on the robot, make /data/vacuumstramer&lt;br /&gt;
robot# mkdir /data/vacuumstreamer&lt;br /&gt;
&lt;br /&gt;
## on your computer, download vacuumstreamer packages and the go2rtc binary and copy it to the robot&lt;br /&gt;
computer# git clone https://github.com/tihmstar/vacuumstreamer&lt;br /&gt;
computer# scp vacuumstreamer/vacuumstreamer.so                root@vacuum:/data/vacuumstreamer/&lt;br /&gt;
computer# scp vacuumstreamer/dist/usr/bin/video_monitor       root@vacuum:/data/vacuumstreamer/&lt;br /&gt;
computer# scp -r vacuumstreamer/dist/ava/conf/video_monitor   root@vacuum:/data/vacuumstreamer/ava_conf_video_monitor&lt;br /&gt;
&lt;br /&gt;
computer# wget https://github.com/AlexxIT/go2rtc/releases/download/v1.9.13/go2rtc_linux_arm64&lt;br /&gt;
computer# scp go2rtc_linux_arm64  root@vacuum:/data/vacuumstreamer/go2rtc&lt;br /&gt;
&lt;br /&gt;
## back on the robot, set up the streamer&lt;br /&gt;
robot# cp -r /mnt/private /data/vacuumstreamer/mnt_private_copy &lt;br /&gt;
robot# touch /data/vacuumstreamer/mnt_private_copy/certificate.bin  # workaround for missing certificate bug, see https://github.com/tihmstar/vacuumstreamer/issues/1 for details&lt;br /&gt;
robot# cat &amp;lt;&amp;lt;EOF &amp;gt;&amp;gt; /data/_root_postboot.sh&lt;br /&gt;
&lt;br /&gt;
if [[ -f /data/vacuumstreamer/video_monitor ]]; then&lt;br /&gt;
    mount --bind /data/vacuumstreamer/ava_conf_video_monitor /ava/conf/video_monitor&lt;br /&gt;
    mount --bind /data/vacuumstreamer/mnt_private_copy /mnt/private&lt;br /&gt;
    LD_PRELOAD=/data/vacuumstreamer/vacuumstreamer.so /data/vacuumstreamer/video_monitor &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&lt;br /&gt;
    /data/vacuumstreamer/go2rtc -c &#039;{&amp;quot;streams&amp;quot;: {&amp;quot;tcp_magic&amp;quot;: &amp;quot;tcp://127.0.0.1:6969&amp;quot;&amp;lt;nowiki&amp;gt;}}&amp;lt;/nowiki&amp;gt;&#039; &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&lt;br /&gt;
fi&lt;br /&gt;
EOF&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Reboot. You should see the video stream at http://robot:1984.&lt;br /&gt;
&lt;br /&gt;
=== Multiple maps (for different floors) ===&lt;br /&gt;
Because Valetudo only supports working with only one map, you will have to save/restore the map data if you want to use multiple maps. There is a project that already does this via MQTT called Maploader (https://github.com/pkoehlers/maploader). It uses the MQTT settings from the Valetudo config file.&lt;br /&gt;
&lt;br /&gt;
==== Installation ====&lt;br /&gt;
Install Maploader on the vacuum cleaner (see the project&#039;s README).&lt;br /&gt;
&lt;br /&gt;
In Home Assistant&#039;s configuration.yaml file, add the following yaml config. I added a &amp;lt;code&amp;gt;unique_id&amp;lt;/code&amp;gt; so that these can be added to a dashboard.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = mqtt:&lt;br /&gt;
  sensor:&lt;br /&gt;
    - state_topic: valetudo/DreameL40Ultra/maploader/status&lt;br /&gt;
      unique_id: &amp;quot;vacuum_maploader_status&amp;quot;&lt;br /&gt;
      name: &amp;quot;vacuum_maploader_status&amp;quot;&lt;br /&gt;
  select:&lt;br /&gt;
    - command_topic: valetudo/DreameL40Ultra/maploader/map/set&lt;br /&gt;
      state_topic: valetudo/DreameL40Ultra/maploader/map&lt;br /&gt;
      unique_id: &amp;quot;vacuum_maploader_map&amp;quot;&lt;br /&gt;
      name: &amp;quot;vacuum_maploader_map&amp;quot;&lt;br /&gt;
      options:&lt;br /&gt;
        - main&lt;br /&gt;
        - second_floor&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;br /&gt;
Restart Home Assistant and the Vacuum cleaner so that Maploader is running. In Home Assistant, find the sensors by searching for &#039;maploader&#039; and then add it to your dashboard.&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Benchmark_for_Intel_E5-1660_v3&amp;diff=7726</id>
		<title>Benchmark for Intel E5-1660 v3</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Benchmark_for_Intel_E5-1660_v3&amp;diff=7726"/>
		<updated>2025-12-24T23:13:02Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This is the CPU on the x99 board that I got off EBay in early 2025.&lt;br /&gt;
&lt;br /&gt;
{{Benchmark&lt;br /&gt;
 | hardware =   X99-E WS/USB 3.1&lt;br /&gt;
 | cpu =        Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz  CPU @ 3.0GHz&lt;br /&gt;
 | memory =     128 GB DDR4 2133&lt;br /&gt;
 | disk =       128 GB SanDisk SD7SB3Q-128G-1006, SATA III&lt;br /&gt;
 | os =         Debian GNU/Linux 12&lt;br /&gt;
 | type =       server&lt;br /&gt;
 | score =      1471.6 / 8075.3&lt;br /&gt;
 | raw = &lt;br /&gt;
========================================================================&lt;br /&gt;
   BYTE UNIX Benchmarks (Version 5.1.3)&lt;br /&gt;
&lt;br /&gt;
   System: pve: GNU/Linux&lt;br /&gt;
   OS: GNU/Linux -- 6.8.12-15-pve -- #1 SMP PREEMPT_DYNAMIC PMX 6.8.12-15 (2025-09-12T11:02Z)&lt;br /&gt;
   Machine: x86_64 (unknown)&lt;br /&gt;
   Language: en_US.utf8 (charmap=&amp;quot;UTF-8&amp;quot;, collate=&amp;quot;UTF-8&amp;quot;)&lt;br /&gt;
   CPU 0: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 1: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 2: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 3: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 4: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 5: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 6: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 7: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 8: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 9: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 10: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 11: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 12: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 13: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 14: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 15: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   14:28:32 up 30 days,  5:17,  2 users,  load average: 3.47, 3.91, 3.85; runlevel 2025-11-24&lt;br /&gt;
&lt;br /&gt;
------------------------------------------------------------------------&lt;br /&gt;
Benchmark Run: Wed Dec 24 2025 14:28:32 - 14:56:32&lt;br /&gt;
16 CPUs in system; running 1 parallel copy of tests&lt;br /&gt;
&lt;br /&gt;
Dhrystone 2 using register variables       48113206.2 lps   (10.0 s, 7 samples)&lt;br /&gt;
Double-Precision Whetstone                     7561.5 MWIPS (10.0 s, 7 samples)&lt;br /&gt;
Execl Throughput                               3508.8 lps   (30.0 s, 2 samples)&lt;br /&gt;
File Copy 1024 bufsize 2000 maxblocks        780264.4 KBps  (30.0 s, 2 samples)&lt;br /&gt;
File Copy 256 bufsize 500 maxblocks          202458.5 KBps  (30.0 s, 2 samples)&lt;br /&gt;
File Copy 4096 bufsize 8000 maxblocks       2477067.6 KBps  (30.0 s, 2 samples)&lt;br /&gt;
Pipe Throughput                             1013971.6 lps   (10.0 s, 7 samples)&lt;br /&gt;
Pipe-based Context Switching                 150952.0 lps   (10.0 s, 7 samples)&lt;br /&gt;
Process Creation                               7841.4 lps   (30.0 s, 2 samples)&lt;br /&gt;
Shell Scripts (1 concurrent)                  13070.0 lpm   (60.0 s, 2 samples)&lt;br /&gt;
Shell Scripts (8 concurrent)                   5925.7 lpm   (60.0 s, 2 samples)&lt;br /&gt;
System Call Overhead                         557537.5 lps   (10.0 s, 7 samples)&lt;br /&gt;
&lt;br /&gt;
System Benchmarks Index Values               BASELINE       RESULT    INDEX&lt;br /&gt;
Dhrystone 2 using register variables         116700.0   48113206.2   4122.8&lt;br /&gt;
Double-Precision Whetstone                       55.0       7561.5   1374.8&lt;br /&gt;
Execl Throughput                                 43.0       3508.8    816.0&lt;br /&gt;
File Copy 1024 bufsize 2000 maxblocks          3960.0     780264.4   1970.4&lt;br /&gt;
File Copy 256 bufsize 500 maxblocks            1655.0     202458.5   1223.3&lt;br /&gt;
File Copy 4096 bufsize 8000 maxblocks          5800.0    2477067.6   4270.8&lt;br /&gt;
Pipe Throughput                               12440.0    1013971.6    815.1&lt;br /&gt;
Pipe-based Context Switching                   4000.0     150952.0    377.4&lt;br /&gt;
Process Creation                                126.0       7841.4    622.3&lt;br /&gt;
Shell Scripts (1 concurrent)                     42.4      13070.0   3082.6&lt;br /&gt;
Shell Scripts (8 concurrent)                      6.0       5925.7   9876.2&lt;br /&gt;
System Call Overhead                          15000.0     557537.5    371.7&lt;br /&gt;
                                                                   ========&lt;br /&gt;
System Benchmarks Index Score                                        1471.6&lt;br /&gt;
&lt;br /&gt;
------------------------------------------------------------------------&lt;br /&gt;
Benchmark Run: Wed Dec 24 2025 14:56:32 - 15:24:35&lt;br /&gt;
16 CPUs in system; running 16 parallel copies of tests&lt;br /&gt;
&lt;br /&gt;
Dhrystone 2 using register variables      503918570.5 lps   (10.0 s, 7 samples)&lt;br /&gt;
Double-Precision Whetstone                   105274.5 MWIPS (10.0 s, 7 samples)&lt;br /&gt;
Execl Throughput                              31544.8 lps   (30.0 s, 2 samples)&lt;br /&gt;
File Copy 1024 bufsize 2000 maxblocks       1496137.5 KBps  (30.0 s, 2 samples)&lt;br /&gt;
File Copy 256 bufsize 500 maxblocks          392970.6 KBps  (30.0 s, 2 samples)&lt;br /&gt;
File Copy 4096 bufsize 8000 maxblocks       4403975.7 KBps  (30.0 s, 2 samples)&lt;br /&gt;
Pipe Throughput                             9061747.9 lps   (10.0 s, 7 samples)&lt;br /&gt;
Pipe-based Context Switching                1607736.7 lps   (10.0 s, 7 samples)&lt;br /&gt;
Process Creation                              74350.4 lps   (30.0 s, 2 samples)&lt;br /&gt;
Shell Scripts (1 concurrent)                  82013.7 lpm   (60.0 s, 2 samples)&lt;br /&gt;
Shell Scripts (8 concurrent)                  10784.8 lpm   (60.0 s, 2 samples)&lt;br /&gt;
System Call Overhead                        4649424.3 lps   (10.0 s, 7 samples)&lt;br /&gt;
&lt;br /&gt;
System Benchmarks Index Values               BASELINE       RESULT    INDEX&lt;br /&gt;
Dhrystone 2 using register variables         116700.0  503918570.5  43180.7&lt;br /&gt;
Double-Precision Whetstone                       55.0     105274.5  19140.8&lt;br /&gt;
Execl Throughput                                 43.0      31544.8   7336.0&lt;br /&gt;
File Copy 1024 bufsize 2000 maxblocks          3960.0    1496137.5   3778.1&lt;br /&gt;
File Copy 256 bufsize 500 maxblocks            1655.0     392970.6   2374.4&lt;br /&gt;
File Copy 4096 bufsize 8000 maxblocks          5800.0    4403975.7   7593.1&lt;br /&gt;
Pipe Throughput                               12440.0    9061747.9   7284.4&lt;br /&gt;
Pipe-based Context Switching                   4000.0    1607736.7   4019.3&lt;br /&gt;
Process Creation                                126.0      74350.4   5900.8&lt;br /&gt;
Shell Scripts (1 concurrent)                     42.4      82013.7  19342.8&lt;br /&gt;
Shell Scripts (8 concurrent)                      6.0      10784.8  17974.7&lt;br /&gt;
System Call Overhead                          15000.0    4649424.3   3099.6&lt;br /&gt;
                                                                   ========&lt;br /&gt;
System Benchmarks Index Score                                        8075.3&lt;br /&gt;
&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{{Navbox Benchmarks}}&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Benchmark_for_Intel_E5-1660_v3&amp;diff=7725</id>
		<title>Benchmark for Intel E5-1660 v3</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Benchmark_for_Intel_E5-1660_v3&amp;diff=7725"/>
		<updated>2025-12-24T21:02:12Z</updated>

		<summary type="html">&lt;p&gt;Leo: Created page with &amp;quot;This is the CPU on the x99 board that I got off EBay in early 2025.  {{Benchmark  | hardware =   X99-E WS/USB 3.1  | cpu =        Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz  CPU @ 3.0GHz  | memory =     128 GB DDR4 2133  | disk =       128 GB SanDisk SD7SB3Q-128G-1006, SATA III  | os =         Debian GNU/Linux 12  | type =       server  | score =      1397.1 / 7161.3  | raw =  ========================================================================    BYTE UNIX Benchmarks...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This is the CPU on the x99 board that I got off EBay in early 2025.&lt;br /&gt;
&lt;br /&gt;
{{Benchmark&lt;br /&gt;
 | hardware =   X99-E WS/USB 3.1&lt;br /&gt;
 | cpu =        Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz  CPU @ 3.0GHz&lt;br /&gt;
 | memory =     128 GB DDR4 2133&lt;br /&gt;
 | disk =       128 GB SanDisk SD7SB3Q-128G-1006, SATA III&lt;br /&gt;
 | os =         Debian GNU/Linux 12&lt;br /&gt;
 | type =       server&lt;br /&gt;
 | score =      1397.1 / 7161.3&lt;br /&gt;
 | raw = &lt;br /&gt;
========================================================================&lt;br /&gt;
   BYTE UNIX Benchmarks (Version 5.1.3)&lt;br /&gt;
&lt;br /&gt;
   System: pve: GNU/Linux&lt;br /&gt;
   OS: GNU/Linux -- 6.8.12-15-pve -- #1 SMP PREEMPT_DYNAMIC PMX 6.8.12-15 (2025-09-12T11:02Z)&lt;br /&gt;
   Machine: x86_64 (unknown)&lt;br /&gt;
   Language: en_US.utf8 (charmap=&amp;quot;UTF-8&amp;quot;, collate=&amp;quot;UTF-8&amp;quot;)&lt;br /&gt;
   CPU 0: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 1: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 2: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 3: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 4: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 5: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 6: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 7: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 8: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 9: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 10: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 11: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 12: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 13: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 14: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   CPU 15: Intel(R) Xeon(R) CPU E5-1660 v3 @ 3.00GHz (6010.2 bogomips)&lt;br /&gt;
          Hyper-Threading, x86-64, MMX, Physical Address Ext, SYSENTER/SYSEXIT, SYSCALL/SYSRET, Intel virtualization&lt;br /&gt;
   18:15:54 up 28 days,  9:04,  3 users,  load average: 2.33, 2.03, 2.09; runlevel 2025-11-24&lt;br /&gt;
&lt;br /&gt;
------------------------------------------------------------------------&lt;br /&gt;
Benchmark Run: Mon Dec 22 2025 18:15:54 - 18:54:46&lt;br /&gt;
16 CPUs in system; running 1 parallel copy of tests&lt;br /&gt;
&lt;br /&gt;
Dhrystone 2 using register variables       46788047.8 lps   (10.0 s, 7 samples)&lt;br /&gt;
Double-Precision Whetstone                     7496.2 MWIPS (10.0 s, 7 samples)&lt;br /&gt;
Execl Throughput                               3359.7 lps   (29.3 s, 2 samples)&lt;br /&gt;
File Copy 1024 bufsize 2000 maxblocks        753698.8 KBps  (30.0 s, 2 samples)&lt;br /&gt;
File Copy 256 bufsize 500 maxblocks          197234.5 KBps  (30.0 s, 2 samples)&lt;br /&gt;
File Copy 4096 bufsize 8000 maxblocks       2184722.3 KBps  (30.0 s, 2 samples)&lt;br /&gt;
Pipe Throughput                              999845.1 lps   (10.0 s, 7 samples)&lt;br /&gt;
Pipe-based Context Switching                 139904.5 lps   (10.0 s, 7 samples)&lt;br /&gt;
Process Creation                               7180.2 lps   (30.0 s, 2 samples)&lt;br /&gt;
Shell Scripts (1 concurrent)                  12207.0 lpm   (60.0 s, 2 samples)&lt;br /&gt;
Shell Scripts (8 concurrent)                   5380.9 lpm   (60.0 s, 2 samples)&lt;br /&gt;
System Call Overhead                         549632.4 lps   (10.0 s, 7 samples)&lt;br /&gt;
&lt;br /&gt;
System Benchmarks Index Values               BASELINE       RESULT    INDEX&lt;br /&gt;
Dhrystone 2 using register variables         116700.0   46788047.8   4009.3&lt;br /&gt;
Double-Precision Whetstone                       55.0       7496.2   1362.9&lt;br /&gt;
Execl Throughput                                 43.0       3359.7    781.3&lt;br /&gt;
File Copy 1024 bufsize 2000 maxblocks          3960.0     753698.8   1903.3&lt;br /&gt;
File Copy 256 bufsize 500 maxblocks            1655.0     197234.5   1191.7&lt;br /&gt;
File Copy 4096 bufsize 8000 maxblocks          5800.0    2184722.3   3766.8&lt;br /&gt;
Pipe Throughput                               12440.0     999845.1    803.7&lt;br /&gt;
Pipe-based Context Switching                   4000.0     139904.5    349.8&lt;br /&gt;
Process Creation                                126.0       7180.2    569.9&lt;br /&gt;
Shell Scripts (1 concurrent)                     42.4      12207.0   2879.0&lt;br /&gt;
Shell Scripts (8 concurrent)                      6.0       5380.9   8968.1&lt;br /&gt;
System Call Overhead                          15000.0     549632.4    366.4&lt;br /&gt;
                                                                   ========&lt;br /&gt;
System Benchmarks Index Score                                        1397.1&lt;br /&gt;
&lt;br /&gt;
------------------------------------------------------------------------&lt;br /&gt;
Benchmark Run: Mon Dec 22 2025 18:54:46 - 19:34:27&lt;br /&gt;
16 CPUs in system; running 16 parallel copies of tests&lt;br /&gt;
&lt;br /&gt;
Dhrystone 2 using register variables      446158122.0 lps   (10.0 s, 7 samples)&lt;br /&gt;
Double-Precision Whetstone                    97049.0 MWIPS (10.0 s, 7 samples)&lt;br /&gt;
Execl Throughput                              27936.0 lps   (29.2 s, 2 samples)&lt;br /&gt;
File Copy 1024 bufsize 2000 maxblocks       1364285.5 KBps  (30.0 s, 2 samples)&lt;br /&gt;
File Copy 256 bufsize 500 maxblocks          368175.9 KBps  (30.0 s, 2 samples)&lt;br /&gt;
File Copy 4096 bufsize 8000 maxblocks       3847624.4 KBps  (30.0 s, 2 samples)&lt;br /&gt;
Pipe Throughput                             8199713.7 lps   (10.0 s, 7 samples)&lt;br /&gt;
Pipe-based Context Switching                1435331.2 lps   (10.0 s, 7 samples)&lt;br /&gt;
Process Creation                              61806.6 lps   (30.0 s, 2 samples)&lt;br /&gt;
Shell Scripts (1 concurrent)                  66920.1 lpm   (60.0 s, 2 samples)&lt;br /&gt;
Shell Scripts (8 concurrent)                   9336.9 lpm   (60.0 s, 2 samples)&lt;br /&gt;
System Call Overhead                        4298119.8 lps   (10.0 s, 7 samples)&lt;br /&gt;
&lt;br /&gt;
System Benchmarks Index Values               BASELINE       RESULT    INDEX&lt;br /&gt;
Dhrystone 2 using register variables         116700.0  446158122.0  38231.2&lt;br /&gt;
Double-Precision Whetstone                       55.0      97049.0  17645.3&lt;br /&gt;
Execl Throughput                                 43.0      27936.0   6496.8&lt;br /&gt;
File Copy 1024 bufsize 2000 maxblocks          3960.0    1364285.5   3445.2&lt;br /&gt;
File Copy 256 bufsize 500 maxblocks            1655.0     368175.9   2224.6&lt;br /&gt;
File Copy 4096 bufsize 8000 maxblocks          5800.0    3847624.4   6633.8&lt;br /&gt;
Pipe Throughput                               12440.0    8199713.7   6591.4&lt;br /&gt;
Pipe-based Context Switching                   4000.0    1435331.2   3588.3&lt;br /&gt;
Process Creation                                126.0      61806.6   4905.3&lt;br /&gt;
Shell Scripts (1 concurrent)                     42.4      66920.1  15783.1&lt;br /&gt;
Shell Scripts (8 concurrent)                      6.0       9336.9  15561.4&lt;br /&gt;
System Call Overhead                          15000.0    4298119.8   2865.4&lt;br /&gt;
                                                                   ========&lt;br /&gt;
System Benchmarks Index Score                                        7161.3&lt;br /&gt;
&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{{Navbox Benchmarks}}&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Dreame_L40_Ultra&amp;diff=7724</id>
		<title>Dreame L40 Ultra</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Dreame_L40_Ultra&amp;diff=7724"/>
		<updated>2025-12-24T00:11:31Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The Dreame L40 Ultra is a robot vacuum and mop cleaner. I purchased this off Amazon in early November 2025 for about $580 CAD.&lt;br /&gt;
&lt;br /&gt;
== Running Valetudo ==&lt;br /&gt;
The Dreame L40 Ultra robot will need the Dreame App to function. This requires the robot to have a constant internet network connection to Dreame&#039;s servers. If this doesn&#039;t sound appleaing, you can root and then install Valetudo on the robot. Valetudo serves a web-interface right from the robot itself and offers most of the functionality of the Dreame App. It is able to make the necessary calls to the underlying firmware such that you do not need to rely on Dreame&#039;s cloud services for the robot to function, thereby making the robot completely self-sufficient on your home network.&lt;br /&gt;
&lt;br /&gt;
Be aware of the downsides with Valetudo:&lt;br /&gt;
* Controlling it from outside your home network or sharing it with multiple people requires more work (a reverse proxy or tailscale should do the trick). If you&#039;re using HomeAssistant, this might not be a huge issue as there&#039;s a HA integration with Valetudo.&lt;br /&gt;
* Some of the more niche features from Dreame is not available (CleanGenius is not available, so things like  auto-recleaning, or auto-rewashing won&#039;t be available. There is no way to do the washboard base cleaning in Valetudo either)&lt;br /&gt;
* No multi-level / multi-map support (you can sort of hack this by replacing the map directory when you switch the robot)&lt;br /&gt;
* No camera view (but you can install rtc2go in addition to Valetudo and watch the camera stream it that way)&lt;br /&gt;
* Map view is good, but there&#039;s no way to hide/delete rooms.&lt;br /&gt;
* Because you&#039;ve rooted the device and divorced it from the cloud, there won&#039;t be any more firmware / security updates or new (anti-)features.&lt;br /&gt;
&lt;br /&gt;
== Rooting and installing Valetudo ==&lt;br /&gt;
You can root and install Valetudo on the Dreame L40 Ultra without needing to disassemble the vacuum cleaner by using the exposed debug headers. The steps on how to do this is documented on Valetudo&#039;s installation page (https://valetudo.cloud/pages/installation/dreame.html) and ideally requires the Dreame breakout PCB because you will need to use the FEL / fastboot installation method for this model. The UART Shell method is not available for this model due to secure boot. The installation instructions is somewhat scattered across multiple different pages and websites and I am documenting everything here for this specific model so it&#039;s all together.&lt;br /&gt;
&lt;br /&gt;
{{Warning|Trusting random people on the internet|Pretty much everything below requires trusting that Dennis Giese (the person behind rooting the robot) isn&#039;t doing something malicous.&lt;br /&gt;
&lt;br /&gt;
The FEL boot images that we&#039;ll be running is entirely generated by the dustbuilder service that Dennis is providing and it isn&#039;t completely open source at the time of writing.&lt;br /&gt;
&lt;br /&gt;
Once you gain root access, you can look around for anything suspicious. At the time of writing, I was able to get a robot that, after changing the NTP server to one hosted locally, was absolutely network-silent.&lt;br /&gt;
&lt;br /&gt;
But then again... you did get a Chinese robot vacuum cleaner that requires constant Internet access...}}&lt;br /&gt;
&lt;br /&gt;
=== Prerequsites ===&lt;br /&gt;
Things you need to root and install Valetudo:&lt;br /&gt;
&lt;br /&gt;
* Dreame Breakout PCB - https://github.com/Hypfer/valetudo-dreameadapter&lt;br /&gt;
** You can order the PCB from JLCPCB and it&#039;ll come in about 2 weeks (~$7 CAD).&lt;br /&gt;
** You will need to make sure you have 2mm headers ($2 CAD), some 2.4mm female headers ($1 CAD), a DIP button, a vertical micro USB header that fits the PCB layout ($2 CAD for 10), and a USB 2.0 type A female port ($2 CAD for 20, optional for this device/installation process).&lt;br /&gt;
* A 3.3v USB serial adapter and some dupont wires (optional, but is nice to have if you want to connect to the robot via serial)&lt;br /&gt;
* A computer running Linux (Ubuntu or Debian is preferable because we need to run Hypfer&#039;s fork of the LiveSuit program which was targeted for those distros). Ideally, this computer shouldn&#039;t be used for something important because you&#039;ll need to download/install a kernel module (which I don&#039;t really know where the source is from)&lt;br /&gt;
** Install Livesuit (https://github.com/Hypfer/valetudo-sunxi-livesuit). Follow the instructions in the README&lt;br /&gt;
* A rooted FEL firmware from https://builder.dontvacuum.me/_dreame_r2492.html (more on this in the instructions below).&lt;br /&gt;
** You will need to use the dustbuilder tool to generate FEL image that allows us to run the robot without secure boot. The tools to do this doesn&#039;t seem to be fully open source, possibly because Dennis (the author) wants to delay Dreame from patching this issue. &lt;br /&gt;
&lt;br /&gt;
=== Installation ===&lt;br /&gt;
&lt;br /&gt;
# Pry off the front of the robot to expose the debug headers. Your Dreame Breakout PCB with the 2mm headers should fit and the top of the breakout board should be facing the LIDAR.&lt;br /&gt;
# Download https://builder.dontvacuum.me/nextgen/dust-livesuit-mr813-ddr4.img and then run LiveSuit. Set the image to dust-livesuit-mr813-ddr4.img&lt;br /&gt;
# Factory reset the vacuum cleaner by pressing the reset button next to the wifi light for 10 seconds (it&#039;s next to the dust compartment). Once it comes back up, press and hold the power button until the vacuum is off&lt;br /&gt;
# While pressing the boot sel button the breakout PCB, press and hold power button on the vacuum cleaner for 5 seconds until the vacuum indicator lights are flashing. You can release the boot sel button once this happens.&lt;br /&gt;
# Plug a USB micro cable from your computer to the breakout port&lt;br /&gt;
# LiveSuit will ask to format the partition. Press No.&lt;br /&gt;
# In a terminal, you should be able to run: {{Highlight&lt;br /&gt;
| code = # fastboot devices&lt;br /&gt;
# fastboot getvar dustversion&lt;br /&gt;
# fastboot getvar config&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Go to the dustbuilder page (https://builder.dontvacuum.me/_dreame_r2492.html) and enter your device serial number (the QR code next to the dust compartment or the serial number under the dust compartment), the config value from the command above, and ensure you check off &#039;Create FEL image&#039;. The dustbuilder took about 20 minutes for me before it generated a FEL image I can download. While that&#039;s being generated, we can continue...&lt;br /&gt;
# Backup the boot stages in case we need it. Keep the bin files somewhere safe. They should all be approximately 400MB.{{Highlight&lt;br /&gt;
| code = # fastboot get_staged dustx100.bin&lt;br /&gt;
# fastboot oem stage1&lt;br /&gt;
# fastboot get_staged dustx101.bin&lt;br /&gt;
# fastboot oem stage2&lt;br /&gt;
# fastboot get_staged dustx102.bin&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Download the firmware image from the dustbuilder. You should have received a dreame.vacuum.r2492_1680_fel.zip file in the package. Extract the contents.&lt;br /&gt;
# Open LiveSuit and set the image to the _dreame.vacuum.r2492_phoenixsuit.img image file.&lt;br /&gt;
# Make a note of the contents in check.txt. You need this ready for the next step.&lt;br /&gt;
# Reboot the vacuum cleaner and re-enter fastboot again. We do this because there is a 160 second watchdog that reboots the robot and we want as much time as we can get to avoid having it reboot on us before we&#039;re done.&lt;br /&gt;
# Run: {{Highlight&lt;br /&gt;
| code = # fastboot getvar config   ## Confirm fastboot works&lt;br /&gt;
# fastboot oem dust xxxxxxx   ## Replace xxxx with the contents in check.txt&lt;br /&gt;
# fastboot oem prep   ## If this fails, don&#039;t proceed. Ensure LiveSuit has the correct img file set&lt;br /&gt;
## Ensure that everything below can run before the watchdog resets the device. If you took too long above, reset and try again.&lt;br /&gt;
# fastboot flash toc1 toc1.img   ## Should return OKAY. Stop otherwise and re-assess.&lt;br /&gt;
# fastboot flash boot1 boot.img&lt;br /&gt;
# fastboot flash rootfs1 rootfs.img&lt;br /&gt;
# fastboot flash boot2 boot.img&lt;br /&gt;
# fastboot flash rootfs2 rootfs.img&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Once all the flash commands succeed, you can run &amp;lt;code&amp;gt;fastboot reboot&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Press the outer buttons together for 3 seconds so that the robot starts its WiFi access point. Connect your computer to the robot&#039;s WiFi network.&lt;br /&gt;
# Confirm that you can SSH to the robot at root@192.168.5.1 using the SSH key that you selected (or were provided) in dustbuilder. If you don&#039;t have a working key, you can always connect to the robot via serial (there&#039;s a root shell running there)&lt;br /&gt;
# Backup all the calibration data from the robot. {{Highlight&lt;br /&gt;
| code = robot# tar -czf /tmp/calibration.tar.gz /mnt&lt;br /&gt;
&lt;br /&gt;
## on your computer, scp it out, or if dropbear utils aren&#039;t available, you can just do:&lt;br /&gt;
computer# ssh -i *rsa root@192.168.5.1 cat /tmp/calibration.tar.gz &amp;gt; calibration.tar.gz&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Download Valetudo&#039;s latest release from https://github.com/Hypfer/Valetudo and copy it into the robot to &amp;lt;code&amp;gt;/data/valetudo&amp;lt;/code&amp;gt;. {{Highlight&lt;br /&gt;
| code = robot# scp leo@computer:~/valetudo /data/valetudo&lt;br /&gt;
robot# chmod +x /data/valetudo&lt;br /&gt;
robot# cp /misc/_root_postboot.sh.tpl /data/_root_postboot.sh&lt;br /&gt;
robot# chmod +x /data/_root_postboot.sh&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Reboot and once the robot comes back up, you should see the Valetudo web interface on the robot at http://192.168.5.1. Complete the robot setup using Valetudo (such as connecting it to your home&#039;s WiFi network).&lt;br /&gt;
&lt;br /&gt;
=== Camera Streaming ===&lt;br /&gt;
The camera on the vacuum cleaner can be seen via go2rtc. Anthony Zhang&#039;s blog talks more about this (https://anthony-zhang.me/blog/offline-robot-vacuum/) and it requires installing tihmstar&#039;s vacuumstreamer repo and the official go2rtc binary:&lt;br /&gt;
* https://github.com/tihmstar/vacuumstreamer&lt;br /&gt;
* https://github.com/AlexxIT/go2rtc&lt;br /&gt;
{{Warning|Trusting random people on the internet... part 2|The vacuumstreamer repo has a video_monitor binary which seems to have come from another AVA powered vacuum cleaner. The source of this binary isn&#039;t clear. You&#039;re running someone&#039;s binary blob at this point.}}{{Highlight&lt;br /&gt;
| code = ## on the robot, make /data/vacuumstramer&lt;br /&gt;
robot# mkdir /data/vacuumstreamer&lt;br /&gt;
&lt;br /&gt;
## on your computer, download vacuumstreamer packages and the go2rtc binary and copy it to the robot&lt;br /&gt;
computer# git clone https://github.com/tihmstar/vacuumstreamer&lt;br /&gt;
computer# scp vacuumstreamer/vacuumstreamer.so                root@vacuum:/data/vacuumstreamer/&lt;br /&gt;
computer# scp vacuumstreamer/dist/usr/bin/video_monitor       root@vacuum:/data/vacuumstreamer/&lt;br /&gt;
computer# scp -r vacuumstreamer/dist/ava/conf/video_monitor   root@vacuum:/data/vacuumstreamer/ava_conf_video_monitor&lt;br /&gt;
&lt;br /&gt;
computer# wget https://github.com/AlexxIT/go2rtc/releases/download/v1.9.13/go2rtc_linux_arm64&lt;br /&gt;
computer# scp go2rtc_linux_arm64  root@vacuum:/data/vacuumstreamer/go2rtc&lt;br /&gt;
&lt;br /&gt;
## back on the robot, set up the streamer&lt;br /&gt;
robot# cp -r /mnt/private /data/vacuumstreamer/mnt_private_copy &lt;br /&gt;
robot# touch /data/vacuumstreamer/mnt_private_copy/certificate.bin  # workaround for missing certificate bug, see https://github.com/tihmstar/vacuumstreamer/issues/1 for details&lt;br /&gt;
robot# cat &amp;lt;&amp;lt;EOF &amp;gt;&amp;gt; /data/_root_postboot.sh&lt;br /&gt;
&lt;br /&gt;
if [[ -f /data/vacuumstreamer/video_monitor ]]; then&lt;br /&gt;
    mount --bind /data/vacuumstreamer/ava_conf_video_monitor /ava/conf/video_monitor&lt;br /&gt;
    mount --bind /data/vacuumstreamer/mnt_private_copy /mnt/private&lt;br /&gt;
    LD_PRELOAD=/data/vacuumstreamer/vacuumstreamer.so /data/vacuumstreamer/video_monitor &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&lt;br /&gt;
    /data/vacuumstreamer/go2rtc -c &#039;{&amp;quot;streams&amp;quot;: {&amp;quot;tcp_magic&amp;quot;: &amp;quot;tcp://127.0.0.1:6969&amp;quot;&amp;lt;nowiki&amp;gt;}}&amp;lt;/nowiki&amp;gt;&#039; &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&lt;br /&gt;
fi&lt;br /&gt;
EOF&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Reboot. You should see the video stream at http://robot:1984.&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Dreame_L40_Ultra&amp;diff=7723</id>
		<title>Dreame L40 Ultra</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Dreame_L40_Ultra&amp;diff=7723"/>
		<updated>2025-12-24T00:00:33Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The Dreame L40 Ultra is a robot vacuum and mop cleaner. I purchased this off Amazon in early November 2025 for about $580 CAD.&lt;br /&gt;
&lt;br /&gt;
== Running Valetudo ==&lt;br /&gt;
The Dreame L40 Ultra robot will need the Dreame App to function. This requires the robot to have a constant internet network connection to Dreame&#039;s servers. If this doesn&#039;t sound appleaing, you can install Valetudo on this robot vacuum which will present a web-based interface served from the robot itself that has most of the functionality from the Dreame app. Part of the rooting process can also divorce your robot from all internet/cloud-facing services so it is completely self-sufficient on your home network.&lt;br /&gt;
&lt;br /&gt;
Be aware of the downsides with Valetudo:&lt;br /&gt;
&lt;br /&gt;
* No native IOS client, but you can always control it via a web browser (even from a desktop! so it&#039;s really a plus)&lt;br /&gt;
* Controlling it from outside your home network or sharing it with multiple people requires more work (a reverse proxy or tailscale should do the trick). If you&#039;re using HomeAssistant, this might not be a huge issue as there&#039;s a HA integration with Valetudo.&lt;br /&gt;
* Some of the more niche features from Dreame is not available (CleanGenius is not available, so things like  auto-recleaning, or auto-rewashing won&#039;t be available. There is no way to do the washboard base cleaning in Valetudo either)&lt;br /&gt;
* No multi-level / multi-map support (you can sort of hack this by replacing the map directory when you switch the robot)&lt;br /&gt;
* No camera view (but you can install rtc2go in addition to Valetudo and watch the camera stream it that way)&lt;br /&gt;
* Map view is good, but there&#039;s no way to hide/delete rooms.&lt;br /&gt;
&lt;br /&gt;
The upside is that it is completely self-sufficient and doesn&#039;t require any cloud services to function.&lt;br /&gt;
&lt;br /&gt;
== Rooting and installing Valetudo ==&lt;br /&gt;
You can root and install Valetudo on the Dreame L40 Ultra without needing to disassemble the vacuum cleaner by using the exposed debug headers. The steps on how to do this is documented on Valetudo&#039;s installation page (https://valetudo.cloud/pages/installation/dreame.html) and ideally requires the Dreame breakout PCB because you will need to use the FEL / fastboot installation method for this model. The UART Shell method is not available for this model due to secure boot. The installation instructions is somewhat scattered across multiple different pages and websites and I am documenting everything here for this specific model so it&#039;s all together.&lt;br /&gt;
&lt;br /&gt;
{{Warning|Trusting random people on the internet|Pretty much everything below requires trusting that Dennis Giese (the person behind rooting the robot) isn&#039;t doing something malicous.&lt;br /&gt;
&lt;br /&gt;
The FEL boot images that we&#039;ll be running is entirely generated by the dustbuilder service that Dennis is providing and it isn&#039;t completely open source at the time of writing.&lt;br /&gt;
&lt;br /&gt;
Once you gain root access, you can look around for anything suspicious. At the time of writing, I was able to get a robot that, after changing the NTP server to one hosted locally, was absolutely network-silent.&lt;br /&gt;
&lt;br /&gt;
But then again... you did get a Chinese robot vacuum cleaner that requires constant Internet access...}}&lt;br /&gt;
&lt;br /&gt;
=== Prerequsites ===&lt;br /&gt;
Things you need to root and install Valetudo:&lt;br /&gt;
&lt;br /&gt;
* Dreame Breakout PCB - https://github.com/Hypfer/valetudo-dreameadapter&lt;br /&gt;
** You can order the PCB from JLCPCB and it&#039;ll come in about 2 weeks (~$7 CAD).&lt;br /&gt;
** You will need to make sure you have 2mm headers ($2 CAD), some 2.4mm female headers ($1 CAD), a DIP button, a vertical micro USB header that fits the PCB layout ($2 CAD for 10), and a USB 2.0 type A female port ($2 CAD for 20, optional for this device/installation process).&lt;br /&gt;
* A 3.3v USB serial adapter and some dupont wires (optional, but is nice to have if you want to connect to the robot via serial)&lt;br /&gt;
* A computer running Linux (Ubuntu or Debian is preferable because we need to run Hypfer&#039;s fork of the LiveSuit program which was targeted for those distros). Ideally, this computer shouldn&#039;t be used for something important because you&#039;ll need to download/install a kernel module (which I don&#039;t really know where the source is from)&lt;br /&gt;
** Install Livesuit (https://github.com/Hypfer/valetudo-sunxi-livesuit). Follow the instructions in the README&lt;br /&gt;
* A rooted FEL firmware from https://builder.dontvacuum.me/_dreame_r2492.html (more on this in the instructions below).&lt;br /&gt;
** You will need to use the dustbuilder tool to generate FEL image that allows us to run the robot without secure boot. The tools to do this doesn&#039;t seem to be fully open source, possibly because Dennis (the author) wants to delay Dreame from patching this issue. &lt;br /&gt;
&lt;br /&gt;
=== Installation ===&lt;br /&gt;
&lt;br /&gt;
# Pry off the front of the robot to expose the debug headers. Your Dreame Breakout PCB with the 2mm headers should fit and the top of the breakout board should be facing the LIDAR.&lt;br /&gt;
# Download https://builder.dontvacuum.me/nextgen/dust-livesuit-mr813-ddr4.img and then run LiveSuit. Set the image to dust-livesuit-mr813-ddr4.img&lt;br /&gt;
# Factory reset the vacuum cleaner by pressing the reset button next to the wifi light for 10 seconds (it&#039;s next to the dust compartment). Once it comes back up, press and hold the power button until the vacuum is off&lt;br /&gt;
# While pressing the boot sel button the breakout PCB, press and hold power button on the vacuum cleaner for 5 seconds until the vacuum indicator lights are flashing. You can release the boot sel button once this happens.&lt;br /&gt;
# Plug a USB micro cable from your computer to the breakout port&lt;br /&gt;
# LiveSuit will ask to format the partition. Press No.&lt;br /&gt;
# In a terminal, you should be able to run: {{Highlight&lt;br /&gt;
| code = # fastboot devices&lt;br /&gt;
# fastboot getvar dustversion&lt;br /&gt;
# fastboot getvar config&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Go to the dustbuilder page (https://builder.dontvacuum.me/_dreame_r2492.html) and enter your device serial number (the QR code next to the dust compartment or the serial number under the dust compartment), the config value from the command above, and ensure you check off &#039;Create FEL image&#039;. The dustbuilder took about 20 minutes for me before it generated a FEL image I can download. While that&#039;s being generated, we can continue...&lt;br /&gt;
# Backup the boot stages in case we need it. Keep the bin files somewhere safe. They should all be approximately 400MB.{{Highlight&lt;br /&gt;
| code = # fastboot get_staged dustx100.bin&lt;br /&gt;
# fastboot oem stage1&lt;br /&gt;
# fastboot get_staged dustx101.bin&lt;br /&gt;
# fastboot oem stage2&lt;br /&gt;
# fastboot get_staged dustx102.bin&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Download the firmware image from the dustbuilder. You should have received a dreame.vacuum.r2492_1680_fel.zip file in the package. Extract the contents.&lt;br /&gt;
# Open LiveSuit and set the image to the _dreame.vacuum.r2492_phoenixsuit.img image file.&lt;br /&gt;
# Make a note of the contents in check.txt. You need this ready for the next step.&lt;br /&gt;
# Reboot the vacuum cleaner and re-enter fastboot again. We do this because there is a 160 second watchdog that reboots the robot and we want as much time as we can get to avoid having it reboot on us before we&#039;re done.&lt;br /&gt;
# Run: {{Highlight&lt;br /&gt;
| code = # fastboot getvar config   ## Confirm fastboot works&lt;br /&gt;
# fastboot oem dust xxxxxxx   ## Replace xxxx with the contents in check.txt&lt;br /&gt;
# fastboot oem prep   ## If this fails, don&#039;t proceed. Ensure LiveSuit has the correct img file set&lt;br /&gt;
## Ensure that everything below can run before the watchdog resets the device. If you took too long above, reset and try again.&lt;br /&gt;
# fastboot flash toc1 toc1.img   ## Should return OKAY. Stop otherwise and re-assess.&lt;br /&gt;
# fastboot flash boot1 boot.img&lt;br /&gt;
# fastboot flash rootfs1 rootfs.img&lt;br /&gt;
# fastboot flash boot2 boot.img&lt;br /&gt;
# fastboot flash rootfs2 rootfs.img&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Once all the flash commands succeed, you can run &amp;lt;code&amp;gt;fastboot reboot&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Press the outer buttons together for 3 seconds so that the robot starts its WiFi access point. Connect your computer to the robot&#039;s WiFi network.&lt;br /&gt;
# Confirm that you can SSH to the robot at root@192.168.5.1 using the SSH key that you selected (or were provided) in dustbuilder. If you don&#039;t have a working key, you can always connect to the robot via serial (there&#039;s a root shell running there)&lt;br /&gt;
# Backup all the calibration data from the robot. {{Highlight&lt;br /&gt;
| code = robot# tar -czf /tmp/calibration.tar.gz /mnt&lt;br /&gt;
&lt;br /&gt;
## on your computer, scp it out, or if dropbear utils aren&#039;t available, you can just do:&lt;br /&gt;
computer# ssh -i *rsa root@192.168.5.1 cat /tmp/calibration.tar.gz &amp;gt; calibration.tar.gz&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Download Valetudo&#039;s latest release from https://github.com/Hypfer/Valetudo and copy it into the robot to &amp;lt;code&amp;gt;/data/valetudo&amp;lt;/code&amp;gt;. {{Highlight&lt;br /&gt;
| code = robot# scp leo@computer:~/valetudo /data/valetudo&lt;br /&gt;
robot# chmod +x /data/valetudo&lt;br /&gt;
robot# cp /misc/_root_postboot.sh.tpl /data/_root_postboot.sh&lt;br /&gt;
robot# chmod +x /data/_root_postboot.sh&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Reboot and once the robot comes back up, you should see the Valetudo web interface on the robot at http://192.168.5.1. Complete the robot setup using Valetudo (such as connecting it to your home&#039;s WiFi network).&lt;br /&gt;
&lt;br /&gt;
=== Camera Streaming ===&lt;br /&gt;
The camera on the vacuum cleaner can be seen via go2rtc. Anthony Zhang&#039;s blog talks more about this (https://anthony-zhang.me/blog/offline-robot-vacuum/) and it requires installing tihmstar&#039;s vacuumstreamer repo and the official go2rtc binary:&lt;br /&gt;
* https://github.com/tihmstar/vacuumstreamer&lt;br /&gt;
* https://github.com/AlexxIT/go2rtc&lt;br /&gt;
{{Warning|Trusting random people on the internet... part 2|The vacuumstreamer repo has a video_monitor binary which seems to have come from another AVA powered vacuum cleaner. The source of this binary isn&#039;t clear. You&#039;re running someone&#039;s binary blob at this point.}}{{Highlight&lt;br /&gt;
| code = ## on the robot, make /data/vacuumstramer&lt;br /&gt;
robot# mkdir /data/vacuumstreamer&lt;br /&gt;
&lt;br /&gt;
## on your computer, download vacuumstreamer packages and the go2rtc binary and copy it to the robot&lt;br /&gt;
computer# git clone https://github.com/tihmstar/vacuumstreamer&lt;br /&gt;
computer# scp vacuumstreamer/vacuumstreamer.so                root@vacuum:/data/vacuumstreamer/&lt;br /&gt;
computer# scp vacuumstreamer/dist/usr/bin/video_monitor       root@vacuum:/data/vacuumstreamer/&lt;br /&gt;
computer# scp -r vacuumstreamer/dist/ava/conf/video_monitor   root@vacuum:/data/vacuumstreamer/ava_conf_video_monitor&lt;br /&gt;
&lt;br /&gt;
computer# wget https://github.com/AlexxIT/go2rtc/releases/download/v1.9.13/go2rtc_linux_arm64&lt;br /&gt;
computer# scp go2rtc_linux_arm64  root@vacuum:/data/vacuumstreamer/go2rtc&lt;br /&gt;
&lt;br /&gt;
## back on the robot, set up the streamer&lt;br /&gt;
robot# cp -r /mnt/private /data/vacuumstreamer/mnt_private_copy &lt;br /&gt;
robot# touch /data/vacuumstreamer/mnt_private_copy/certificate.bin  # workaround for missing certificate bug, see https://github.com/tihmstar/vacuumstreamer/issues/1 for details&lt;br /&gt;
robot# cat &amp;lt;&amp;lt;EOF &amp;gt;&amp;gt; /data/_root_postboot.sh&lt;br /&gt;
&lt;br /&gt;
if [[ -f /data/vacuumstreamer/video_monitor ]]; then&lt;br /&gt;
    mount --bind /data/vacuumstreamer/ava_conf_video_monitor /ava/conf/video_monitor&lt;br /&gt;
    mount --bind /data/vacuumstreamer/mnt_private_copy /mnt/private&lt;br /&gt;
    LD_PRELOAD=/data/vacuumstreamer/vacuumstreamer.so /data/vacuumstreamer/video_monitor &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&lt;br /&gt;
    /data/vacuumstreamer/go2rtc -c &#039;{&amp;quot;streams&amp;quot;: {&amp;quot;tcp_magic&amp;quot;: &amp;quot;tcp://127.0.0.1:6969&amp;quot;&amp;lt;nowiki&amp;gt;}}&amp;lt;/nowiki&amp;gt;&#039; &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&lt;br /&gt;
fi&lt;br /&gt;
EOF&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Reboot. You should see the video stream at http://robot:1984.&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Dreame_L40_Ultra&amp;diff=7722</id>
		<title>Dreame L40 Ultra</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Dreame_L40_Ultra&amp;diff=7722"/>
		<updated>2025-12-23T23:36:43Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The Dreame L40 Ultra is a robot vacuum and mop cleaner. I purchased this off Amazon in early November 2025 for about $580 CAD.&lt;br /&gt;
&lt;br /&gt;
== Rooting and installing Valetudo ==&lt;br /&gt;
You can root and install Valetudo on the Dreame L40 Ultra without needing to disassemble the vacuum cleaner by using the exposed debug headers. The steps on how to do this is documented on Valetudo&#039;s installation page (https://valetudo.cloud/pages/installation/dreame.html) and ideally requires the Dreame breakout PCB because you will need to use the FEL / fastboot installation method for this model. The installation instructions is somewhat scattered across multiple different pages and websites and I am documenting everything here for this specific model so it&#039;s all together.&lt;br /&gt;
&lt;br /&gt;
{{Warning|Trusting random people on the internet|Pretty much everything below requires trusting that Dennis Giese (the person behind rooting the robot) isn&#039;t doing something malicous.&lt;br /&gt;
&lt;br /&gt;
The FEL boot images that we&#039;ll be running is entirely generated by the dustbuilder service that Dennis is providing and it isn&#039;t completely open source at the time of writing.&lt;br /&gt;
&lt;br /&gt;
Once you gain root access, you can look around for anything suspicious. At the time of writing, I was able to get a robot that, after changing the NTP server to one hosted locally, was absolutely network-silent.&lt;br /&gt;
&lt;br /&gt;
But then again... you did get a Chinese robot vacuum cleaner that requires constant Internet access...}}&lt;br /&gt;
&lt;br /&gt;
=== Prerequsites ===&lt;br /&gt;
Things you need to root and install Valetudo:&lt;br /&gt;
&lt;br /&gt;
* Dreame Breakout PCB - https://github.com/Hypfer/valetudo-dreameadapter&lt;br /&gt;
** You can order the PCB from JLCPCB and it&#039;ll come in about 2 weeks (~$7 CAD).&lt;br /&gt;
** You will need to make sure you have 2mm headers ($2 CAD), some 2.4mm female headers ($1 CAD), a DIP button, a vertical micro USB header that fits the PCB layout ($2 CAD for 10), and a USB 2.0 type A female port ($2 CAD for 20, optional for this device/installation process).&lt;br /&gt;
* A 3.3v USB serial adapter and some dupont wires (optional, but is nice to have if you want to connect to the robot via serial)&lt;br /&gt;
* A computer running Linux (Ubuntu or Debian is preferable because we need to run Hypfer&#039;s fork of the LiveSuit program which was targeted for those distros). Ideally, this computer shouldn&#039;t be used for something important because you&#039;ll need to download/install a kernel module (which I don&#039;t really know where the source is from)&lt;br /&gt;
** Install Livesuit (https://github.com/Hypfer/valetudo-sunxi-livesuit). Follow the instructions in the README&lt;br /&gt;
* A rooted FEL firmware from https://builder.dontvacuum.me/_dreame_r2492.html (more on this in the instructions below).&lt;br /&gt;
** You will need to use the dustbuilder tool to generate FEL image that allows us to run the robot without secure boot. The tools to do this doesn&#039;t seem to be fully open source, possibly because Dennis (the author) wants to delay Dreame from patching this issue. &lt;br /&gt;
&lt;br /&gt;
=== Installation ===&lt;br /&gt;
&lt;br /&gt;
# Pry off the front of the robot to expose the debug headers. Your Dreame Breakout PCB with the 2mm headers should fit and the top of the breakout board should be facing the LIDAR.&lt;br /&gt;
# Download https://builder.dontvacuum.me/nextgen/dust-livesuit-mr813-ddr4.img and then run LiveSuit. Set the image to dust-livesuit-mr813-ddr4.img&lt;br /&gt;
# Factory reset the vacuum cleaner by pressing the reset button next to the wifi light for 10 seconds (it&#039;s next to the dust compartment). Once it comes back up, press and hold the power button until the vacuum is off&lt;br /&gt;
# While pressing the boot sel button the breakout PCB, press and hold power button on the vacuum cleaner for 5 seconds until the vacuum indicator lights are flashing. You can release the boot sel button once this happens.&lt;br /&gt;
# Plug a USB micro cable from your computer to the breakout port&lt;br /&gt;
# LiveSuit will ask to format the partition. Press No.&lt;br /&gt;
# In a terminal, you should be able to run: {{Highlight&lt;br /&gt;
| code = # fastboot devices&lt;br /&gt;
# fastboot getvar dustversion&lt;br /&gt;
# fastboot getvar config&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Go to the dustbuilder page (https://builder.dontvacuum.me/_dreame_r2492.html) and enter your device serial number (the QR code next to the dust compartment or the serial number under the dust compartment), the config value from the command above, and ensure you check off &#039;Create FEL image&#039;. The dustbuilder took about 20 minutes for me before it generated a FEL image I can download. While that&#039;s being generated, we can continue...&lt;br /&gt;
# Backup the boot stages in case we need it. Keep the bin files somewhere safe. They should all be approximately 400MB.{{Highlight&lt;br /&gt;
| code = # fastboot get_staged dustx100.bin&lt;br /&gt;
# fastboot oem stage1&lt;br /&gt;
# fastboot get_staged dustx101.bin&lt;br /&gt;
# fastboot oem stage2&lt;br /&gt;
# fastboot get_staged dustx102.bin&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Download the firmware image from the dustbuilder. You should have received a dreame.vacuum.r2492_1680_fel.zip file in the package. Extract the contents.&lt;br /&gt;
# Open LiveSuit and set the image to the _dreame.vacuum.r2492_phoenixsuit.img image file.&lt;br /&gt;
# Make a note of the contents in check.txt. You need this ready for the next step.&lt;br /&gt;
# Reboot the vacuum cleaner and re-enter fastboot again. We do this because there is a 160 second watchdog that reboots the robot and we want as much time as we can get to avoid having it reboot on us before we&#039;re done.&lt;br /&gt;
# Run: {{Highlight&lt;br /&gt;
| code = # fastboot getvar config   ## Confirm fastboot works&lt;br /&gt;
# fastboot oem dust xxxxxxx   ## Replace xxxx with the contents in check.txt&lt;br /&gt;
# fastboot oem prep   ## If this fails, don&#039;t proceed. Ensure LiveSuit has the correct img file set&lt;br /&gt;
## Ensure that everything below can run before the watchdog resets the device. If you took too long above, reset and try again.&lt;br /&gt;
# fastboot flash toc1 toc1.img   ## Should return OKAY. Stop otherwise and re-assess.&lt;br /&gt;
# fastboot flash boot1 boot.img&lt;br /&gt;
# fastboot flash rootfs1 rootfs.img&lt;br /&gt;
# fastboot flash boot2 boot.img&lt;br /&gt;
# fastboot flash rootfs2 rootfs.img&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Once all the flash commands succeed, you can run &amp;lt;code&amp;gt;fastboot reboot&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Press the outer buttons together for 3 seconds so that the robot starts its WiFi access point. Connect your computer to the robot&#039;s WiFi network.&lt;br /&gt;
# Confirm that you can SSH to the robot at root@192.168.5.1 using the SSH key that you selected (or were provided) in dustbuilder. If you don&#039;t have a working key, you can always connect to the robot via serial (there&#039;s a root shell running there)&lt;br /&gt;
# Backup all the calibration data from the robot. {{Highlight&lt;br /&gt;
| code = robot# tar -czf /tmp/calibration.tar.gz /mnt&lt;br /&gt;
&lt;br /&gt;
## on your computer, scp it out, or if dropbear utils aren&#039;t available, you can just do:&lt;br /&gt;
computer# ssh -i *rsa root@192.168.5.1 cat /tmp/calibration.tar.gz &amp;gt; calibration.tar.gz&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Download Valetudo&#039;s latest release from https://github.com/Hypfer/Valetudo and copy it into the robot to &amp;lt;code&amp;gt;/data/valetudo&amp;lt;/code&amp;gt;. {{Highlight&lt;br /&gt;
| code = robot# scp leo@computer:~/valetudo /data/valetudo&lt;br /&gt;
robot# chmod +x /data/valetudo&lt;br /&gt;
robot# cp /misc/_root_postboot.sh.tpl /data/_root_postboot.sh&lt;br /&gt;
robot# chmod +x /data/_root_postboot.sh&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Reboot and once the robot comes back up, you should see the Valetudo web interface on the robot at http://192.168.5.1. Complete the robot setup using Valetudo (such as connecting it to your home&#039;s WiFi network).&lt;br /&gt;
&lt;br /&gt;
=== Camera Streaming ===&lt;br /&gt;
The camera on the vacuum cleaner can be seen via go2rtc. Anthony Zhang&#039;s blog talks more about this (https://anthony-zhang.me/blog/offline-robot-vacuum/) and it requires installing tihmstar&#039;s vacuumstreamer repo and the official go2rtc binary:&lt;br /&gt;
* https://github.com/tihmstar/vacuumstreamer&lt;br /&gt;
* https://github.com/AlexxIT/go2rtc&lt;br /&gt;
{{Warning|Trusting random people on the internet... part 2|The vacuumstreamer repo has a video_monitor binary which seems to have come from another AVA powered vacuum cleaner. The source of this binary isn&#039;t clear. You&#039;re running someone&#039;s binary blob at this point.}}{{Highlight&lt;br /&gt;
| code = ## on the robot, make /data/vacuumstramer&lt;br /&gt;
robot# mkdir /data/vacuumstreamer&lt;br /&gt;
&lt;br /&gt;
## on your computer, download vacuumstreamer packages and the go2rtc binary and copy it to the robot&lt;br /&gt;
computer# git clone https://github.com/tihmstar/vacuumstreamer&lt;br /&gt;
computer# scp vacuumstreamer/vacuumstreamer.so                root@vacuum:/data/vacuumstreamer/&lt;br /&gt;
computer# scp vacuumstreamer/dist/usr/bin/video_monitor       root@vacuum:/data/vacuumstreamer/&lt;br /&gt;
computer# scp -r vacuumstreamer/dist/ava/conf/video_monitor   root@vacuum:/data/vacuumstreamer/ava_conf_video_monitor&lt;br /&gt;
&lt;br /&gt;
computer# wget https://github.com/AlexxIT/go2rtc/releases/download/v1.9.13/go2rtc_linux_arm64&lt;br /&gt;
computer# scp go2rtc_linux_arm64  root@vacuum:/data/vacuumstreamer/go2rtc&lt;br /&gt;
&lt;br /&gt;
## back on the robot, set up the streamer&lt;br /&gt;
robot# cp -r /mnt/private /data/vacuumstreamer/mnt_private_copy &lt;br /&gt;
robot# touch /data/vacuumstreamer/mnt_private_copy/certificate.bin  # workaround for missing certificate bug, see https://github.com/tihmstar/vacuumstreamer/issues/1 for details&lt;br /&gt;
robot# cat &amp;lt;&amp;lt;EOF &amp;gt;&amp;gt; /data/_root_postboot.sh&lt;br /&gt;
&lt;br /&gt;
if [[ -f /data/vacuumstreamer/video_monitor ]]; then&lt;br /&gt;
    mount --bind /data/vacuumstreamer/ava_conf_video_monitor /ava/conf/video_monitor&lt;br /&gt;
    mount --bind /data/vacuumstreamer/mnt_private_copy /mnt/private&lt;br /&gt;
    LD_PRELOAD=/data/vacuumstreamer/vacuumstreamer.so /data/vacuumstreamer/video_monitor &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&lt;br /&gt;
    /data/vacuumstreamer/go2rtc -c &#039;{&amp;quot;streams&amp;quot;: {&amp;quot;tcp_magic&amp;quot;: &amp;quot;tcp://127.0.0.1:6969&amp;quot;&amp;lt;nowiki&amp;gt;}}&amp;lt;/nowiki&amp;gt;&#039; &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&lt;br /&gt;
fi&lt;br /&gt;
EOF&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Reboot. You should see the video stream at http://robot:1984.&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Dreame_L40_Ultra&amp;diff=7721</id>
		<title>Dreame L40 Ultra</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Dreame_L40_Ultra&amp;diff=7721"/>
		<updated>2025-12-23T23:27:00Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The Dreame L40 Ultra is a robot vacuum and mop cleaner. I purchased this off Amazon in early November 2025 for about $580 CAD.&lt;br /&gt;
&lt;br /&gt;
== Rooting and installing Valetudo ==&lt;br /&gt;
You can root and install Valetudo on the Dreame L40 Ultra without needing to disassemble the vacuum cleaner by using the exposed debug header pins. The steps on how to do this is documented on Valetudo&#039;s installation page (https://valetudo.cloud/pages/installation/dreame.html) and ideally requires the Dreame breakout PCB because you will need to use the FEL / fastboot installation method for this model. The installation instructions is somewhat scattered across multiple different pages and websites and I am documenting everything here for this specific model so it&#039;s all together.&lt;br /&gt;
&lt;br /&gt;
{{Warning|Trusting random people on the internet|Pretty much everything below requires trusting that Dennis Giese (the person behind rooting the robot) isn&#039;t doing something malicous.&lt;br /&gt;
&lt;br /&gt;
The FEL boot images that we&#039;ll be running is entirely generated by the dustbuilder service that Dennis is providing and it isn&#039;t completely open source at the time of writing.}}&lt;br /&gt;
&lt;br /&gt;
=== Prerequsites ===&lt;br /&gt;
Things you need to root and install Valetudo:&lt;br /&gt;
&lt;br /&gt;
* Dreame Breakout PCB - https://github.com/Hypfer/valetudo-dreameadapter&lt;br /&gt;
** You can order the PCB from JLCPCB and it&#039;ll come in about 2 weeks (~$7 CAD).&lt;br /&gt;
** You will need to make sure you have 2mm headers ($2 CAD), some 2.4mm female headers ($1 CAD), a DIP button, a vertical micro USB header that fits the PCB layout ($2 CAD for 10), and a USB 2.0 type A female port ($2 CAD for 20, optional for this device/installation process).&lt;br /&gt;
* A 3.3v USB serial adapter and some dupont wires (optional, but is nice to have if you want to connect to the robot via serial)&lt;br /&gt;
* A computer running Linux (Ubuntu or Debian is preferable because we need to run Hypfer&#039;s fork of the LiveSuit program which was targeted for those distros). Ideally, this computer shouldn&#039;t be used for something important because you&#039;ll need to download/install a kernel module (which I don&#039;t really know where the source is from)&lt;br /&gt;
** Install Livesuit (https://github.com/Hypfer/valetudo-sunxi-livesuit). Follow the instructions in the README&lt;br /&gt;
* A rooted FEL firmware from https://builder.dontvacuum.me/_dreame_r2492.html (more on this in the instructions below).&lt;br /&gt;
** You will need to use the dustbuilder tool to generate FEL image that allows us to run the robot without secure boot. The tools to do this doesn&#039;t seem to be fully open source, possibly because Dennis (the author) wants to delay Dreame from patching this issue. &lt;br /&gt;
&lt;br /&gt;
=== Installation ===&lt;br /&gt;
&lt;br /&gt;
# Pry off the front of the robot to expose the debug headers. Your Dreame Breakout PCB with the 2mm headers should fit and the top of the breakout board should be facing the LIDAR.&lt;br /&gt;
# Download https://builder.dontvacuum.me/nextgen/dust-livesuit-mr813-ddr4.img and then run LiveSuit. Set the image to dust-livesuit-mr813-ddr4.img&lt;br /&gt;
# Factory reset the vacuum cleaner by pressing the reset button next to the wifi light for 10 seconds (it&#039;s next to the dust compartment). Once it comes back up, press and hold the power button until the vacuum is off&lt;br /&gt;
# While pressing the boot sel button the breakout PCB, press and hold power button on the vacuum cleaner for 5 seconds until the vacuum indicator lights are flashing. You can release the boot sel button once this happens.&lt;br /&gt;
# Plug a USB micro cable from your computer to the breakout port&lt;br /&gt;
# LiveSuit will ask to format the partition. Press No.&lt;br /&gt;
# In a terminal, you should be able to run: {{Highlight&lt;br /&gt;
| code = # fastboot devices&lt;br /&gt;
# fastboot getvar dustversion&lt;br /&gt;
# fastboot getvar config&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Go to the dustbuilder page (https://builder.dontvacuum.me/_dreame_r2492.html) and enter your device serial number (the QR code next to the dust compartment or the serial number under the dust compartment), the config value from the command above, and ensure you check off &#039;Create FEL image&#039;. The dustbuilder took about 20 minutes for me before it generated a FEL image I can download. While that&#039;s being generated, we can continue...&lt;br /&gt;
# Backup the boot stages in case we need it. Keep the bin files somewhere safe. They should all be approximately 400MB.{{Highlight&lt;br /&gt;
| code = # fastboot get_staged dustx100.bin&lt;br /&gt;
# fastboot oem stage1&lt;br /&gt;
# fastboot get_staged dustx101.bin&lt;br /&gt;
# fastboot oem stage2&lt;br /&gt;
# fastboot get_staged dustx102.bin&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Download the firmware image from the dustbuilder. You should have received a dreame.vacuum.r2492_1680_fel.zip file in the package. Extract the contents.&lt;br /&gt;
# Open LiveSuit and set the image to the _dreame.vacuum.r2492_phoenixsuit.img image file.&lt;br /&gt;
# Make a note of the contents in check.txt. You need this ready for the next step.&lt;br /&gt;
# Reboot the vacuum cleaner and re-enter fastboot again. We do this because there is a 160 second watchdog that reboots the robot and we want as much time as we can get to avoid having it reboot on us before we&#039;re done.&lt;br /&gt;
# Run: {{Highlight&lt;br /&gt;
| code = # fastboot getvar config   ## Confirm fastboot works&lt;br /&gt;
# fastboot oem dust xxxxxxx   ## Replace xxxx with the contents in check.txt&lt;br /&gt;
# fastboot oem prep   ## If this fails, don&#039;t proceed. Ensure LiveSuit has the correct img file set&lt;br /&gt;
## Ensure that everything below can run before the watchdog resets the device. If you took too long above, reset and try again.&lt;br /&gt;
# fastboot flash toc1 toc1.img   ## Should return OKAY. Stop otherwise and re-assess.&lt;br /&gt;
# fastboot flash boot1 boot.img&lt;br /&gt;
# fastboot flash rootfs1 rootfs.img&lt;br /&gt;
# fastboot flash boot2 boot.img&lt;br /&gt;
# fastboot flash rootfs2 rootfs.img&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Once all the flash commands succeed, you can run &amp;lt;code&amp;gt;fastboot reboot&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Press the outer buttons together for 3 seconds so that the robot starts its WiFi access point. Connect your computer to the robot&#039;s WiFi network.&lt;br /&gt;
# Confirm that you can SSH to the robot at root@192.168.5.1 using the SSH key that you selected (or were provided) in dustbuilder. If you don&#039;t have a working key, you can always connect to the robot via serial (there&#039;s a root shell running there)&lt;br /&gt;
# Backup all the calibration data from the robot. {{Highlight&lt;br /&gt;
| code = robot# tar -czf /tmp/calibration.tar.gz /mnt&lt;br /&gt;
&lt;br /&gt;
## on your computer, scp it out, or if dropbear utils aren&#039;t available, you can just do:&lt;br /&gt;
computer# ssh -i *rsa root@192.168.5.1 cat /tmp/calibration.tar.gz &amp;gt; calibration.tar.gz&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Download Valetudo&#039;s latest release from https://github.com/Hypfer/Valetudo and copy it into the robot to &amp;lt;code&amp;gt;/data/valetudo&amp;lt;/code&amp;gt;. {{Highlight&lt;br /&gt;
| code = robot# scp leo@computer:~/valetudo /data/valetudo&lt;br /&gt;
robot# chmod +x /data/valetudo&lt;br /&gt;
robot# cp /misc/_root_postboot.sh.tpl /data/_root_postboot.sh&lt;br /&gt;
robot# chmod +x /data/_root_postboot.sh&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Reboot and once the robot comes back up, you should see the Valetudo web interface on the robot at http://192.168.5.1. Complete the robot setup using Valetudo (such as connecting it to your home&#039;s WiFi network).&lt;br /&gt;
&lt;br /&gt;
=== Camera Streaming ===&lt;br /&gt;
The camera on the vacuum cleaner can be seen via go2rtc. Anthony Zhang&#039;s blog talks more about this (https://anthony-zhang.me/blog/offline-robot-vacuum/) and it requires installing tihmstar&#039;s vacuumstreamer repo and the official go2rtc binary:&lt;br /&gt;
* https://github.com/tihmstar/vacuumstreamer&lt;br /&gt;
* https://github.com/AlexxIT/go2rtc&lt;br /&gt;
{{Warning|Trusting random people on the internet... part 2|The vacuumstreamer repo has a video_monitor binary which seems to have come from another AVA powered vacuum cleaner. The source of this binary isn&#039;t clear. You&#039;re running someone&#039;s binary blob at this point.}}{{Highlight&lt;br /&gt;
| code = ## on the robot, make /data/vacuumstramer&lt;br /&gt;
robot# mkdir /data/vacuumstreamer&lt;br /&gt;
&lt;br /&gt;
## on your computer, download vacuumstreamer packages and the go2rtc binary and copy it to the robot&lt;br /&gt;
computer# git clone https://github.com/tihmstar/vacuumstreamer&lt;br /&gt;
computer# scp vacuumstreamer/vacuumstreamer.so                root@vacuum:/data/vacuumstreamer/&lt;br /&gt;
computer# scp vacuumstreamer/dist/usr/bin/video_monitor       root@vacuum:/data/vacuumstreamer/&lt;br /&gt;
computer# scp -r vacuumstreamer/dist/ava/conf/video_monitor   root@vacuum:/data/vacuumstreamer/ava_conf_video_monitor&lt;br /&gt;
&lt;br /&gt;
computer# wget https://github.com/AlexxIT/go2rtc/releases/download/v1.9.13/go2rtc_linux_arm64&lt;br /&gt;
computer# scp go2rtc_linux_arm64  root@vacuum:/data/vacuumstreamer/go2rtc&lt;br /&gt;
&lt;br /&gt;
## back on the robot, set up the streamer&lt;br /&gt;
robot# cp -r /mnt/private /data/vacuumstreamer/mnt_private_copy &lt;br /&gt;
robot# touch /data/vacuumstreamer/mnt_private_copy/certificate.bin  # workaround for missing certificate bug, see https://github.com/tihmstar/vacuumstreamer/issues/1 for details&lt;br /&gt;
robot# cat &amp;lt;&amp;lt;EOF &amp;gt;&amp;gt; /data/_root_postboot.sh&lt;br /&gt;
&lt;br /&gt;
if [[ -f /data/vacuumstreamer/video_monitor ]]; then&lt;br /&gt;
    mount --bind /data/vacuumstreamer/ava_conf_video_monitor /ava/conf/video_monitor&lt;br /&gt;
    mount --bind /data/vacuumstreamer/mnt_private_copy /mnt/private&lt;br /&gt;
    LD_PRELOAD=/data/vacuumstreamer/vacuumstreamer.so /data/vacuumstreamer/video_monitor &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&lt;br /&gt;
    /data/vacuumstreamer/go2rtc -c &#039;{&amp;quot;streams&amp;quot;: {&amp;quot;tcp_magic&amp;quot;: &amp;quot;tcp://127.0.0.1:6969&amp;quot;&amp;lt;nowiki&amp;gt;}}&amp;lt;/nowiki&amp;gt;&#039; &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&lt;br /&gt;
fi&lt;br /&gt;
EOF&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Reboot. You should see the video stream at http://robot:1984.&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Dreame_L40_Ultra&amp;diff=7720</id>
		<title>Dreame L40 Ultra</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Dreame_L40_Ultra&amp;diff=7720"/>
		<updated>2025-12-23T23:16:55Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The Dreame L40 Ultra is a robot vacuum and mop cleaner. I purchased this off Amazon in early November 2025 for about $580 CAD.&lt;br /&gt;
&lt;br /&gt;
== Rooting and installing Valetudo ==&lt;br /&gt;
You can root and install Valetudo on the Dreame L40 Ultra without needing to disassemble the vacuum cleaner by using the exposed debug header pins. The steps on how to do this is documented on Valetudo&#039;s installation page (https://valetudo.cloud/pages/installation/dreame.html) and ideally requires the Dreame breakout PCB because you will need to use the FEL / fastboot installation method for this model. The installation instructions is somewhat scattered across multiple different pages and websites and I am documenting everything here for this specific model so it&#039;s all together.&lt;br /&gt;
&lt;br /&gt;
=== Prerequsites ===&lt;br /&gt;
Things you need to root and install Valetudo:&lt;br /&gt;
&lt;br /&gt;
* Dreame Breakout PCB - https://github.com/Hypfer/valetudo-dreameadapter&lt;br /&gt;
** You can order the PCB from JLCPCB and it&#039;ll come in about 2 weeks (~$7 CAD).&lt;br /&gt;
** You will need to make sure you have 2mm headers ($2 CAD), some 2.4mm female headers ($1 CAD), a DIP button, a vertical micro USB header that fits the PCB layout ($2 CAD for 10), and a USB 2.0 type A female port ($2 CAD for 20, optional for this device/installation process).&lt;br /&gt;
* A 3.3v USB serial adapter and some dupont wires (optional, but is nice to have if you want to connect to the robot via serial)&lt;br /&gt;
* A computer running Linux (Ubuntu or Debian is preferable because we need to run Hypfer&#039;s fork of the LiveSuit program which was targeted for those distros). Ideally, this computer shouldn&#039;t be used for something important because you&#039;ll need to download/install a kernel module (which I don&#039;t really know where the source is from)&lt;br /&gt;
** Install Livesuit (https://github.com/Hypfer/valetudo-sunxi-livesuit). Follow the instructions in the README&lt;br /&gt;
* A rooted FEL firmware from https://builder.dontvacuum.me/_dreame_r2492.html (more on this in the instructions below).&lt;br /&gt;
** You will need to use the dustbuilder tool to generate FEL image that allows us to run the robot without secure boot. The tools to do this doesn&#039;t seem to be fully open source, possibly because Dennis (the author) wants to delay Dreame from patching this issue. &lt;br /&gt;
&lt;br /&gt;
=== Installation ===&lt;br /&gt;
&lt;br /&gt;
# Pry off the front of the robot to expose the debug headers. Your Dreame Breakout PCB with the 2mm headers should fit and the top of the breakout board should be facing the LIDAR.&lt;br /&gt;
# Download https://builder.dontvacuum.me/nextgen/dust-livesuit-mr813-ddr4.img and then run LiveSuit. Set the image to dust-livesuit-mr813-ddr4.img&lt;br /&gt;
# Factory reset the vacuum cleaner by pressing the reset button next to the wifi light for 10 seconds (it&#039;s next to the dust compartment). Once it comes back up, press and hold the power button until the vacuum is off&lt;br /&gt;
# While pressing the boot sel button the breakout PCB, press and hold power button on the vacuum cleaner for 5 seconds until the vacuum indicator lights are flashing. You can release the boot sel button once this happens.&lt;br /&gt;
# Plug a USB micro cable from your computer to the breakout port&lt;br /&gt;
# LiveSuit will ask to format the partition. Press No.&lt;br /&gt;
# In a terminal, you should be able to run: {{Highlight&lt;br /&gt;
| code = # fastboot devices&lt;br /&gt;
# fastboot getvar dustversion&lt;br /&gt;
# fastboot getvar config&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Go to the dustbuilder page (https://builder.dontvacuum.me/_dreame_r2492.html) and enter your device serial number (the QR code next to the dust compartment or the serial number under the dust compartment), the config value from the command above, and ensure you check off &#039;Create FEL image&#039;. The dustbuilder took about 20 minutes for me before it generated a FEL image I can download. While that&#039;s being generated, we can continue...&lt;br /&gt;
# Backup the boot stages in case we need it. Keep the bin files somewhere safe. They should all be approximately 400MB.{{Highlight&lt;br /&gt;
| code = # fastboot get_staged dustx100.bin&lt;br /&gt;
# fastboot oem stage1&lt;br /&gt;
# fastboot get_staged dustx101.bin&lt;br /&gt;
# fastboot oem stage2&lt;br /&gt;
# fastboot get_staged dustx102.bin&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Download the firmware image from the dustbuilder. You should have received a dreame.vacuum.r2492_1680_fel.zip file in the package. Extract the contents.&lt;br /&gt;
# Open LiveSuit and set the image to the _dreame.vacuum.r2492_phoenixsuit.img image file.&lt;br /&gt;
# Make a note of the contents in check.txt. You need this ready for the next step.&lt;br /&gt;
# Reboot the vacuum cleaner and re-enter fastboot again. We do this because there is a 160 second watchdog that reboots the robot and we want as much time as we can get to avoid having it reboot on us before we&#039;re done.&lt;br /&gt;
# Run: {{Highlight&lt;br /&gt;
| code = # fastboot getvar config   ## Confirm fastboot works&lt;br /&gt;
# fastboot oem dust xxxxxxx   ## Replace xxxx with the contents in check.txt&lt;br /&gt;
# fastboot oem prep   ## If this fails, don&#039;t proceed. Ensure LiveSuit has the correct img file set&lt;br /&gt;
## Ensure that everything below can run before the watchdog resets the device. If you took too long above, reset and try again.&lt;br /&gt;
# fastboot flash toc1 toc1.img   ## Should return OKAY. Stop otherwise and re-assess.&lt;br /&gt;
# fastboot flash boot1 boot.img&lt;br /&gt;
# fastboot flash rootfs1 rootfs.img&lt;br /&gt;
# fastboot flash boot2 boot.img&lt;br /&gt;
# fastboot flash rootfs2 rootfs.img&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Once all the flash commands succeed, you can run &amp;lt;code&amp;gt;fastboot reboot&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Press the outer buttons together for 3 seconds so that the robot starts its WiFi access point. Connect your computer to the robot&#039;s WiFi network.&lt;br /&gt;
# Confirm that you can SSH to the robot at root@192.168.5.1 using the SSH key that you selected (or were provided) in dustbuilder. If you don&#039;t have a working key, you can always connect to the robot via serial (there&#039;s a root shell running there)&lt;br /&gt;
# Backup all the calibration data from the robot. {{Highlight&lt;br /&gt;
| code = robot# tar -czf /tmp/calibration.tar.gz /mnt&lt;br /&gt;
&lt;br /&gt;
## on your computer, scp it out, or if dropbear utils aren&#039;t available, you can just do:&lt;br /&gt;
computer# ssh -i *rsa root@192.168.5.1 cat /tmp/calibration.tar.gz &amp;gt; calibration.tar.gz&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Download Valetudo&#039;s latest release from https://github.com/Hypfer/Valetudo and copy it into the robot to &amp;lt;code&amp;gt;/data/valetudo&amp;lt;/code&amp;gt;. {{Highlight&lt;br /&gt;
| code = robot# scp leo@computer:~/valetudo /data/valetudo&lt;br /&gt;
robot# chmod +x /data/valetudo&lt;br /&gt;
robot# cp /misc/_root_postboot.sh.tpl /data/_root_postboot.sh&lt;br /&gt;
robot# chmod +x /data/_root_postboot.sh&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Reboot and once the robot comes back up, you should see the Valetudo web interface on the robot at http://192.168.5.1. Complete the robot setup using Valetudo (such as connecting it to your home&#039;s WiFi network).&lt;br /&gt;
&lt;br /&gt;
=== Camera Streaming ===&lt;br /&gt;
The camera on the vacuum cleaner can be seen via go2rtc. Anthony Zhang&#039;s blog talks more about this (https://anthony-zhang.me/blog/offline-robot-vacuum/) and it requires installing his vacuumstreamer repo and the go2rtc binary:&lt;br /&gt;
* https://github.com/tihmstar/vacuumstreamer&lt;br /&gt;
* https://github.com/AlexxIT/go2rtc&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ## on the robot, make /data/vacuumstramer&lt;br /&gt;
robot# mkdir /data/vacuumstreamer&lt;br /&gt;
&lt;br /&gt;
## on your computer, download vacuumstreamer packages and the go2rtc binary and copy it to the robot&lt;br /&gt;
computer# git clone https://github.com/tihmstar/vacuumstreamer&lt;br /&gt;
computer# scp vacuumstreamer/vacuumstreamer.so                root@vacuum:/data/vacuumstreamer/&lt;br /&gt;
computer# scp vacuumstreamer/dist/usr/bin/video_monitor       root@vacuum:/data/vacuumstreamer/&lt;br /&gt;
computer# scp -r vacuumstreamer/dist/ava/conf/video_monitor   root@vacuum:/data/vacuumstreamer/ava_conf_video_monitor&lt;br /&gt;
&lt;br /&gt;
computer# wget https://github.com/AlexxIT/go2rtc/releases/download/v1.9.13/go2rtc_linux_arm64&lt;br /&gt;
computer# scp go2rtc_linux_arm64  root@vacuum:/data/vacuumstreamer/go2rtc&lt;br /&gt;
&lt;br /&gt;
## back on the robot, set up the streamer&lt;br /&gt;
robot# cp -r /mnt/private /data/vacuumstreamer/mnt_private_copy &lt;br /&gt;
robot# touch /data/vacuumstreamer/mnt_private_copy/certificate.bin  # workaround for missing certificate bug, see https://github.com/tihmstar/vacuumstreamer/issues/1 for details&lt;br /&gt;
robot# cat &amp;lt;&amp;lt;EOF &amp;gt;&amp;gt; /data/_root_postboot.sh&lt;br /&gt;
&lt;br /&gt;
if [[ -f /data/vacuumstreamer/video_monitor ]]; then&lt;br /&gt;
    mount --bind /data/vacuumstreamer/ava_conf_video_monitor /ava/conf/video_monitor&lt;br /&gt;
    mount --bind /data/vacuumstreamer/mnt_private_copy /mnt/private&lt;br /&gt;
    LD_PRELOAD=/data/vacuumstreamer/vacuumstreamer.so /data/vacuumstreamer/video_monitor &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&lt;br /&gt;
    /data/vacuumstreamer/go2rtc -c &#039;{&amp;quot;streams&amp;quot;: {&amp;quot;tcp_magic&amp;quot;: &amp;quot;tcp://127.0.0.1:6969&amp;quot;}}&#039; &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&lt;br /&gt;
fi&lt;br /&gt;
EOF&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Reboot. You should see the video stream at http://robot:1984.&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Dreame_L40_Ultra&amp;diff=7719</id>
		<title>Dreame L40 Ultra</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Dreame_L40_Ultra&amp;diff=7719"/>
		<updated>2025-12-23T23:05:36Z</updated>

		<summary type="html">&lt;p&gt;Leo: Initial content&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The Dreame L40 Ultra is a robot vacuum and mop cleaner. I purchased this off Amazon in early November 2025 for about $580 CAD.&lt;br /&gt;
&lt;br /&gt;
== Rooting and installing Valetudo ==&lt;br /&gt;
You can root and install Valetudo on the Dreame L40 Ultra without needing to disassemble the vacuum cleaner by using the exposed debug header pins. The steps on how to do this is documented on Valetudo&#039;s installation page (https://valetudo.cloud/pages/installation/dreame.html) and ideally requires the Dreame breakout PCB because you will need to use the FEL / fastboot installation method for this model. The installation instructions is somewhat scattered across multiple different pages and websites and I am documenting everything here for this specific model so it&#039;s all together.&lt;br /&gt;
&lt;br /&gt;
=== Prerequsites ===&lt;br /&gt;
Things you need to root and install Valetudo:&lt;br /&gt;
&lt;br /&gt;
* Dreame Breakout PCB - https://github.com/Hypfer/valetudo-dreameadapter&lt;br /&gt;
** You can order the PCB from JLCPCB and it&#039;ll come in about 2 weeks (~$7 CAD).&lt;br /&gt;
** You will need to make sure you have 2mm headers ($2 CAD), some 2.4mm female headers ($1 CAD), a DIP button, a vertical micro USB header that fits the PCB layout ($2 CAD for 10), and a USB 2.0 type A female port ($2 CAD for 20, optional for this device/installation process).&lt;br /&gt;
* A 3.3v USB serial adapter and some dupont wires (optional, but is nice to have if you want to connect to the robot via serial)&lt;br /&gt;
* A computer running Linux (Ubuntu or Debian is preferable because we need to run Hypfer&#039;s fork of the LiveSuit program which was targeted for those distros). Ideally, this computer shouldn&#039;t be used for something important because you&#039;ll need to download/install a kernel module (which I don&#039;t really know where the source is from)&lt;br /&gt;
** Install Livesuit (https://github.com/Hypfer/valetudo-sunxi-livesuit). Follow the instructions in the README&lt;br /&gt;
* A rooted FEL firmware from https://builder.dontvacuum.me/_dreame_r2492.html (more on this in the instructions below).&lt;br /&gt;
** You will need to use the dustbuilder tool to generate FEL image that allows us to run the robot without secure boot. The tools to do this doesn&#039;t seem to be fully open source, possibly because Dennis (the author) wants to delay Dreame from patching this issue. &lt;br /&gt;
&lt;br /&gt;
=== Installation ===&lt;br /&gt;
&lt;br /&gt;
# Pry off the front of the robot to expose the debug headers. Your Dreame Breakout PCB with the 2mm headers should fit and the top of the breakout board should be facing the LIDAR.&lt;br /&gt;
# Download https://builder.dontvacuum.me/nextgen/dust-livesuit-mr813-ddr4.img and then run LiveSuit. Set the image to dust-livesuit-mr813-ddr4.img&lt;br /&gt;
# Factory reset the vacuum cleaner by pressing the reset button next to the wifi light for 10 seconds (it&#039;s next to the dust compartment). Once it comes back up, press and hold the power button until the vacuum is off&lt;br /&gt;
# While pressing the boot sel button the breakout PCB, press and hold power button on the vacuum cleaner for 5 seconds until the vacuum indicator lights are flashing. You can release the boot sel button once this happens.&lt;br /&gt;
# Plug a USB micro cable from your computer to the breakout port&lt;br /&gt;
# LiveSuit will ask to format the partition. Press No.&lt;br /&gt;
# In a terminal, you should be able to run: {{Highlight&lt;br /&gt;
| code = # fastboot devices&lt;br /&gt;
# fastboot getvar dustversion&lt;br /&gt;
# fastboot getvar config&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Go to the dustbuilder page (https://builder.dontvacuum.me/_dreame_r2492.html) and enter your device serial number (the QR code next to the dust compartment or the serial number under the dust compartment), the config value from the command above, and ensure you check off &#039;Create FEL image&#039;. The dustbuilder took about 20 minutes for me before it generated a FEL image I can download. While that&#039;s being generated, we can continue...&lt;br /&gt;
# Backup the boot stages in case we need it. Keep the bin files somewhere safe. They should all be approximately 400MB.{{Highlight&lt;br /&gt;
| code = # fastboot get_staged dustx100.bin&lt;br /&gt;
# fastboot oem stage1&lt;br /&gt;
# fastboot get_staged dustx101.bin&lt;br /&gt;
# fastboot oem stage2&lt;br /&gt;
# fastboot get_staged dustx102.bin&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Download the firmware image from the dustbuilder. You should have received a dreame.vacuum.r2492_1680_fel.zip file in the package. Extract the contents.&lt;br /&gt;
# Open LiveSuit and set the image to the _dreame.vacuum.r2492_phoenixsuit.img image file.&lt;br /&gt;
# Make a note of the contents in check.txt. You need this ready for the next step.&lt;br /&gt;
# Reboot the vacuum cleaner and re-enter fastboot again. We do this because there is a 160 second watchdog that reboots the robot and we want as much time as we can get to avoid having it reboot on us before we&#039;re done.&lt;br /&gt;
# Run: {{Highlight&lt;br /&gt;
| code = # fastboot getvar config   ## Confirm fastboot works&lt;br /&gt;
# fastboot oem dust xxxxxxx   ## Replace xxxx with the contents in check.txt&lt;br /&gt;
# fastboot oem prep   ## If this fails, don&#039;t proceed. Ensure LiveSuit has the correct img file set&lt;br /&gt;
## Ensure that everything below can run before the watchdog resets the device. If you took too long above, reset and try again.&lt;br /&gt;
# fastboot flash toc1 toc1.img   ## Should return OKAY. Stop otherwise and re-assess.&lt;br /&gt;
# fastboot flash boot1 boot.img&lt;br /&gt;
# fastboot flash rootfs1 rootfs.img&lt;br /&gt;
# fastboot flash boot2 boot.img&lt;br /&gt;
# fastboot flash rootfs2 rootfs.img&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Once all the flash commands succeed, you can run &amp;lt;code&amp;gt;fastboot reboot&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Press the outer buttons together for 3 seconds so that the robot starts its WiFi access point. Connect your computer to the robot&#039;s WiFi network.&lt;br /&gt;
# Confirm that you can SSH to the robot at root@192.168.5.1 using the SSH key that you selected (or were provided) in dustbuilder. If you don&#039;t have a working key, you can always connect to the robot via serial (there&#039;s a root shell running there)&lt;br /&gt;
# Backup all the calibration data from the robot. {{Highlight&lt;br /&gt;
| code = robot# tar -czf /tmp/calibration.tar.gz /mnt&lt;br /&gt;
&lt;br /&gt;
## on your computer, scp it out, or if dropbear utils aren&#039;t available, you can just do:&lt;br /&gt;
computer# ssh -i *rsa root@192.168.5.1 cat /tmp/calibration.tar.gz &amp;gt; calibration.tar.gz&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Download Valetudo&#039;s latest release from https://github.com/Hypfer/Valetudo and copy it into the robot to &amp;lt;code&amp;gt;/data/valetudo&amp;lt;/code&amp;gt;. {{Highlight&lt;br /&gt;
| code = robot# scp leo@computer:~/valetudo /data/valetudo&lt;br /&gt;
robot# chmod +x /data/valetudo&lt;br /&gt;
robot# cp /misc/_root_postboot.sh.tpl /data/_root_postboot.sh&lt;br /&gt;
robot# chmod +x /data/_root_postboot.sh&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Hire_Me&amp;diff=7718</id>
		<title>Hire Me</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Hire_Me&amp;diff=7718"/>
		<updated>2025-12-06T06:39:26Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Hello!&lt;br /&gt;
&lt;br /&gt;
I am a senior Linux administrator with {{#expr: floor( {{CURRENTYEAR}} - 2006) }}+ years of experience managing Linux systems including HPC, cloud, and on‑prem Linux desktop and server infrastructure. My recent experience has proven my expertise in automating large‑scale environments to support scientific research, teaching &amp;amp; learning, and enterprise workloads, as well as custom solutions for metrics gathering and report generation. I am adept at translating business needs into reliable, scalable solutions while mentoring teams and driving process improvements.&lt;br /&gt;
&lt;br /&gt;
== Core competencies ==&lt;br /&gt;
* &#039;&#039;&#039;Operating systems&#039;&#039;&#039;: {{#expr: floor( {{CURRENTYEAR}} - 2006) }}+ years of server experience with Red Hat-based OSes (CentOS, Rocky Linux, Fedora), Debian, Ubuntu. Experience with Solaris and FreeBSD&lt;br /&gt;
* &#039;&#039;&#039;Automation &amp;amp; configuration&#039;&#039;&#039;: Ansible, Terraform, Packer, SaltStack, Puppet, GitLab CI/CD&lt;br /&gt;
* &#039;&#039;&#039;Container Platforms&#039;&#039;&#039;: Docker, Kubernetes, Apptainer&lt;br /&gt;
* &#039;&#039;&#039;Cloud &amp;amp; Virtualization&#039;&#039;&#039;: AWS, OCI, VMware vSphere/Automation, Proxmox, CloudStack&lt;br /&gt;
* &#039;&#039;&#039;Programming &amp;amp; Scripting&#039;&#039;&#039;: Bash, PowerShell, Python, Java, C#, PHP, C&lt;br /&gt;
* &#039;&#039;&#039;Databases&#039;&#039;&#039;: MySQL, MariaDB, PostgreSQL, SQLite, InfluxDB / Flux, ElasticSearch&lt;br /&gt;
* &#039;&#039;&#039;Monitoring&#039;&#039;&#039;: Grafana, InfluxDB, Telegraf, ELK stack&lt;br /&gt;
* &#039;&#039;&#039;Networking&#039;&#039;&#039;: HP and Cisco switches, software firewalls, NAS systems&lt;br /&gt;
* &#039;&#039;&#039;Security&#039;&#039;&#039;: Kerberos, LDAP, PAM, PKI, log‑based threat hunting, network packet analysis&lt;br /&gt;
* &#039;&#039;&#039;Hardware Platforms&#039;&#039;&#039;: x86_64, ARM64, Sun, Dell, HP, IBM Tape Library (TS4500)&lt;br /&gt;
&lt;br /&gt;
== Professional experience ==&lt;br /&gt;
=== HPC System Administrator - University of Calgary (December 2019 - Present) ===&lt;br /&gt;
One of three lead administrators of HPC resources at Research Computing Services (RCS). Tasked with the successful transition of the 1PB+ GPFS/Tape storage system at the Centre for Health Genomics and Informatics (CHGI) at the Cumming School of Medicine (CSM) into RCS&#039;s service offering. Streamlined over 1000 HPC cluster nodes to Ansible, automated account vending, and implemented a robust metrics collection and reporting system.&lt;br /&gt;
&lt;br /&gt;
Major accomplishments include:&lt;br /&gt;
* Transitioned our HPC cluster of over 1000+ nodes to Ansible and automated configuration deployments and leveraging CI/CD pipelines to automate account provisioning and systems configuration.&lt;br /&gt;
* Automated HPC cluster usage report generation from various sources (hardware sensors, servers, network gear) into InfluxDB and visualize CPU, memory, storage usage by users, department, and faculty with Grafana to drive proactive capacity planning.&lt;br /&gt;
* Developed custom log-parsing scripts to ingest Slurm jobs, system metrics, and audit logs for reports, alerting, and governance reporting.&lt;br /&gt;
* Created RCS&#039;s Open OnDemand portal and containerized scientific apps, improving user productivity and easing the learning curve for users new to HPC.&lt;br /&gt;
* Implemented and maintained CloudStack as a private cloud solution and authored training materials and one-on-one workshops with researchers.&lt;br /&gt;
* Facilitated researchers into RCS managed AWS Control Tower environment while ensuring institutional security compliance and governance&lt;br /&gt;
&lt;br /&gt;
=== Infrastructure Automation &amp;amp; Containers Specialist - University of Calgary (December 2018 - December 2019) ===&lt;br /&gt;
Part of a newly created team tasked with spearheading the University&#039;s first internal Kubernetes as a service platform and VMware vRealize Automation pipeline for delivering virtual machines for secure research workloads.&lt;br /&gt;
&lt;br /&gt;
Achievements include:&lt;br /&gt;
* Ensured the smooth operation of 8 CoreOS Tectonic Kubernetes clusters while coordinating internal web team’s transition into our in-house Kubernetes infrastructure&lt;br /&gt;
* Evaluated Pivotal Enterprise PKS and Red Hat OpenShift as a new managed Kubernetes solution&lt;br /&gt;
* Built and migrated custom applications into a containerized environment using Helm charts&lt;br /&gt;
* Automated VM deployments on the University&#039;s private cloud with VMware vRealize, enabling zero-touch VM provisioning from ServiceNow and integration with Red Hat Satellite&lt;br /&gt;
* Authored change-management documentation compliant with ITIL and IT policies, ensuring audit readiness&lt;br /&gt;
&lt;br /&gt;
=== Technical Systems Analyst for Linux Desktop - University of Calgary (July 2018 - November 2018) ===&lt;br /&gt;
I spearheaded the standardized Linux desktop environment for the Customer Technology Services (CTS) group at the University of Calgary. Building on my experience from the Computer Science department, I transitioned and unified disparate Linux desktop infrastructures across various departments into a single standardized solution managed by central IT. &lt;br /&gt;
* Deployed Foreman, an open-source alternative to Red Hat Satellite, for unattended Linux deployments supporting Fedora &amp;amp; Kali Linux on both Intel and ARM based computers. Used by over 250+ Linux workstations for the Department of Computer Science&lt;br /&gt;
* Deployed infrastructure as code and CI/CD pipeline based on Puppet and GitLab for automated configuration management&lt;br /&gt;
* Created and updated custom Linux based packages used by teaching and research&lt;br /&gt;
* Consulted with faculty members to ensure software requirements required for teaching are met&lt;br /&gt;
* Authored support materials for technical staff in Computer Science&lt;br /&gt;
&lt;br /&gt;
=== Linux System Administrator - University of Calgary (October 2012 - July 2018) ===&lt;br /&gt;
I was a Linux system administrator that oversaw over 400 Linux-based servers and workstations for the department of Computer Science at the University of Calgary. I ensured that all teaching and learning needs were met with a consistent and reliable computing environment while providing end-user support for faculty, staff, and students within the department.&lt;br /&gt;
&lt;br /&gt;
* Spearheaded a rewrite of the departmental firewall and intrusion detection system. Firewall rules were parsed and pattern matching was deployed to block malicious traffic patterns from reaching the department network&lt;br /&gt;
* Modernized over 400 machines from an aging CFEngine configuration management system to SaltStack, simplifying systems provisioning and account management&lt;br /&gt;
* Enabled visibility into lab usage with custom machine telemetry data, integrated automatic data polling of student data and courses from the IT data warehouse, and data dashboards&lt;br /&gt;
* Deployed Logstash + ElasticSearch for internal logging of both Windows and Linux servers and workstations&lt;br /&gt;
* Converted applications on dedicated servers into containerized solutions, reducing server count by half and ensuring new teaching and research apps are supported, including WebCAT, OwnCloud, Moodle.&lt;br /&gt;
* Authored support documentation and end-user guides with a focus on undergraduate student experience&lt;br /&gt;
&lt;br /&gt;
=== C# Developer - Onstream Pipeline Inspection Ltd. (2011 - 2012) ===&lt;br /&gt;
I was one of two C# developers at a pipeline inspection company, developing their new version of pipeline analysis software.&lt;br /&gt;
* Debugged existing in-house software written in Borland C++ on Windows while working with low-level hardware interfaces with custom in-house pigs&lt;br /&gt;
* Developed analysis software using C# .NET4 to visualize pipeline data&lt;br /&gt;
&lt;br /&gt;
=== Freelance Web Developer &amp;amp; Designer – Steamr (2005 - 2015) ===&lt;br /&gt;
I was a freelance web designer and developer for over 20 clients across North America with custom web designs and applications. I was responsible for ensuring projects are completed on-time, on-budget and satisfies client requirements. &lt;br /&gt;
* Consulted with clients on requirements, budget, and timelines. Delivered frequent deliverables and project updates to ensure transparency&lt;br /&gt;
* Created and deployed custom web designs from scratch using Adobe Photoshop, Illustrator, Notepad++ while ensuring full compatibility across half a dozen web browsers&lt;br /&gt;
=== Server Administrator - SwiftHost.net (2004 - 2012) ===&lt;br /&gt;
Operated a web hosting company with a partner with a focus on affordable shared and reseller hosting. Servers utilized were based on the LAMP stack along with the use of the cPanel control panel. &lt;br /&gt;
* Managed production servers hosting over 400 individual websites&lt;br /&gt;
* Responsible for responding to customer support tickets and provided technical support for a wide range of web applications&lt;br /&gt;
* Performed security audits and server hardening, firewall and intrusion detection in a shared hosting environment&lt;br /&gt;
=== AS/400 Night Operator - Calgary Programming Factory (2007) ===&lt;br /&gt;
I worked as night operator as part of the AS/400 support team at the Calgary Programming Factory for The Sovereign General Insurance Company.&lt;br /&gt;
* Operated the tape backup system, ensuring backups are valid and without errors.&lt;br /&gt;
* Operated the AS/400 system to print system logs and claim cheques&lt;br /&gt;
* Performed a feasibility study on moving existing projects based on JSP servlets to ASP.NET&lt;br /&gt;
&lt;br /&gt;
== Education &amp;amp; Training ==&lt;br /&gt;
* Bachelor of Science in Computer Science, University of Calgary, 2011&lt;br /&gt;
** Notable courses: CPSC 550; System Administration with Darcy Grant, Wayne Pearson (2010 – 2011)&lt;br /&gt;
* Red Hat Satellite 6 Training&lt;br /&gt;
* VMware vSphere 6.7&lt;br /&gt;
* VMware vRealize Automation 6.5&lt;br /&gt;
&lt;br /&gt;
== Personal ==&lt;br /&gt;
* &#039;&#039;&#039;Languages&#039;&#039;&#039;: English, Cantonese&lt;br /&gt;
* &#039;&#039;&#039;Interests&#039;&#039;&#039;: system infrastructure, systems security, game development, electronics &amp;amp; hardware tinkering&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Hire_Me&amp;diff=7717</id>
		<title>Hire Me</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Hire_Me&amp;diff=7717"/>
		<updated>2025-12-06T06:37:28Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Hello!&lt;br /&gt;
&lt;br /&gt;
I am a senior Linux administrator with {{#expr: floor( {{CURRENTYEAR}} - 2006) }}+ years of experience managing Linux systems including HPC, cloud, and on‑prem Linux desktop and server infrastructure. My recent experience has proven my expertise in automating large‑scale environments to support scientific research, teaching &amp;amp; learning, and enterprise workloads, as well as custom solutions for metrics gathering and report generation. I am adept at translating business needs into reliable, scalable solutions while mentoring teams and driving process improvements.&lt;br /&gt;
&lt;br /&gt;
== Core competencies ==&lt;br /&gt;
* &#039;&#039;&#039;Operating systems&#039;&#039;&#039;: {{#expr: floor( {{CURRENTYEAR}} - 2006) }}+ years of server experience with Red Hat-based OSes (CentOS, Rocky Linux, Fedora), Debian, Ubuntu. Experience with Solaris and FreeBSD&lt;br /&gt;
* &#039;&#039;&#039;Automation &amp;amp; configuration&#039;&#039;&#039;: Ansible, Terraform, Packer, SaltStack, Puppet, GitLab CI/CD&lt;br /&gt;
* &#039;&#039;&#039;Container Platforms&#039;&#039;&#039;: Docker, Kubernetes, Apptainer&lt;br /&gt;
* &#039;&#039;&#039;Cloud &amp;amp; Virtualization&#039;&#039;&#039;: AWS, OCI, VMware vSphere/Automation, Proxmox, CloudStack&lt;br /&gt;
* &#039;&#039;&#039;Programming &amp;amp; Scripting&#039;&#039;&#039;: Bash, PowerShell, Python, Java, C#, PHP, C/C++&lt;br /&gt;
* &#039;&#039;&#039;Databases&#039;&#039;&#039;: MySQL, MariaDB, PostgreSQL, SQLite, InfluxDB / Flux&lt;br /&gt;
* &#039;&#039;&#039;Monitoring&#039;&#039;&#039;: Grafana, InfluxDB, Telegraf, ELK stack&lt;br /&gt;
* &#039;&#039;&#039;Networking&#039;&#039;&#039;: HP and Cisco switches, software firewalls, NAS systems&lt;br /&gt;
* &#039;&#039;&#039;Security&#039;&#039;&#039;: Kerberos, LDAP, PAM, PKI, log‑based threat hunting, network packet analysis&lt;br /&gt;
* &#039;&#039;&#039;Hardware Platforms&#039;&#039;&#039;: x86_64, ARM64, Sun, Dell, HP, IBM Tape Library (TS4500)&lt;br /&gt;
&lt;br /&gt;
== Professional experience ==&lt;br /&gt;
=== HPC System Administrator - University of Calgary (December 2019 - Present) ===&lt;br /&gt;
One of three lead administrators of HPC resources at Research Computing Services (RCS). Tasked with the successful transition of the 1PB+ GPFS/Tape storage system at the Centre for Health Genomics and Informatics (CHGI) at the Cumming School of Medicine (CSM) into RCS&#039;s service offering. Streamlined over 1000 HPC cluster nodes to Ansible, automated account vending, and implemented a robust metrics collection and reporting system.&lt;br /&gt;
&lt;br /&gt;
Major accomplishments include:&lt;br /&gt;
* Transitioned our HPC cluster of over 1000+ nodes to Ansible and automated configuration deployments and leveraging CI/CD pipelines to automate account provisioning and systems configuration.&lt;br /&gt;
* Automated HPC cluster usage report generation from various sources (hardware sensors, servers, network gear) into InfluxDB and visualize CPU, memory, storage usage by users, department, and faculty with Grafana to drive proactive capacity planning.&lt;br /&gt;
* Developed custom log-parsing scripts to ingest Slurm jobs, system metrics, and audit logs for reports, alerting, and governance reporting.&lt;br /&gt;
* Created RCS&#039;s Open OnDemand portal and containerized scientific apps, improving user productivity and easing the learning curve for users new to HPC.&lt;br /&gt;
* Implemented and maintained CloudStack as a private cloud solution and authored training materials and one-on-one workshops with researchers.&lt;br /&gt;
* Facilitated researchers into RCS managed AWS Control Tower environment while ensuring institutional security compliance and governance&lt;br /&gt;
&lt;br /&gt;
=== Infrastructure Automation &amp;amp; Containers Specialist - University of Calgary (December 2018 - December 2019) ===&lt;br /&gt;
Part of a newly created team tasked with spearheading the University&#039;s first internal Kubernetes as a service platform and VMware vRealize Automation pipeline for delivering virtual machines for secure research workloads.&lt;br /&gt;
&lt;br /&gt;
Achievements include:&lt;br /&gt;
* Ensured the smooth operation of 8 CoreOS Tectonic Kubernetes clusters while coordinating internal web team’s transition into our in-house Kubernetes infrastructure&lt;br /&gt;
* Evaluated Pivotal Enterprise PKS and Red Hat OpenShift as a new managed Kubernetes solution&lt;br /&gt;
* Built and migrated custom applications into a containerized environment using Helm charts&lt;br /&gt;
* Automated VM deployments on the University&#039;s private cloud with VMware vRealize, enabling zero-touch VM provisioning from ServiceNow and integration with Red Hat Satellite&lt;br /&gt;
* Authored change-management documentation compliant with ITIL and IT policies, ensuring audit readiness&lt;br /&gt;
&lt;br /&gt;
=== Technical Systems Analyst for Linux Desktop - University of Calgary (July 2018 - November 2018) ===&lt;br /&gt;
I spearheaded the standardized Linux desktop environment for the Customer Technology Services (CTS) group at the University of Calgary. Building on my experience from the Computer Science department, I transitioned and unified disparate Linux desktop infrastructures across various departments into a single standardized solution managed by central IT. &lt;br /&gt;
* Deployed Foreman, an open-source alternative to Red Hat Satellite, for unattended Linux deployments supporting Fedora &amp;amp; Kali Linux on both Intel and ARM based computers. Used by over 250+ Linux workstations for the Department of Computer Science&lt;br /&gt;
* Deployed infrastructure as code and CI/CD pipeline based on Puppet and GitLab for automated configuration management&lt;br /&gt;
* Created and updated custom Linux based packages used by teaching and research&lt;br /&gt;
* Consulted with faculty members to ensure software requirements required for teaching are met&lt;br /&gt;
* Authored support materials for technical staff in Computer Science&lt;br /&gt;
&lt;br /&gt;
=== Linux System Administrator - University of Calgary (October 2012 - July 2018) ===&lt;br /&gt;
I was a Linux system administrator that oversaw over 400 Linux-based servers and workstations for the department of Computer Science at the University of Calgary. I ensured that all teaching and learning needs were met with a consistent and reliable computing environment while providing end-user support for faculty, staff, and students within the department.&lt;br /&gt;
&lt;br /&gt;
* Spearheaded a rewrite of the departmental firewall and intrusion detection system. Firewall rules were parsed and pattern matching was deployed to block malicious traffic patterns from reaching the department network&lt;br /&gt;
* Modernized over 400 machines from an aging CFEngine configuration management system to SaltStack, simplifying systems provisioning and account management&lt;br /&gt;
* Enabled visibility into lab usage with custom machine telemetry data, integrated automatic data polling of student data and courses from the IT data warehouse, and data dashboards&lt;br /&gt;
* Deployed Logstash + ElasticSearch for internal logging of both Windows and Linux servers and workstations&lt;br /&gt;
* Converted applications on dedicated servers into containerized solutions, reducing server count by half and ensuring new teaching and research apps are supported, including WebCAT, OwnCloud, Moodle.&lt;br /&gt;
* Authored support documentation and end-user guides with a focus on undergraduate student experience&lt;br /&gt;
&lt;br /&gt;
=== C# Developer - Onstream Pipeline Inspection Ltd. (2011 - 2012) ===&lt;br /&gt;
I was one of two C# developers at a pipeline inspection company, developing their new version of pipeline analysis software.&lt;br /&gt;
* Debugged existing in-house software written in Borland C++ on Windows while working with low-level hardware interfaces with custom in-house pigs&lt;br /&gt;
* Developed analysis software using C# .NET4 to visualize pipeline data&lt;br /&gt;
&lt;br /&gt;
=== Freelance Web Developer &amp;amp; Designer – Steamr (2005 - 2015) ===&lt;br /&gt;
I was a freelance web designer and developer for over 20 clients across North America with custom web designs and applications. I was responsible for ensuring projects are completed on-time, on-budget and satisfies client requirements. &lt;br /&gt;
* Consulted with clients on requirements, budget, and timelines. Delivered frequent deliverables and project updates to ensure transparency&lt;br /&gt;
* Created and deployed custom web designs from scratch using Adobe Photoshop, Illustrator, Notepad++ while ensuring full compatibility across half a dozen web browsers&lt;br /&gt;
=== Server Administrator - SwiftHost.net (2004 - 2012) ===&lt;br /&gt;
Operated a web hosting company with a partner with a focus on affordable shared and reseller hosting. Servers utilized were based on the LAMP stack along with the use of the cPanel control panel. &lt;br /&gt;
* Managed production servers hosting over 400 individual websites&lt;br /&gt;
* Responsible for responding to customer support tickets and provided technical support for a wide range of web applications&lt;br /&gt;
* Performed security audits and server hardening, firewall and intrusion detection in a shared hosting environment&lt;br /&gt;
=== AS/400 Night Operator - Calgary Programming Factory (2007) ===&lt;br /&gt;
I worked as night operator as part of the AS/400 support team at the Calgary Programming Factory for The Sovereign General Insurance Company.&lt;br /&gt;
* Operated the tape backup system, ensuring backups are valid and without errors.&lt;br /&gt;
* Operated the AS/400 system to print system logs and claim cheques&lt;br /&gt;
* Performed a feasibility study on moving existing projects based on JSP servlets to ASP.NET&lt;br /&gt;
&lt;br /&gt;
== Education &amp;amp; Training ==&lt;br /&gt;
* Bachelor of Science in Computer Science, University of Calgary, 2011&lt;br /&gt;
** Notable courses: CPSC 550; System Administration with Darcy Grant, Wayne Pearson (2010 – 2011)&lt;br /&gt;
* Red Hat Satellite 6 Training&lt;br /&gt;
* VMware vSphere 6.7&lt;br /&gt;
* VMware vRealize Automation 6.5&lt;br /&gt;
&lt;br /&gt;
== Personal ==&lt;br /&gt;
* &#039;&#039;&#039;Languages&#039;&#039;&#039;: English, Cantonese&lt;br /&gt;
* &#039;&#039;&#039;Interests&#039;&#039;&#039;: system infrastructure, systems security, game development, electronics &amp;amp; hardware tinkering&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Hire_Me&amp;diff=7716</id>
		<title>Hire Me</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Hire_Me&amp;diff=7716"/>
		<updated>2025-12-06T05:42:37Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Hello!&lt;br /&gt;
&lt;br /&gt;
I am a senior Linux administrator with {{#expr: floor( {{CURRENTYEAR}} - 2006) }}+ years of experience managing Linux systems including HPC, cloud, and on‑prem Linux desktop and server infrastructure. My recent experience has proven my expertise in automating large‑scale environments to support scientific research, teaching &amp;amp; learning, and enterprise workloads, as well as custom solutions for metrics gathering and report generation. I am adept at translating business needs into reliable, scalable solutions while mentoring teams and driving process improvements.&lt;br /&gt;
&lt;br /&gt;
== Core competencies ==&lt;br /&gt;
* &#039;&#039;&#039;Operating systems&#039;&#039;&#039;: {{#expr: floor( {{CURRENTYEAR}} - 2006) }}+ years of server experience with Red Hat-based OSes (CentOS, Rocky Linux, Fedora), Debian, Ubuntu.  Past experience with Solaris and FreeBSD&lt;br /&gt;
* &#039;&#039;&#039;Automation &amp;amp; configuration&#039;&#039;&#039;: Ansible, Terraform, Packer, SaltStack, Puppet, GitLab CI/CD&lt;br /&gt;
* &#039;&#039;&#039;Container Platforms&#039;&#039;&#039;: Docker, Kubernetes, Apptainer&lt;br /&gt;
* &#039;&#039;&#039;Cloud &amp;amp; Virtualization&#039;&#039;&#039;: AWS, OCI, VMware vSphere/Automation, Proxmox, CloudStack&lt;br /&gt;
* &#039;&#039;&#039;Programming &amp;amp; Scripting&#039;&#039;&#039;: Bash, PowerShell, Python, Java, C#, PHP, C/C++&lt;br /&gt;
* &#039;&#039;&#039;Databases&#039;&#039;&#039;: MySQL, MariaDB, PostgreSQL, SQLite, InfluxDB / Flux&lt;br /&gt;
* &#039;&#039;&#039;Monitoring&#039;&#039;&#039;: Grafana, InfluxDB, Telegraf, ELK stack&lt;br /&gt;
* &#039;&#039;&#039;Networking&#039;&#039;&#039;: HP and Cisco switches, software firewalls, NAS systems&lt;br /&gt;
* &#039;&#039;&#039;Security&#039;&#039;&#039;: Kerberos, LDAP, PAM, PKI, log‑based threat hunting, network packet analysis&lt;br /&gt;
* &#039;&#039;&#039;Hardware Platforms&#039;&#039;&#039;: x86_64, ARM64, Sun, Dell, HP, IBM Tape Library (TS4500)&lt;br /&gt;
&lt;br /&gt;
== Professional experience ==&lt;br /&gt;
=== HPC System Administrator - University of Calgary (December 2019 - Decmber 2025) ===&lt;br /&gt;
One of three lead architect and operators for the HPC resources at Research Computing Services (RCS). Tasked with the successful transition of the 1PB+ storage system at the Centre for Health Genomics and Informatics (CHGI) at the Cumming School of Medicine (CSM) into RCS&#039;s service offering. Streamlined over 1000 HPC cluster nodes to Ansible, automated account vending, and implemented a robust metrics collection and reporting system.&lt;br /&gt;
&lt;br /&gt;
Major accomplishments include:&lt;br /&gt;
* Transitioned our HPC cluster of over 1000 nodes to Ansible and automated configuration deployments and leveraging CI/CD pipelines to automate account provisioning and systems configuration.&lt;br /&gt;
* Automated HPC cluster usage report generation from various sources (Eg. sensors, servers, network gear). Visualize CPU, memory, storage usage by users, department, and faculty which was used to drive proactive capacity planning. &lt;br /&gt;
* Developed custom log-parsing scripts to ingest Slurm jobs, system metrics, and audit logs for reports, alerting, and governance reporting.&lt;br /&gt;
* Created RCS&#039;s Open OnDemand portal and containerized scientific apps, improving user productivity and easing the learning curve for users new to HPC.&lt;br /&gt;
* Implemented and maintained CloudStack as a private cloud solution and authored training materials and one-on-one workshops with researchers.&lt;br /&gt;
* Facilitated researchers into RCS managed AWS Control Tower environment while ensuring instutional security compliance and governance&lt;br /&gt;
&lt;br /&gt;
=== Infrastructure Automation &amp;amp; Containers Specialist - University of Calgary (December 2018 - December 2019) ===&lt;br /&gt;
Part of a newly created team tasked with spearheading the University&#039;s first internal kubernetes as a service platform and VMware vRealize Automation pipeline for delivering virtual machines for secure research workloads.&lt;br /&gt;
&lt;br /&gt;
Achievements include:&lt;br /&gt;
* Ensured the smooth operating of around 8 CoreOS Tectonic Kubernetes clusters&lt;br /&gt;
* Evaluated Pivotal Enterprise PKS and Red Hat OpenShift as a new managed Kubernetes solution&lt;br /&gt;
* Coordinated with our internal web team to transition all of the University&#039;s websites onto our Kubernetes infrastructure&lt;br /&gt;
* Built and migrated custom applications into a containerized environment using Helm charts&lt;br /&gt;
* Automated VM deployments on the University&#039;s private cloud with VMware vRealize, enabling zero-touch VM provisioning from ServiceNow and integration with Red Hat Satellite&lt;br /&gt;
* Authoried change-management documentation compliant with ITIL and IT policies, ensuring audit readiness&lt;br /&gt;
&lt;br /&gt;
=== Technical Systems Analyst for Linux Desktop - University of Calgary (July 2018 - November 2018) ===&lt;br /&gt;
I spearheaded the standardized Linux desktop environment for the Customer Technology Services (CTS) group at the University of Calgary. Building on my past experience from the Computer Science department, I transitioned and unified disparate Linux desktop infrastructures across various departments into a single standardized solution managed by central IT. &lt;br /&gt;
* Deployed TheForeman, an open source alternative to Red Hat Satellite, for unattended Linux deployments supporting Fedora &amp;amp; Kali Linux on both Intel and ARM based computers. Used by over 250+ Linux workstations for the Department of Computer Science&lt;br /&gt;
* Wrote and integrated Puppet manifests with TheForeman for desired state configuration on all hosts&lt;br /&gt;
* Created and updated custom Linux based packages used by teaching and research&lt;br /&gt;
* Consulted with faculty members to ensure software requirements required for teaching are met&lt;br /&gt;
* Authored support materials on the new deployment system for technical staff in Computer Science&lt;br /&gt;
&lt;br /&gt;
=== Linux System Administrator - University of Calgary (October 2012 - July 2018) ===&lt;br /&gt;
I was a Linux system administrator that oversaw over 400 Linux-based servers and workstations for the department of Computer Science at the University of Calgary. I ensured that all teaching and learning needs were met with a consistent and reliable computing environment while providing end-user support for faculty, staff, and students within the department.&lt;br /&gt;
&lt;br /&gt;
* Spearheaded a rewrite of the departmental firewall and intrustion detection system. Firewall rules were parsed and pattern matching was deployed to block malicious traffic patterns from reaching the department network&lt;br /&gt;
* Modernized over 400 machines from an aging CFEngine configuration management system to SaltStack, simplifying systems provisioning and account management&lt;br /&gt;
* Enabled visibility into lab usage with custom machine telemetry data, integrated automatic data polling of student data and courses from the IT data warehouse, and data dashboards&lt;br /&gt;
* Deployed Logstash + ElasticSearch for internal logging of both Windows and Linux servers and workstations&lt;br /&gt;
* Converted applications on dedicated servers into containerized solutions, reducing server count by half and ensuring new teaching and research apps are supported, including WebCAT, OwnCloud, Moodle.&lt;br /&gt;
* Authored support documentation and end-user guides with a focus on undergraduate student experience&lt;br /&gt;
&lt;br /&gt;
=== C# Developer - Onstream Pipeline Inspection Ltd. (2011 - 2012) ===&lt;br /&gt;
I was one of two C# developers at a pipeline inspection company, developing their new version of pipeline analysis software.&lt;br /&gt;
* Debugged existing in-house software written in Borland C++ on the Windows platform&lt;br /&gt;
* Low-level hardware interface with custom designed pigs&lt;br /&gt;
* Developed the next version of their analysis software from scratch using C# .NET4&lt;br /&gt;
&lt;br /&gt;
=== Freelance Web Developer &amp;amp; Designer – Steamr (2005 - 2015) ===&lt;br /&gt;
I was a freelance web designer and developer for over 20 clients across North America with custom web designs and applications. I was responsible for ensuring projects are completed on-time, on-budget and satisfies client requirements. &lt;br /&gt;
* Consulted with clients on requirements, budget, and timelines. Delivered frequent deliverables and project updates to ensure transparency&lt;br /&gt;
* Created and deployed custom web designs from scratch using Adobe Photoshop, Illustrator, Notepad++ while ensuring full compatibility across half a dozen web browsers&lt;br /&gt;
=== Server Administrator - SwiftHost.net (2004 - 2012) ===&lt;br /&gt;
Operated a web hosting company with a partner with a focus on affordable shared and reseller hosting. Servers utilized were based on the LAMP stack along with the use of the cPanel control panel. &lt;br /&gt;
* Managed production servers hosting over 400 individual websites&lt;br /&gt;
* Responsible for responding to customer support tickets and provided technical support for a wide range of web applications&lt;br /&gt;
* Performed security audits and server hardening, firewall and intrusion detection in a shared hosting environment&lt;br /&gt;
=== AS/400 Night Operator - Calgary Programming Factory (2007) ===&lt;br /&gt;
I worked as night operator as part of the AS/400 support team at the Calgary Programming Factory for The Sovereign General Insurance Company as a summer job.&lt;br /&gt;
* Operated the tape backup system, ensuring backups are valid and without errors.&lt;br /&gt;
* Operated the AS/400 system to print system logs and claim checks&lt;br /&gt;
* Ensured paper was loaded and aligned properly to the printers before printing&lt;br /&gt;
* Researched feasibility to migrate their existing projects based on JSP servlets to ASP.NET&lt;br /&gt;
&lt;br /&gt;
== Education &amp;amp; Training ==&lt;br /&gt;
* Bachelor of Science in Computer Science, University of Calgary, 2011&lt;br /&gt;
** Notable courses: CPSC 550; System Administration with Darcy Grant, Wayne Pearson (2010 – 2011)&lt;br /&gt;
* Red Hat Satellite 6 Training&lt;br /&gt;
* VMware vSphere 6.7&lt;br /&gt;
* VMware vRealize Automation 6.5&lt;br /&gt;
&lt;br /&gt;
== Personal ==&lt;br /&gt;
* Languages: English, Cantonese&lt;br /&gt;
* Interests: system infrastructure, systems security, game development, electronics &amp;amp; hardware tinkering&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Hire_Me&amp;diff=7715</id>
		<title>Hire Me</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Hire_Me&amp;diff=7715"/>
		<updated>2025-12-06T01:50:31Z</updated>

		<summary type="html">&lt;p&gt;Leo: Simplify professional experience&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Hello!&lt;br /&gt;
&lt;br /&gt;
I am a senior Linux administrator with {{#expr: floor( {{CURRENTYEAR}} - 2006) }}+ years of experience managing Linux systems including HPC, cloud, and on‑prem Linux desktop and server infrastructure. My recent experience has proven my expertise in automating large‑scale environments to support scientific research, teaching &amp;amp; learning, and enterprise workloads, as well as custom solutions for metrics gathering and report generation. I am adept at translating business needs into reliable, scalable solutions while mentoring teams and driving process improvements.&lt;br /&gt;
&lt;br /&gt;
== Core competencies ==&lt;br /&gt;
* &#039;&#039;&#039;Operating systems&#039;&#039;&#039;: {{#expr: floor( {{CURRENTYEAR}} - 2006) }}+ years of server experience with Red Hat-based OSes (CentOS, Rocky Linux, Fedora), Debian, Ubuntu.  Past experience with Solaris and FreeBSD&lt;br /&gt;
* &#039;&#039;&#039;Automation &amp;amp; configuration&#039;&#039;&#039;: Ansible, Terraform, Packer, SaltStack, Puppet, GitLab CI/CD&lt;br /&gt;
* &#039;&#039;&#039;Container Platforms&#039;&#039;&#039;: Docker, Kubernetes, Apptainer&lt;br /&gt;
* &#039;&#039;&#039;Cloud &amp;amp; Virtualization&#039;&#039;&#039;: AWS, OCI, VMware vSphere/Automation, Proxmox, CloudStack&lt;br /&gt;
* &#039;&#039;&#039;Programming &amp;amp; Scripting&#039;&#039;&#039;: Bash, PowerShell, Python, Java, C#, PHP, C/C++&lt;br /&gt;
* &#039;&#039;&#039;Databases&#039;&#039;&#039;: MySQL, MariaDB, PostgreSQL, SQLite, InfluxDB / Flux&lt;br /&gt;
* &#039;&#039;&#039;Monitoring&#039;&#039;&#039;: Grafana, InfluxDB, Telegraf, ELK stack&lt;br /&gt;
* &#039;&#039;&#039;Networking&#039;&#039;&#039;: HP and Cisco switches, software firewalls, NAS systems&lt;br /&gt;
* &#039;&#039;&#039;Security&#039;&#039;&#039;: Kerberos, LDAP, PAM, PKI, log‑based threat hunting, network packet analysis&lt;br /&gt;
* &#039;&#039;&#039;Hardware Platforms&#039;&#039;&#039;: x86_64, ARM64, Sun, Dell, HP, IBM Tape Library (TS4500)&lt;br /&gt;
&lt;br /&gt;
=== Education &amp;amp; Training ===&lt;br /&gt;
* Bachelor of Science in Computer Science, University of Calgary&lt;br /&gt;
** August 2007 - May 2011&lt;br /&gt;
** Notable courses: CPSC 550; System Administration with Darcy Grant, Wayne Pearson (2010 – 2011)&lt;br /&gt;
* Red Hat Satellite 6 Training&lt;br /&gt;
** Completed August 2018&lt;br /&gt;
* VMware vSphere 6.7&lt;br /&gt;
** Completed January 2019&lt;br /&gt;
* VMware vRealize Automation 6.5&lt;br /&gt;
** Completed January 2019&lt;br /&gt;
&lt;br /&gt;
== Professional experience ==&lt;br /&gt;
=== HPC System Administrator - University of Calgary (December 2019 - Deecmber 2025) ===&lt;br /&gt;
One of three lead architect and operators for the HPC resources at Research Computing Services (RCS). Tasked with the successful transition of the 1PB+ storage system at the Centre for Health Genomics and Informatics (CHGI) at the Cumming School of Medicine (CSM) into RCS&#039;s service offering. Streamlined over 1000 HPC cluster nodes to Ansible, automated account vending, and implemented a robust metrics collection and reporting system.&lt;br /&gt;
&lt;br /&gt;
Major accomplishments include:&lt;br /&gt;
* Transitioned our HPC cluster of over 1000 nodes to Ansible and automated configuration deployments and leveraging CI/CD pipelines to automate account provisioning and systems configuration.&lt;br /&gt;
* Automated HPC cluster usage report generation from various sources (Eg. sensors, servers, network gear). Visualize CPU, memory, storage usage by users, department, and faculty which was used to drive proactive capacity planning. &lt;br /&gt;
* Developed custom log-parsing scripts to ingest Slurm jobs, system metrics, and audit logs for reports, alerting, and governance reporting.&lt;br /&gt;
* Created RCS&#039;s Open OnDemand portal and containerized scientific apps, improving user productivity and easing the learning curve for users new to HPC.&lt;br /&gt;
* Implemented and maintained CloudStack as a private cloud solution and authored training materials and one-on-one workshops with researchers.&lt;br /&gt;
* Facilitated researchers into RCS managed AWS Control Tower environment while ensuring instutional security compliance and governance&lt;br /&gt;
&lt;br /&gt;
=== Infrastructure Automation &amp;amp; Containers Specialist - University of Calgary (December 2018 - December 2019) ===&lt;br /&gt;
Part of a newly created team tasked with spearheading the University&#039;s first internal kubernetes as a service platform and VMware vRealize Automation pipeline for delivering virtual machines for secure research workloads.&lt;br /&gt;
&lt;br /&gt;
Achievements include:&lt;br /&gt;
* Ensured the smooth operating of around 8 CoreOS Tectonic Kubernetes clusters&lt;br /&gt;
* Evaluated Pivotal Enterprise PKS and Red Hat OpenShift as a new managed Kubernetes solution&lt;br /&gt;
* Coordinated with our internal web team to transition all of the University&#039;s websites onto our Kubernetes infrastructure&lt;br /&gt;
* Built and migrated custom applications into a containerized environment using Helm charts&lt;br /&gt;
* Automated VM deployments on the University&#039;s private cloud with VMware vRealize, enabling zero-touch VM provisioning from ServiceNow and integration with Red Hat Satellite&lt;br /&gt;
* Authoried change-management documentation compliant with ITIL and IT policies, ensuring audit readiness&lt;br /&gt;
&lt;br /&gt;
=== Technical Systems Analyst for Linux Desktop - University of Calgary (July 2018 - November 2018) ===&lt;br /&gt;
I spearheaded the standardized Linux desktop environment for the Customer Technology Services (CTS) group at the University of Calgary. Building on my past experience from the Computer Science department, I transitioned and unified disparate Linux desktop infrastructures across various departments into a single standardized solution managed by central IT. &lt;br /&gt;
* Deployed TheForeman, an open source alternative to Red Hat Satellite, for unattended Linux deployments supporting Fedora &amp;amp; Kali Linux on both Intel and ARM based computers. Used by over 250+ Linux workstations for the Department of Computer Science&lt;br /&gt;
* Wrote and integrated Puppet manifests with TheForeman for desired state configuration on all hosts&lt;br /&gt;
* Created and updated custom Linux based packages used by teaching and research&lt;br /&gt;
* Consulted with faculty members to ensure software requirements required for teaching are met&lt;br /&gt;
* Authored support materials on the new deployment system for technical staff in Computer Science&lt;br /&gt;
&lt;br /&gt;
=== Linux System Administrator - University of Calgary (October 2012 - July 2018) ===&lt;br /&gt;
I was a Linux system administrator that oversaw over 400 Linux-based servers and workstations for the department of Computer Science at the University of Calgary. I ensured that all teaching and learning needs were met with a consistent and reliable computing environment while providing end-user support for faculty, staff, and students within the department.&lt;br /&gt;
&lt;br /&gt;
* Spearheaded a rewrite of the departmental firewall and intrustion detection system. Firewall rules were parsed and pattern matching was deployed to block malicious traffic patterns from reaching the department network&lt;br /&gt;
* Modernized over 400 machines from an aging CFEngine configuration management system to SaltStack, simplifying systems provisioning and account management&lt;br /&gt;
* Enabled visibility into lab usage with custom machine telemetry data, integrated automatic data polling of student data and courses from the IT data warehouse, and data dashboards&lt;br /&gt;
* Deployed Logstash + ElasticSearch for internal logging of both Windows and Linux servers and workstations&lt;br /&gt;
* Converted applications on dedicated servers into containerized solutions, reducing server count by half and ensuring new teaching and research apps are supported, including WebCAT, OwnCloud, Moodle.&lt;br /&gt;
* Authored support documentation and end-user guides with a focus on undergraduate student experience&lt;br /&gt;
&lt;br /&gt;
=== C# Developer - Onstream Pipeline Inspection Ltd. ===&lt;br /&gt;
May 2011 – September 2013, Full Time, http://onstream-pipeline.com/ &lt;br /&gt;
&lt;br /&gt;
I was one of two C# developers at a pipeline inspection company, developing their new version of pipeline analysis software.&lt;br /&gt;
* Debugged existing in-house software written in Borland C++ on the Windows platform&lt;br /&gt;
* Low-level hardware interface with custom designed pigs&lt;br /&gt;
* Developed the next version of their analysis software from scratch using C# .NET4&lt;br /&gt;
&lt;br /&gt;
=== Freelance Web Developer &amp;amp; Designer – Steamr ===&lt;br /&gt;
June 2005 – 2014, Freelance, http://steamr.com/ &lt;br /&gt;
&lt;br /&gt;
I am a freelance web designer and developer and provided clients primarily in North America with custom web designs and applications. I am responsible for ensuring projects are completed on-time, on-budget and satisfies client requirements. &lt;br /&gt;
&lt;br /&gt;
A sample of my work can be found on my portfolio at: http://steamr.com/portfolio &lt;br /&gt;
&lt;br /&gt;
Responsibilities: &lt;br /&gt;
* Consulted with clients for every project for budget, timeline and user requirements and delivered frequent deliverables and project updates.&lt;br /&gt;
* Created and deployed custom web designs from scratch using Adobe Photoshop, Illustrator, Notepad++, and half a dozen web browsers. &lt;br /&gt;
* Ensured all websites were optimal, standards compliant, cross-browser compatible, and search engine optimized (SEO) based on web analytics.&lt;br /&gt;
* Created and maintained over 20 projects under the LAMP stack, along with using PHP frameworks and libraries including: Kohana, CodeIgniter, FPDF, Smarty.&lt;br /&gt;
* Created over 40 individual websites from scratch for businesses and individuals.&lt;br /&gt;
* Customized design themes for WHMCS, SolusVM, Kayako Support Suite for hosting companies.&lt;br /&gt;
	&lt;br /&gt;
Related Work:&lt;br /&gt;
* Previous webmaster for GlobalTech Communications including: nworks.ca, gtcomm.net, unmeteredserver.net, zenprotection.com&lt;br /&gt;
* Responsible for updating content and promotions while ensuring website correctly interfaces with the point of sale application.&lt;br /&gt;
* Developed an internal network management application for gtcomm.net in PHP utilizing MRTG. Abnormal network patterns will notify an administrator via email and SMS. &lt;br /&gt;
* Created a distributed network monitoring system based on Java, PHP and MySQL, which utilizes http and ping tests to determine the availability and latency of remote services. Results are then recorded and monthly reports generated.&lt;br /&gt;
* Designed the front-end of an in-house inventory system used by gtcomm.net used to track networking/server hardware along with managing available IP pools.&lt;br /&gt;
* Designed the front-end of an in-house project enabling end users to purchase and manage DDoS protection services by zenprotection.net.&lt;br /&gt;
&lt;br /&gt;
=== Server Administrator - SwiftHost.net ===&lt;br /&gt;
January 2004 – May 2012, http://swifthost.net/ &lt;br /&gt;
&lt;br /&gt;
With a partner, I created a web hosting company, focusing on affordable shared and reseller hosting. Servers utilized were based on the LAMP stack along with the use of the cPanel control panel. &lt;br /&gt;
&lt;br /&gt;
Responsibilities: &lt;br /&gt;
* Managed production servers hosting over 400 individual websites&lt;br /&gt;
* Responsible for responding to many customer support tickets within 6 hours for issues under the technical, abuse and billing department.&lt;br /&gt;
* Technical issues handled by me ranged from fixing simple permission issues to handling requests for specific software such as Apache/PHP modules, system libraries, and Caching / PHP Accelerators&lt;br /&gt;
* Performed security audits and server hardening by disabling or restricting services, configuring the firewall, detecting compromised websites and spam control in a shared hosting environment.&lt;br /&gt;
* Configured weekly, network-based backups to an off-site location using cronjobs, rsync and shell scripts. Performed account or file specific restores from backups.&lt;br /&gt;
	&lt;br /&gt;
Projects:&lt;br /&gt;
* Designed a custom billing solution for account management using PHP, MySQL and the PayPal API.&lt;br /&gt;
* A status page which monitored and reported live server status information such as processor load, traffic load, service availability, memory usage to customers.&lt;br /&gt;
* Developed a custom web control panel, similar to cPanel, targeted to free hosting accounts. The control panel based on PHP, MySQL, Shell + Perl Scripts, enabled end users to manage their websites, domains and databases while automatically maintaining and enforcing disk and bandwidth quotas.&lt;br /&gt;
&lt;br /&gt;
=== AS/400 Night Operator - Calgary Programming Factory ===&lt;br /&gt;
May 2007 – July 2007, Part Time&lt;br /&gt;
&lt;br /&gt;
I worked as night operator as part of the AS/400 support team at the Calgary Programming Factory for The Sovereign General Insurance Company as a summer job. &lt;br /&gt;
&lt;br /&gt;
Responsibilities:&lt;br /&gt;
* Operated the tape backup system, ensuring backups are valid and without errors.&lt;br /&gt;
* Operated the AS/400 system to print system logs and claim checks&lt;br /&gt;
* Ensured paper was loaded and aligned properly to the printers before printing&lt;br /&gt;
* Researched feasibility to migrate their existing projects based on JSP servlets to ASP.NET&lt;br /&gt;
&lt;br /&gt;
== Personal ==&lt;br /&gt;
* Canadian; Speaks English &amp;amp; Cantonese&lt;br /&gt;
* Interest in anything Linux related, System Infrastructure, Systems Security, Game Development, Electronics &amp;amp; Tinkering&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Hire_Me&amp;diff=7714</id>
		<title>Hire Me</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Hire_Me&amp;diff=7714"/>
		<updated>2025-12-05T07:17:05Z</updated>

		<summary type="html">&lt;p&gt;Leo: /* Relevant Skills */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This is my current resume / CV.&lt;br /&gt;
&lt;br /&gt;
== Objective ==&lt;br /&gt;
With my wide skillset in different technical areas, my goal is to come up with elegant and streamlined solutions that meet both user and IT business operation requirements. &lt;br /&gt;
&lt;br /&gt;
== Relevant Skills ==&lt;br /&gt;
&lt;br /&gt;
=== OPERATING SYSTEMS ===&lt;br /&gt;
* {{#expr: floor( {{CURRENTYEAR}} - 2006) }}+ years of server experience with Linux (Primarily Red Hat-based, Eg. CentOS, Rocky Linux, Fedora)&lt;br /&gt;
* {{#expr: floor( {{CURRENTYEAR}} - 2005) }}+ years of desktop support experience with Windows&lt;br /&gt;
* Experience with Solaris and FreeBSD&lt;br /&gt;
&lt;br /&gt;
=== NETWORKING ===&lt;br /&gt;
* Familiar with physical network maintenance, software firewalls, NAS appliances&lt;br /&gt;
* Experience with HP and Cisco switches&lt;br /&gt;
&lt;br /&gt;
=== SOFTWARE ===&lt;br /&gt;
* Containers: Docker, Kubernetes, Apptainer&lt;br /&gt;
* Configuration Management: Ansible, SaltStack, Puppet, Terraform, Packer&lt;br /&gt;
* Logs and metrics: Telegraf, InfluxDB, Grafana, EFK/ELK (ElasticSearch, Logstash/Fluentd, Kibana)&lt;br /&gt;
* Products: Red Hat Satellite, GitLab, CloudStack, Proxmox&lt;br /&gt;
&lt;br /&gt;
=== SECURITY ===&lt;br /&gt;
* Experience with Kerberos and LDAP authentication&lt;br /&gt;
* Can perform security analysis via log processing, packet analysis, tcpdump&lt;br /&gt;
&lt;br /&gt;
=== WEB ===&lt;br /&gt;
* {{#expr: floor( {{CURRENTYEAR}} - 2008) }}+ years configuring and web deploying applications&lt;br /&gt;
* Containerize web applications for Kubernetes&lt;br /&gt;
* Customize templates for MediaWiki, WordPress, Drupal&lt;br /&gt;
*Experience with Amazon Web Services (AWS), Oracle Cloud Infrastructure (OCI)&lt;br /&gt;
&lt;br /&gt;
=== PROGRAMMING ===&lt;br /&gt;
* Scripting: bash/sh/awk, Powershell&lt;br /&gt;
* Coding: Java, C#, PHP, Python, C&lt;br /&gt;
* Database: MySQL/MariaDB, SQLite, PostgreSQL, InfluxDB/flux&lt;br /&gt;
* Debug: C, C++, gdb, strace, Ghidra&lt;br /&gt;
&lt;br /&gt;
=== HARDWARE ===&lt;br /&gt;
* Experience with x86/x86_64/ARM64 Hardware&lt;br /&gt;
* Experience with server and workstation hardware from Sun, Dell, and HP&lt;br /&gt;
* Experience with IBM Tape Library (TS4500)&lt;br /&gt;
&lt;br /&gt;
== Education &amp;amp; Training ==&lt;br /&gt;
* Bachelor of Science in Computer Science, University of Calgary&lt;br /&gt;
** August 2007 - May 2011&lt;br /&gt;
** Notable courses: CPSC 550; System Administration with Darcy Grant, Wayne Pearson (2010 – 2011)&lt;br /&gt;
* Red Hat Satellite 6 Training&lt;br /&gt;
** Completed August 2018&lt;br /&gt;
* VMware vSphere 6.7&lt;br /&gt;
** Completed January 2019&lt;br /&gt;
* VMware vRealize Automation 6.5&lt;br /&gt;
** Completed January 2019&lt;br /&gt;
&lt;br /&gt;
== Work History ==&lt;br /&gt;
=== HPC System Administrator - University of Calgary ===&lt;br /&gt;
December 2019 - December 2025, Full Time, http://ucalgary.ca/hpc&lt;br /&gt;
&lt;br /&gt;
I maintained and transitioned infrastructure operated at the Centre for Health Genomics and Informatics (CHGI) at the Cumming School of Medicine (CSM) to the centralized IT infrastructure, operated by Research Computing Services (RCS). &lt;br /&gt;
&lt;br /&gt;
Major accomplishments include:&lt;br /&gt;
* Transitioned our HPC cluster of over 1000 nodes to Ansible and automated configuration deployments. Explored using Warewulf for node deployment.&lt;br /&gt;
* Automated HPC cluster usage report generation from various sources (Eg. sensors, servers, network gear) for senior leadership and institutional decision making&lt;br /&gt;
* Developed customized solutions for log parsing and processing for reports and alerts&lt;br /&gt;
* Implemented and maintained Open OnDemand and custom containerized apps as a friendlier interface to RCS&#039;s HPC resources&lt;br /&gt;
* Implemented and maintained CloudStack as a private cloud solution; written training material and provided training sessions for users&lt;br /&gt;
* Facilitated researchers into RCS managed AWS Control Tower environment while ensuring instutional security compliance and governance&lt;br /&gt;
&lt;br /&gt;
My day to day duties included:&lt;br /&gt;
* Operated, maintained, and troubleshooted HPC clusters running Slurm&lt;br /&gt;
* Collected cluster metrics and hardware performance data which are stored into InfluxDB and visualized with Grafana&lt;br /&gt;
* Provided researcher and lab support for HPC, Open OnDemand, storage, CloudStack requests and issues&lt;br /&gt;
* Gathered researcher requirements and ensure deployed services met their operational needs&lt;br /&gt;
* Automate and streamline operational processes with custom scripts and CI/CD pipelines&lt;br /&gt;
* Supported RCS&#039;s internal development team by providing guidance related to systems programming and development&lt;br /&gt;
&lt;br /&gt;
=== Infrastructure Automation &amp;amp; Containers Specialist - University of Calgary ===&lt;br /&gt;
December 2018 - December 2019, Full Time Limited Term, http://ucalgary.ca/it&lt;br /&gt;
&lt;br /&gt;
I was part of a newly created team of two tasked with creating a new infrastructure automation system using VMware vRealize Automation as well as creating a managed on-site Kubernetes as a service solution for the University of Calgary.&lt;br /&gt;
&lt;br /&gt;
Due to the early stage of both infrastructure automation and Kubernetes as a Service projects, my tasks revolved around requirements gathering, solution testing, ensuring compatibility with the University&#039;s existing infrastructure, and adhering to all change and release processes. &lt;br /&gt;
&lt;br /&gt;
Achievements include:&lt;br /&gt;
* Ensured the smooth operating of around 8 CoreOS Tectonic Kubernetes clusters&lt;br /&gt;
* Evaluated Pivotal Enterprise PKS and Red Hat OpenShift as a new managed Kubernetes solution&lt;br /&gt;
* Coordinated with our internal web team to transition the University&#039;s websites onto our Kubernetes infrastructure&lt;br /&gt;
* Built and migrated applications into a containerized environment&lt;br /&gt;
* Automated VM deployments on the University&#039;s private cloud&lt;br /&gt;
* Automated Linux deployments with VMware vRealize Automation and VMware vRealize Orchestrator with integration to Red Hat Satellite 6&lt;br /&gt;
* Documented change / release as per IT policy for new service offerings &lt;br /&gt;
&lt;br /&gt;
=== Technical Systems Analyst for Linux Desktop - University of Calgary ===&lt;br /&gt;
July 2018 - November 2018, Full Time, http://ucalgary.ca/it&lt;br /&gt;
&lt;br /&gt;
I was a Linux systems analyst under the Customer Technology Standards group tasked with designing and building a standardized Linux desktop environment for the University of Calgary. This position required transitioning and unifying the disparate Linux desktop infrastructures across various departments into a single standardized solution managed by central IT. &lt;br /&gt;
&lt;br /&gt;
Over a period of a few short months, I successfully:&lt;br /&gt;
* Deployed TheForeman, an open source alternative to Red Hat Satellite, for unattended Linux deployments supporting Fedora &amp;amp; Kali Linux on both Intel and ARM based computers&lt;br /&gt;
* Wrote and integrated Puppet manifests with TheForeman for desired state configuration on all hosts&lt;br /&gt;
* Created and updated custom Linux based packages used by teaching and research&lt;br /&gt;
* Deployed 250+ Linux workstations for the Department of Computer Science&lt;br /&gt;
* Consulted with faculty members to ensure software requirements required for teaching are met&lt;br /&gt;
* Created support materials on the new deployment system for technical staff in Computer Science&lt;br /&gt;
&lt;br /&gt;
=== Linux System Administrator - University of Calgary ===&lt;br /&gt;
October 2013 - July 2018, Full Time, http://ucalgary.ca/cpsc&lt;br /&gt;
&lt;br /&gt;
I was a Linux system administrator that oversaw over 400 Linux-based servers and workstations for the department of Computer Science at the University of Calgary.&lt;br /&gt;
&lt;br /&gt;
My primary tasks were to provide a consistent and reliable computing environment for research and teaching by supporting all Linux-based servers and workstations along with their applications and services. I was also primarily responsible for the proper function and operation of the department&#039;s firewall and intrusion detection system.&lt;br /&gt;
&lt;br /&gt;
Secondary tasks include:&lt;br /&gt;
* Providing tier 2-3 support (via desk-side, phone, email, help desk) for faculty, staff, and students, including Windows and MacOS support&lt;br /&gt;
* Testing and applying updates to servers and services every month&lt;br /&gt;
* Monitoring, maintaining, and configuring hardware for desktops, servers, and switches&lt;br /&gt;
* Deploying new servers and workstations and disposing or relocating old computers as required&lt;br /&gt;
* Ensuring public infrastructure services are up to date and operational, including mirror.cpsc.ucalgary.ca, CPSC&#039;s public NTP servers, and one of four DNS nameservers for the University.&lt;br /&gt;
* Maintaining and updating a private package repository, including creating custom packages for servers and workstations&lt;br /&gt;
* Maintain proper support documentation and end-user guides on services&lt;br /&gt;
* Consulting with faculty members to ensure software requirements for teaching are met&lt;br /&gt;
* Ensuring labs are using an updated and fully patched distribution every year&lt;br /&gt;
&lt;br /&gt;
Other achievements that I am proud of are:&lt;br /&gt;
* Integrated Linux workstations and servers with Windows Active Directory to streamline authentication&lt;br /&gt;
* Developed many in-house applications that are used by the Faculty of Science including:&lt;br /&gt;
** PokeAPI - A RESTful API that facades data from multiple sources including LDAP, data warehouse, databases, and file servers&lt;br /&gt;
** User Telemetry - A set of tools to gather computer usage in our undergraduate labs on both Windows and Linux&lt;br /&gt;
** Displays - A web-based tool for managing contents for digital signage across the Faculty of Science powered by a combination of embedded devices and computers&lt;br /&gt;
** The CPSC Guide - A GUI tool that helps students do common tasks rather than through the command line&lt;br /&gt;
** CS Agreement - A Windows/Linux application that allows students to agree to an AUP before being allowed to use our computers&lt;br /&gt;
* Created an automated build system for workstations and servers to automatically deploy machines through PXE and automatically configures its DNS and DHCP configuration&lt;br /&gt;
* Deployed Logstash + ElasticSearch for internal logging of both Windows and Linux servers and workstations&lt;br /&gt;
* Installed and maintained services for teaching and research including WebCAT, OwnCloud, Moodle, cPanel+CloudOS, VMware ESXi&lt;br /&gt;
* Replaced an aging CFEngine automated configuration tool with Salt Stack&lt;br /&gt;
* Rolled out ZFS file servers on undergraduate servers, enabling self-served file restore through snapshots&lt;br /&gt;
* Consolidated multiple servers and services into individual Docker containers&lt;br /&gt;
* Rebuilt the Raspberry Pi lab, reducing the amount of lost parts and clutter in the lab.&lt;br /&gt;
&lt;br /&gt;
Recognition:&lt;br /&gt;
* My team and I received the Faculty of Science innovation and Change Award of Excellence on May 2015.&lt;br /&gt;
&lt;br /&gt;
=== C# Developer - Onstream Pipeline Inspection Ltd. ===&lt;br /&gt;
May 2011 – September 2013, Full Time, http://onstream-pipeline.com/ &lt;br /&gt;
&lt;br /&gt;
I was one of two C# developers at a pipeline inspection company, developing their new version of pipeline analysis software.&lt;br /&gt;
* Debugged existing in-house software written in Borland C++ on the Windows platform&lt;br /&gt;
* Low-level hardware interface with custom designed pigs&lt;br /&gt;
* Developed the next version of their analysis software from scratch using C# .NET4&lt;br /&gt;
&lt;br /&gt;
=== Freelance Web Developer &amp;amp; Designer – Steamr ===&lt;br /&gt;
June 2005 – 2014, Freelance, http://steamr.com/ &lt;br /&gt;
&lt;br /&gt;
I am a freelance web designer and developer and provided clients primarily in North America with custom web designs and applications. I am responsible for ensuring projects are completed on-time, on-budget and satisfies client requirements. &lt;br /&gt;
&lt;br /&gt;
A sample of my work can be found on my portfolio at: http://steamr.com/portfolio &lt;br /&gt;
&lt;br /&gt;
Responsibilities: &lt;br /&gt;
* Consulted with clients for every project for budget, timeline and user requirements and delivered frequent deliverables and project updates.&lt;br /&gt;
* Created and deployed custom web designs from scratch using Adobe Photoshop, Illustrator, Notepad++, and half a dozen web browsers. &lt;br /&gt;
* Ensured all websites were optimal, standards compliant, cross-browser compatible, and search engine optimized (SEO) based on web analytics.&lt;br /&gt;
* Created and maintained over 20 projects under the LAMP stack, along with using PHP frameworks and libraries including: Kohana, CodeIgniter, FPDF, Smarty.&lt;br /&gt;
* Created over 40 individual websites from scratch for businesses and individuals.&lt;br /&gt;
* Customized design themes for WHMCS, SolusVM, Kayako Support Suite for hosting companies.&lt;br /&gt;
	&lt;br /&gt;
Related Work:&lt;br /&gt;
* Previous webmaster for GlobalTech Communications including: nworks.ca, gtcomm.net, unmeteredserver.net, zenprotection.com&lt;br /&gt;
* Responsible for updating content and promotions while ensuring website correctly interfaces with the point of sale application.&lt;br /&gt;
* Developed an internal network management application for gtcomm.net in PHP utilizing MRTG. Abnormal network patterns will notify an administrator via email and SMS. &lt;br /&gt;
* Created a distributed network monitoring system based on Java, PHP and MySQL, which utilizes http and ping tests to determine the availability and latency of remote services. Results are then recorded and monthly reports generated.&lt;br /&gt;
* Designed the front-end of an in-house inventory system used by gtcomm.net used to track networking/server hardware along with managing available IP pools.&lt;br /&gt;
* Designed the front-end of an in-house project enabling end users to purchase and manage DDoS protection services by zenprotection.net.&lt;br /&gt;
&lt;br /&gt;
=== Server Administrator - SwiftHost.net ===&lt;br /&gt;
January 2004 – May 2012, http://swifthost.net/ &lt;br /&gt;
&lt;br /&gt;
With a partner, I created a web hosting company, focusing on affordable shared and reseller hosting. Servers utilized were based on the LAMP stack along with the use of the cPanel control panel. &lt;br /&gt;
&lt;br /&gt;
Responsibilities: &lt;br /&gt;
* Managed production servers hosting over 400 individual websites&lt;br /&gt;
* Responsible for responding to many customer support tickets within 6 hours for issues under the technical, abuse and billing department.&lt;br /&gt;
* Technical issues handled by me ranged from fixing simple permission issues to handling requests for specific software such as Apache/PHP modules, system libraries, and Caching / PHP Accelerators&lt;br /&gt;
* Performed security audits and server hardening by disabling or restricting services, configuring the firewall, detecting compromised websites and spam control in a shared hosting environment.&lt;br /&gt;
* Configured weekly, network-based backups to an off-site location using cronjobs, rsync and shell scripts. Performed account or file specific restores from backups.&lt;br /&gt;
	&lt;br /&gt;
Projects:&lt;br /&gt;
* Designed a custom billing solution for account management using PHP, MySQL and the PayPal API.&lt;br /&gt;
* A status page which monitored and reported live server status information such as processor load, traffic load, service availability, memory usage to customers.&lt;br /&gt;
* Developed a custom web control panel, similar to cPanel, targeted to free hosting accounts. The control panel based on PHP, MySQL, Shell + Perl Scripts, enabled end users to manage their websites, domains and databases while automatically maintaining and enforcing disk and bandwidth quotas.&lt;br /&gt;
&lt;br /&gt;
=== AS/400 Night Operator - Calgary Programming Factory ===&lt;br /&gt;
May 2007 – July 2007, Part Time&lt;br /&gt;
&lt;br /&gt;
I worked as night operator as part of the AS/400 support team at the Calgary Programming Factory for The Sovereign General Insurance Company as a summer job. &lt;br /&gt;
&lt;br /&gt;
Responsibilities:&lt;br /&gt;
* Operated the tape backup system, ensuring backups are valid and without errors.&lt;br /&gt;
* Operated the AS/400 system to print system logs and claim checks&lt;br /&gt;
* Ensured paper was loaded and aligned properly to the printers before printing&lt;br /&gt;
* Researched feasibility to migrate their existing projects based on JSP servlets to ASP.NET&lt;br /&gt;
&lt;br /&gt;
== Personal ==&lt;br /&gt;
* Canadian; Speaks English &amp;amp; Cantonese&lt;br /&gt;
* Interest in anything Linux related, System Infrastructure, Systems Security, Game Development, Electronics &amp;amp; Tinkering&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Hire_Me&amp;diff=7713</id>
		<title>Hire Me</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Hire_Me&amp;diff=7713"/>
		<updated>2025-12-05T07:01:01Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This is my current resume / CV.&lt;br /&gt;
&lt;br /&gt;
== Objective ==&lt;br /&gt;
With my wide skillset in different technical areas, my goal is to come up with elegant and streamlined solutions that meet both user and IT business operation requirements. &lt;br /&gt;
&lt;br /&gt;
== Relevant Skills ==&lt;br /&gt;
&lt;br /&gt;
=== OPERATING SYSTEMS ===&lt;br /&gt;
* {{#expr: floor( {{CURRENTYEAR}} - 2006) }}+ years of server experience with Linux (Primarily Red Hat-based, CentOS, Rocky Linux, Fedora)&lt;br /&gt;
* {{#expr: floor( {{CURRENTYEAR}} - 2005) }}+ years of desktop support experience with Windows&lt;br /&gt;
* Experience with Solaris and FreeBSD&lt;br /&gt;
&lt;br /&gt;
=== NETWORKING ===&lt;br /&gt;
* Understandings key concepts of network protocols (TCP/IP, IP Routing, VLAN, etc.)&lt;br /&gt;
* Familiar with physical network maintenance, software firewalls (IPTables), NAS appliances&lt;br /&gt;
* Experience with HP and Cisco switches&lt;br /&gt;
&lt;br /&gt;
=== SERVICES AND SOFTWARE ===&lt;br /&gt;
* Containers: Docker, Docker Swarm, Kubernetes, Singularity&lt;br /&gt;
* Configuration Tools: Ansible, SaltStack, Puppet, Terraform, Packer&lt;br /&gt;
* Services: HTTP/HTTPS (Apache 2, nodejs), SMTP (Sendmail, Exim), POP/IMAP (Dovecot), DNS (Bind), DHCP, NFS, NTP, FTP, PXE/TFTP, Samba, Active Directory/LDAP&lt;br /&gt;
* Products: Spectrum Archive, Spectrum Protect (TSM), VMware vSphere, TheForeman / Red Hat Satellite, GitLab, EFK/ELK (ElasticSearch, Logstash/Fluentd, Kibana), CloudStack&lt;br /&gt;
&lt;br /&gt;
=== SECURITY ===&lt;br /&gt;
* Experience with Kerberos and LDAP authentication&lt;br /&gt;
* Can perform security analysis via log processing, packet analysis, tcpdump&lt;br /&gt;
&lt;br /&gt;
=== WEB ===&lt;br /&gt;
* {{#expr: floor( {{CURRENTYEAR}} - 2008) }}+ years configuring and web deploying applications&lt;br /&gt;
* Containerize web applications for Kubernetes&lt;br /&gt;
* Customize templates for MediaWiki, WordPress, Drupal, WHMCS, Kayako Support Suite&lt;br /&gt;
* Experience with installing and administering cPanel/WHM, Plesk, DirectAdmin, SolusVM, HyperVM, Webmin&lt;br /&gt;
*Experience with Amazon Web Services (AWS), Oracle Cloud Infrastructure (OCI)&lt;br /&gt;
&lt;br /&gt;
=== PROGRAMMING ===&lt;br /&gt;
* Coding: Java, C#, PHP, Python, HTML + CSS, Javascript (AngularJS, jQuery, nodejs), C, C++&lt;br /&gt;
* Scripting: bash/sh/sed/awk, Perl, Powershell&lt;br /&gt;
* Database: MySQL/MariaDB, SQLite, PostgreSQL, InfluxDB/flux&lt;br /&gt;
* Debug: C, C++, gdb, strace, Ghidra&lt;br /&gt;
&lt;br /&gt;
=== HARDWARE ===&lt;br /&gt;
* Experience with x86/x86_64/ARM64 Hardware&lt;br /&gt;
* Experience with server and workstation hardware from Sun, Dell, and HP&lt;br /&gt;
* Experience with IBM Tape Library (TS4500)&lt;br /&gt;
&lt;br /&gt;
== Education &amp;amp; Training ==&lt;br /&gt;
* Bachelor of Science in Computer Science, University of Calgary&lt;br /&gt;
** August 2007 - May 2011&lt;br /&gt;
** Notable courses: CPSC 550; System Administration with Darcy Grant, Wayne Pearson (2010 – 2011)&lt;br /&gt;
* Red Hat Satellite 6 Training&lt;br /&gt;
** Completed August 2018&lt;br /&gt;
* VMware vSphere 6.7&lt;br /&gt;
** Completed January 2019&lt;br /&gt;
* VMware vRealize Automation 6.5&lt;br /&gt;
** Completed January 2019&lt;br /&gt;
&lt;br /&gt;
== Work History ==&lt;br /&gt;
=== HPC System Administrator - University of Calgary ===&lt;br /&gt;
December 2019 - December 2025, Full Time, http://ucalgary.ca/hpc&lt;br /&gt;
&lt;br /&gt;
I maintained and transitioned infrastructure operated at the Centre for Health Genomics and Informatics (CHGI) at the Cumming School of Medicine (CSM) to the centralized IT infrastructure, operated by Research Computing Services (RCS). &lt;br /&gt;
&lt;br /&gt;
Major accomplishments include:&lt;br /&gt;
* Transitioned our HPC cluster of over 1000 nodes to Ansible and automated configuration deployments. This help speed up deployments and day-to-day tasks such as user creation and node deployment.&lt;br /&gt;
* Automated HPC cluster usage report generation from various sources for senior leadership and decision making&lt;br /&gt;
* Implemented and maintained Open OnDemand and custom containerized apps as a friendlier interface to RCS&#039;s HPC resources&lt;br /&gt;
* Implemented and maintained CloudStack as a private cloud solution; written training material and provided training sessions for users&lt;br /&gt;
* Facilitated researchers into RCS managed AWS Control Tower environment while ensuring instutional security compliance and governance&lt;br /&gt;
&lt;br /&gt;
My duties included:&lt;br /&gt;
* Operated, maintained, and troubleshooted HPC clusters running Slurm&lt;br /&gt;
* Collected cluster metrics and hardware performance data which are stored into InfluxDB and visualized with Grafana&lt;br /&gt;
* Provided researcher support for HPC, Open OnDemand, storage, CloudStack requests and issues&lt;br /&gt;
* Gathered researcher requirements and ensure deployed services met their operational needs&lt;br /&gt;
* Automate and streamline operational processes with custom scripts and CI/CD pipelines&lt;br /&gt;
* Supported RCS&#039;s internal development team by providing guidance related to systems programming and development&lt;br /&gt;
&lt;br /&gt;
=== Infrastructure Automation &amp;amp; Containers Specialist - University of Calgary ===&lt;br /&gt;
December 2018 - December 2019, Full Time Limited Term, http://ucalgary.ca/it&lt;br /&gt;
&lt;br /&gt;
I was part of a newly created team of two tasked with creating a new infrastructure automation system using VMware vRealize Automation as well as creating a managed on-site Kubernetes as a service solution for the University of Calgary.&lt;br /&gt;
&lt;br /&gt;
Due to the early stage of both infrastructure automation and Kubernetes as a Service projects, my tasks revolved around requirements gathering, solution testing, ensuring compatibility with the University&#039;s existing infrastructure, and adhering to all change and release processes. &lt;br /&gt;
&lt;br /&gt;
Achievements include:&lt;br /&gt;
* Ensured the smooth operating of around 8 CoreOS Tectonic Kubernetes clusters&lt;br /&gt;
* Evaluated Pivotal Enterprise PKS and Red Hat OpenShift as a new managed Kubernetes solution&lt;br /&gt;
* Coordinated with our internal web team to transition the University&#039;s websites onto our Kubernetes infrastructure&lt;br /&gt;
* Built and migrated applications into a containerized environment&lt;br /&gt;
* Automated VM deployments on the University&#039;s private cloud&lt;br /&gt;
* Automated Linux deployments with VMware vRealize Automation and VMware vRealize Orchestrator with integration to Red Hat Satellite 6&lt;br /&gt;
* Documented change / release as per IT policy for new service offerings &lt;br /&gt;
&lt;br /&gt;
=== Technical Systems Analyst for Linux Desktop - University of Calgary ===&lt;br /&gt;
July 2018 - November 2018, Full Time, http://ucalgary.ca/it&lt;br /&gt;
&lt;br /&gt;
I was a Linux systems analyst under the Customer Technology Standards group tasked with designing and building a standardized Linux desktop environment for the University of Calgary. This position required transitioning and unifying the disparate Linux desktop infrastructures across various departments into a single standardized solution managed by central IT. &lt;br /&gt;
&lt;br /&gt;
Over a period of a few short months, I successfully:&lt;br /&gt;
* Deployed TheForeman, an open source alternative to Red Hat Satellite, for unattended Linux deployments supporting Fedora &amp;amp; Kali Linux on both Intel and ARM based computers&lt;br /&gt;
* Wrote and integrated Puppet manifests with TheForeman for desired state configuration on all hosts&lt;br /&gt;
* Created and updated custom Linux based packages used by teaching and research&lt;br /&gt;
* Deployed 250+ Linux workstations for the Department of Computer Science&lt;br /&gt;
* Consulted with faculty members to ensure software requirements required for teaching are met&lt;br /&gt;
* Created support materials on the new deployment system for technical staff in Computer Science&lt;br /&gt;
&lt;br /&gt;
=== Linux System Administrator - University of Calgary ===&lt;br /&gt;
October 2013 - July 2018, Full Time, http://ucalgary.ca/cpsc&lt;br /&gt;
&lt;br /&gt;
I was a Linux system administrator that oversaw over 400 Linux-based servers and workstations for the department of Computer Science at the University of Calgary.&lt;br /&gt;
&lt;br /&gt;
My primary tasks were to provide a consistent and reliable computing environment for research and teaching by supporting all Linux-based servers and workstations along with their applications and services. I was also primarily responsible for the proper function and operation of the department&#039;s firewall and intrusion detection system.&lt;br /&gt;
&lt;br /&gt;
Secondary tasks include:&lt;br /&gt;
* Providing tier 2-3 support (via desk-side, phone, email, help desk) for faculty, staff, and students, including Windows and MacOS support&lt;br /&gt;
* Testing and applying updates to servers and services every month&lt;br /&gt;
* Monitoring, maintaining, and configuring hardware for desktops, servers, and switches&lt;br /&gt;
* Deploying new servers and workstations and disposing or relocating old computers as required&lt;br /&gt;
* Ensuring public infrastructure services are up to date and operational, including mirror.cpsc.ucalgary.ca, CPSC&#039;s public NTP servers, and one of four DNS nameservers for the University.&lt;br /&gt;
* Maintaining and updating a private package repository, including creating custom packages for servers and workstations&lt;br /&gt;
* Maintain proper support documentation and end-user guides on services&lt;br /&gt;
* Consulting with faculty members to ensure software requirements for teaching are met&lt;br /&gt;
* Ensuring labs are using an updated and fully patched distribution every year&lt;br /&gt;
&lt;br /&gt;
Other achievements that I am proud of are:&lt;br /&gt;
* Integrated Linux workstations and servers with Windows Active Directory to streamline authentication&lt;br /&gt;
* Developed many in-house applications that are used by the Faculty of Science including:&lt;br /&gt;
** PokeAPI - A RESTful API that facades data from multiple sources including LDAP, data warehouse, databases, and file servers&lt;br /&gt;
** User Telemetry - A set of tools to gather computer usage in our undergraduate labs on both Windows and Linux&lt;br /&gt;
** Displays - A web-based tool for managing contents for digital signage across the Faculty of Science powered by a combination of embedded devices and computers&lt;br /&gt;
** The CPSC Guide - A GUI tool that helps students do common tasks rather than through the command line&lt;br /&gt;
** CS Agreement - A Windows/Linux application that allows students to agree to an AUP before being allowed to use our computers&lt;br /&gt;
* Created an automated build system for workstations and servers to automatically deploy machines through PXE and automatically configures its DNS and DHCP configuration&lt;br /&gt;
* Deployed Logstash + ElasticSearch for internal logging of both Windows and Linux servers and workstations&lt;br /&gt;
* Installed and maintained services for teaching and research including WebCAT, OwnCloud, Moodle, cPanel+CloudOS, VMware ESXi&lt;br /&gt;
* Replaced an aging CFEngine automated configuration tool with Salt Stack&lt;br /&gt;
* Rolled out ZFS file servers on undergraduate servers, enabling self-served file restore through snapshots&lt;br /&gt;
* Consolidated multiple servers and services into individual Docker containers&lt;br /&gt;
* Rebuilt the Raspberry Pi lab, reducing the amount of lost parts and clutter in the lab.&lt;br /&gt;
&lt;br /&gt;
Recognition:&lt;br /&gt;
* My team and I received the Faculty of Science innovation and Change Award of Excellence on May 2015.&lt;br /&gt;
&lt;br /&gt;
=== C# Developer - Onstream Pipeline Inspection Ltd. ===&lt;br /&gt;
May 2011 – September 2013, Full Time, http://onstream-pipeline.com/ &lt;br /&gt;
&lt;br /&gt;
I was one of two C# developers at a pipeline inspection company, developing their new version of pipeline analysis software.&lt;br /&gt;
* Debugged existing in-house software written in Borland C++ on the Windows platform&lt;br /&gt;
* Low-level hardware interface with custom designed pigs&lt;br /&gt;
* Developed the next version of their analysis software from scratch using C# .NET4&lt;br /&gt;
&lt;br /&gt;
=== Freelance Web Developer &amp;amp; Designer – Steamr ===&lt;br /&gt;
June 2005 – 2014, Freelance, http://steamr.com/ &lt;br /&gt;
&lt;br /&gt;
I am a freelance web designer and developer and provided clients primarily in North America with custom web designs and applications. I am responsible for ensuring projects are completed on-time, on-budget and satisfies client requirements. &lt;br /&gt;
&lt;br /&gt;
A sample of my work can be found on my portfolio at: http://steamr.com/portfolio &lt;br /&gt;
&lt;br /&gt;
Responsibilities: &lt;br /&gt;
* Consulted with clients for every project for budget, timeline and user requirements and delivered frequent deliverables and project updates.&lt;br /&gt;
* Created and deployed custom web designs from scratch using Adobe Photoshop, Illustrator, Notepad++, and half a dozen web browsers. &lt;br /&gt;
* Ensured all websites were optimal, standards compliant, cross-browser compatible, and search engine optimized (SEO) based on web analytics.&lt;br /&gt;
* Created and maintained over 20 projects under the LAMP stack, along with using PHP frameworks and libraries including: Kohana, CodeIgniter, FPDF, Smarty.&lt;br /&gt;
* Created over 40 individual websites from scratch for businesses and individuals.&lt;br /&gt;
* Customized design themes for WHMCS, SolusVM, Kayako Support Suite for hosting companies.&lt;br /&gt;
	&lt;br /&gt;
Related Work:&lt;br /&gt;
* Previous webmaster for GlobalTech Communications including: nworks.ca, gtcomm.net, unmeteredserver.net, zenprotection.com&lt;br /&gt;
* Responsible for updating content and promotions while ensuring website correctly interfaces with the point of sale application.&lt;br /&gt;
* Developed an internal network management application for gtcomm.net in PHP utilizing MRTG. Abnormal network patterns will notify an administrator via email and SMS. &lt;br /&gt;
* Created a distributed network monitoring system based on Java, PHP and MySQL, which utilizes http and ping tests to determine the availability and latency of remote services. Results are then recorded and monthly reports generated.&lt;br /&gt;
* Designed the front-end of an in-house inventory system used by gtcomm.net used to track networking/server hardware along with managing available IP pools.&lt;br /&gt;
* Designed the front-end of an in-house project enabling end users to purchase and manage DDoS protection services by zenprotection.net.&lt;br /&gt;
&lt;br /&gt;
=== Server Administrator - SwiftHost.net ===&lt;br /&gt;
January 2004 – May 2012, http://swifthost.net/ &lt;br /&gt;
&lt;br /&gt;
With a partner, I created a web hosting company, focusing on affordable shared and reseller hosting. Servers utilized were based on the LAMP stack along with the use of the cPanel control panel. &lt;br /&gt;
&lt;br /&gt;
Responsibilities: &lt;br /&gt;
* Managed production servers hosting over 400 individual websites&lt;br /&gt;
* Responsible for responding to many customer support tickets within 6 hours for issues under the technical, abuse and billing department.&lt;br /&gt;
* Technical issues handled by me ranged from fixing simple permission issues to handling requests for specific software such as Apache/PHP modules, system libraries, and Caching / PHP Accelerators&lt;br /&gt;
* Performed security audits and server hardening by disabling or restricting services, configuring the firewall, detecting compromised websites and spam control in a shared hosting environment.&lt;br /&gt;
* Configured weekly, network-based backups to an off-site location using cronjobs, rsync and shell scripts. Performed account or file specific restores from backups.&lt;br /&gt;
	&lt;br /&gt;
Projects:&lt;br /&gt;
* Designed a custom billing solution for account management using PHP, MySQL and the PayPal API.&lt;br /&gt;
* A status page which monitored and reported live server status information such as processor load, traffic load, service availability, memory usage to customers.&lt;br /&gt;
* Developed a custom web control panel, similar to cPanel, targeted to free hosting accounts. The control panel based on PHP, MySQL, Shell + Perl Scripts, enabled end users to manage their websites, domains and databases while automatically maintaining and enforcing disk and bandwidth quotas.&lt;br /&gt;
&lt;br /&gt;
=== AS/400 Night Operator - Calgary Programming Factory ===&lt;br /&gt;
May 2007 – July 2007, Part Time&lt;br /&gt;
&lt;br /&gt;
I worked as night operator as part of the AS/400 support team at the Calgary Programming Factory for The Sovereign General Insurance Company as a summer job. &lt;br /&gt;
&lt;br /&gt;
Responsibilities:&lt;br /&gt;
* Operated the tape backup system, ensuring backups are valid and without errors.&lt;br /&gt;
* Operated the AS/400 system to print system logs and claim checks&lt;br /&gt;
* Ensured paper was loaded and aligned properly to the printers before printing&lt;br /&gt;
* Researched feasibility to migrate their existing projects based on JSP servlets to ASP.NET&lt;br /&gt;
&lt;br /&gt;
== Personal ==&lt;br /&gt;
* Canadian; Speaks English &amp;amp; Cantonese&lt;br /&gt;
* Interest in anything Linux related, System Infrastructure, Systems Security, Game Development, Electronics &amp;amp; Tinkering&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Warewulf&amp;diff=7712</id>
		<title>Warewulf</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Warewulf&amp;diff=7712"/>
		<updated>2025-10-09T21:16:11Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Warewulf is an open-source Linux cluster provisioning system. It is typically used for HPC cluster applications, but can also be used for managing environments that need to boot multiple nodes (such as a VM infrastructure environment). Warewulf provides a lightweight and flexible way to boot diskless nodes via PXE and manage node images centrally from a head node.&lt;br /&gt;
&lt;br /&gt;
== Common Commands ==&lt;br /&gt;
&lt;br /&gt;
{|class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Purpose                          &lt;br /&gt;
! Command&lt;br /&gt;
|-&lt;br /&gt;
| Check Warewulf node status            &lt;br /&gt;
| {{code|wwcctl node status}}&lt;br /&gt;
|-&lt;br /&gt;
| View node details                &lt;br /&gt;
| {{code|wwcctl node show &amp;lt;nodename&amp;gt;}}&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| Create a new node                &lt;br /&gt;
| {{code|wwcctl node add &amp;lt;nodename&amp;gt; --ipaddr &amp;lt;ip&amp;gt; --hwaddr &amp;lt;mac&amp;gt;}}&lt;br /&gt;
|-&lt;br /&gt;
|Set node to use disk as /tmp&lt;br /&gt;
|&amp;lt;code&amp;gt;wwctl node set &amp;lt;nodename&amp;gt; --diskname /dev/sda --diskwipe --partname tmp --partcreate --fsname tmp --fsformat ext4 --fspath /tmp --fswipe&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| Set boot image for a node        &lt;br /&gt;
| {{code|wwcctl node set &amp;lt;nodename&amp;gt; --vnfs &amp;lt;image&amp;gt;}}&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| Build VNFS (node image)          &lt;br /&gt;
| {{code|wwcctl overlay build}}&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| Import container image           &lt;br /&gt;
| {{code|wwcctl import docker://rockylinux:9 --name rocky9}}&lt;br /&gt;
|-&lt;br /&gt;
| Push configuration to nodes      &lt;br /&gt;
| {{code|wwcctl overlay build}}&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Common Tasks ==&lt;br /&gt;
&lt;br /&gt;
===Install and Configure Warewulf===&lt;br /&gt;
&lt;br /&gt;
{{highlight&lt;br /&gt;
| lang = terminal&lt;br /&gt;
| code = # wget https://github.com/warewulf/warewulf/releases/download/v4.6.4/warewulf-4.6.4-1.el9.x86_64.rpm&lt;br /&gt;
# dnf -y install warewulf-4.6.4-1.el9.x86_64.rpm&lt;br /&gt;
&lt;br /&gt;
## Configure warewulf in /etc/warewulf/warewulf.conf&lt;br /&gt;
## Ensure that the IP addresses are set to something that works for your network.&lt;br /&gt;
# vi /etc/warewulf/warewulf.conf&lt;br /&gt;
&lt;br /&gt;
## Set up the first boot image and configure the first node&lt;br /&gt;
# wwctl image import docker://ghcr.io/warewulf/warewulf-rockylinux:9 rockylinux-9 --build&lt;br /&gt;
# systemctl enable --now warewulfd&lt;br /&gt;
# wwctl profile set default --image rockylinux-9&lt;br /&gt;
# wwctl node add node01  --ipaddr=172.19.0.101 --hwaddr=02:01:01:58:00:06 --gateway 172.19.0.1&lt;br /&gt;
# wwctl configure --all&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
At this point, you should have a DHCP and TFTP running on the system. NFS server should also be working if that is something you enabled in the configuration file.&lt;br /&gt;
&lt;br /&gt;
Verify the state of warewulf by running:&lt;br /&gt;
{{highlight|lang=terminal|code=&lt;br /&gt;
# warewulf status&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== No network gateway ===&lt;br /&gt;
There doesn&#039;t seem to be a way to define gateways in warewulf&#039;s managed dhcp. &lt;br /&gt;
&lt;br /&gt;
Edit &amp;lt;code&amp;gt;/etc/warewful/warewulf.conf&amp;lt;/code&amp;gt; and set the dhcp template to &#039;static&#039;, then &amp;lt;code&amp;gt;wwctl overlay edit host etc/dhcpd.conf.ww&amp;lt;/code&amp;gt; and add the following within the host definition.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = { {- if $netdevs.Gateway} }&lt;br /&gt;
    option routers { {$netdevs.Gateway} };&lt;br /&gt;
    { {- end } }&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Warewulf&amp;diff=7711</id>
		<title>Warewulf</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Warewulf&amp;diff=7711"/>
		<updated>2025-10-07T22:13:12Z</updated>

		<summary type="html">&lt;p&gt;Leo: Created page with &amp;quot;Warewulf is an open-source Linux cluster provisioning system. It is typically used for HPC cluster applications, but can also be used for managing environments that need to boot multiple nodes (such as a VM infrastructure environment). Warewulf provides a lightweight and flexible way to boot diskless nodes via PXE and manage node images centrally from a head node.  == Common Commands ==  {|class=&amp;quot;wikitable&amp;quot; ! Purpose                           ! Command |- | Check Warewul...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Warewulf is an open-source Linux cluster provisioning system. It is typically used for HPC cluster applications, but can also be used for managing environments that need to boot multiple nodes (such as a VM infrastructure environment). Warewulf provides a lightweight and flexible way to boot diskless nodes via PXE and manage node images centrally from a head node.&lt;br /&gt;
&lt;br /&gt;
== Common Commands ==&lt;br /&gt;
&lt;br /&gt;
{|class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Purpose                          &lt;br /&gt;
! Command&lt;br /&gt;
|-&lt;br /&gt;
| Check Warewulf node status            &lt;br /&gt;
| {{code|wwcctl node status}}&lt;br /&gt;
|-&lt;br /&gt;
| View node details                &lt;br /&gt;
| {{code|wwcctl node show &amp;lt;nodename&amp;gt;}}&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| Create a new node                &lt;br /&gt;
| {{code|wwcctl node add &amp;lt;nodename&amp;gt; --ipaddr &amp;lt;ip&amp;gt; --hwaddr &amp;lt;mac&amp;gt;}}&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| Set boot image for a node        &lt;br /&gt;
| {{code|wwcctl node set &amp;lt;nodename&amp;gt; --vnfs &amp;lt;image&amp;gt;}}&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| Build VNFS (node image)          &lt;br /&gt;
| {{code|wwcctl overlay build}}&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| Import container image           &lt;br /&gt;
| {{code|wwcctl import docker://rockylinux:9 --name rocky9}}&lt;br /&gt;
|-&lt;br /&gt;
| Push configuration to nodes      &lt;br /&gt;
| {{code|wwcctl overlay build}}&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Common Tasks ==&lt;br /&gt;
&lt;br /&gt;
===Install and Configure Warewulf===&lt;br /&gt;
&lt;br /&gt;
{{highlight|lang=terminal|code=&lt;br /&gt;
# wget https://github.com/warewulf/warewulf/releases/download/v4.6.4/warewulf-4.6.4-1.el9.x86_64.rpm&lt;br /&gt;
# dnf -y install warewulf-4.6.4-1.el9.x86_64.rpm&lt;br /&gt;
&lt;br /&gt;
## Configure warewulf in /etc/warewulf/warewulf.conf&lt;br /&gt;
## Ensure that the IP addresses are set to something that works for your network.&lt;br /&gt;
# vi /etc/warewulf/warewulf.conf&lt;br /&gt;
&lt;br /&gt;
## Set up the first boot image and configure the first node&lt;br /&gt;
# wwctl image import docker://ghcr.io/warewulf/warewulf-rockylinux:9 rockylinux-9 --build&lt;br /&gt;
# systemctl enable --now warewulfd&lt;br /&gt;
# wwctl profile set default --image rockylinux-9&lt;br /&gt;
# wwctl node add node01  --ipaddr=172.19.0.101 --hwaddr=02:01:01:58:00:06&lt;br /&gt;
# wwctl configure --all&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
At this point, you should have a DHCP and TFTP running on the system. NFS server should also be working if that is something you enabled in the configuration file.&lt;br /&gt;
&lt;br /&gt;
Verify the state of warewulf by running:&lt;br /&gt;
{{highlight|lang=terminal|code=&lt;br /&gt;
# warewulf status&lt;br /&gt;
}}&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=NixOS_inside_LXC_on_Proxmox&amp;diff=7710</id>
		<title>NixOS inside LXC on Proxmox</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=NixOS_inside_LXC_on_Proxmox&amp;diff=7710"/>
		<updated>2025-09-18T17:57:09Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This page will go over how you can set up and run NixOS in LXC on Proxmox. &lt;br /&gt;
&lt;br /&gt;
The instructions here are based on the following resources:&lt;br /&gt;
&lt;br /&gt;
* https://nixos.wiki/wiki/Proxmox_Linux_Container&lt;br /&gt;
* https://blog.xirion.net/posts/nixos-proxmox-lxc/&lt;br /&gt;
&lt;br /&gt;
== Guide ==&lt;br /&gt;
&lt;br /&gt;
=== Step 1: Obtain the container tarball ===&lt;br /&gt;
Find and download a recently generated NixOS container tarball from https://hydra.nixos.org/job/nixos/trunk-combined/nixos.containerTarball.x86_64-linux. Place the &amp;lt;code&amp;gt;.tar.xz&amp;lt;/code&amp;gt; archive in your CT Volumes store in Proxmox (which is typically located under &amp;lt;code&amp;gt;/var/lib/vz/template/cache/&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== Step 2: Create the container ===&lt;br /&gt;
In Proxmox shell, create the container with the following command:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # pct create 300 --arch amd64 --description nixos --ostype unmanaged \&lt;br /&gt;
  --net0 name=eth0  --storage local-lvm --unprivileged 1 \&lt;br /&gt;
  local:vztmpl/nixos-system-x86_64-linux.tar.xz&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Open PVE and enable nesting. This is required by Nix. Not enabling nesting would cause the nix-daemon to have issues remounting &amp;lt;code&amp;gt;/nix/store&amp;lt;/code&amp;gt; or setting up namespaces. &lt;br /&gt;
&lt;br /&gt;
You may optionally adjust the size of the storage if desired (it defaults to 4GB which may not be enough). You may also want to change the other resource allocations before starting the container.&lt;br /&gt;
&lt;br /&gt;
=== Step 3: Start the container and configure it ===&lt;br /&gt;
Start the CT. The console will be blank. We&#039;ll fix this shortly. However, to connect to our container in the current state, we&#039;ll have to use the Proxmox shell and run:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # lxc-attach --name 300&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Note that because we dropped into the container without any of the environment variables set, nothing other than your shell will work. To fix this, update your path with the NixOS bin path and start bash:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = sh-5.2# PATH=$PATH:/run/current-system/sw/bin/&lt;br /&gt;
sh-5.2# bash&lt;br /&gt;
[root@nixos:~]#&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
That&#039;s better! Next, we&#039;ll fix the blank console. Edit the &amp;lt;code&amp;gt;/etc/nixos/configuration.nix&amp;lt;/code&amp;gt; such that the getty on tty1 works. Add in the following lines to &amp;lt;code&amp;gt;configuration.nix&amp;lt;/code&amp;gt;:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # Supress systemd units that don&#039;t work because of LXC&lt;br /&gt;
  systemd.suppressedSystemUnits = [&lt;br /&gt;
    &amp;quot;dev-mqueue.mount&amp;quot;&lt;br /&gt;
    &amp;quot;sys-kernel-debug.mount&amp;quot;&lt;br /&gt;
    &amp;quot;sys-fs-fuse-connections.mount&amp;quot;&lt;br /&gt;
  ];&lt;br /&gt;
&lt;br /&gt;
  # start tty1 on serial console&lt;br /&gt;
  systemd.services.&amp;quot;getty@tty1&amp;quot; = {&lt;br /&gt;
    enable = true;&lt;br /&gt;
    wantedBy = [ &amp;quot;getty.target&amp;quot; ]; # to start at boot&lt;br /&gt;
    serviceConfig.Restart = &amp;quot;always&amp;quot;; # restart when session is closed&lt;br /&gt;
    serviceConfig.ExecStart = [&amp;quot;&amp;quot; &amp;quot;@${pkgs.util-linux}/sbin/agetty agetty --login-program ${config.services.getty.loginProgram} --noclear --keep-baud %I 115200,38400,9600 $TERM&amp;quot;];&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  environment.systemPackages = with pkgs; [&lt;br /&gt;
    vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.&lt;br /&gt;
    binutils&lt;br /&gt;
  ];&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
Then run &amp;lt;code&amp;gt;nixos-rebuild switch&amp;lt;/code&amp;gt; to update.&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Trouble with nix-channel --update ===&lt;br /&gt;
If you did not nesting in the CT options, you will get: &amp;lt;code&amp;gt;unexpected Nix daemon error: error: remounting /nix/store writable: Permission denied&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== NixOS 25.05 does not have a getty.target ===&lt;br /&gt;
After updating to NixOS 25.05, I get this error in journalctl when I try to start &amp;lt;code&amp;gt;getty@tty1&amp;lt;/code&amp;gt;:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = systemd[1]: getty@tty1.service: Service has no ExecStart=, ExecStop=, or SuccessAction=. Refusing.&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
The configuration I had was: &lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = systemd.services.&amp;quot;getty@tty1&amp;quot; = {&lt;br /&gt;
    enable = lib.mkForce true;&lt;br /&gt;
    wantedBy = [ &amp;quot;getty.target&amp;quot; ]; # to start at boot&lt;br /&gt;
    serviceConfig.Restart = &amp;quot;always&amp;quot;; # restart when session is closed&lt;br /&gt;
  };&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
I&#039;m not totally sure why &amp;lt;code&amp;gt;getty.target&amp;lt;/code&amp;gt; is now missing. As a result, you&#039;ll have to specify what the getty is, so you have to now add the ExecStart line within the service definition:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = serviceConfig.ExecStart = [&amp;quot;&amp;quot; &amp;quot;@${pkgs.util-linux}/sbin/agetty agetty --login-program ${config.services.getty.loginProgram} --noclear --keep-baud %I 115200,38400,9600 $TERM&amp;quot;];&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Frigate&amp;diff=7709</id>
		<title>Frigate</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Frigate&amp;diff=7709"/>
		<updated>2025-08-23T21:07:05Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Frigate is a self hosted and open source video surveillance system.&lt;br /&gt;
&lt;br /&gt;
It is capable of:&lt;br /&gt;
&lt;br /&gt;
* streaming video from RTSP/RTMP&lt;br /&gt;
* performing real-time object detection or simply just motion detection&lt;br /&gt;
* keeping track of video clips and image snapshots with a retention policy&lt;br /&gt;
* MQTT integration for messaging events&lt;br /&gt;
&lt;br /&gt;
This page will contain some of my notes on using Frigate.&lt;br /&gt;
&lt;br /&gt;
== Setup ==&lt;br /&gt;
Frigate is distributed as a Docker image only and has to run as a Docker container. &lt;br /&gt;
&lt;br /&gt;
=== Object detection ===&lt;br /&gt;
The object detection system requires a detector set up. By default, Frigate will have a CPU detector enabled. This isn&#039;t recommended as the object detection uses a lot of CPU cycles while being slow. Frigate recommends using a USB Google Coral for this task, but  you may also choose to use a NVIDIA GPU with TensorRT (supported on post-Pascal GPUs only). &lt;br /&gt;
&lt;br /&gt;
Due to the chip shortage, Google Corals aren&#039;t easy to come by. The best alternative is to pick up a cheap NVIDIA Quadro GPU such as the P400. I got a second-hand P400 for around $80 CAD.&lt;br /&gt;
&lt;br /&gt;
==== ONNX ====&lt;br /&gt;
ONNX now replaces TensorRT. The simplest way to get this working with an NVIDIA card is to change your objection detection to use onnx:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = detectors:&lt;br /&gt;
  onnx:&lt;br /&gt;
    type: onnx&lt;br /&gt;
model:&lt;br /&gt;
  model_type: yolonas&lt;br /&gt;
  width: 320&lt;br /&gt;
  height: 320&lt;br /&gt;
  input_pixel_format: bgr&lt;br /&gt;
  input_tensor: nchw&lt;br /&gt;
  path: /config/yolo_nas_s.onnx&lt;br /&gt;
  labelmap_path: /labelmap/coco-80.txt&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
To get the model, you will have to build and then download the model. This can be done using Google Colab: https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb&lt;br /&gt;
&lt;br /&gt;
Open the Jupyter notebook and then run all the cells. It takes a while for the prerequsites to build, but you should eventually be given the model file that you can plop into Frigate.&lt;br /&gt;
&lt;br /&gt;
==== TensorRT ====&lt;br /&gt;
&#039;&#039;&#039;This is no longer supported -- you must use ONNX going forward.&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
See the documentation on this at: https://docs.frigate.video/configuration/object_detectors/#nvidia-tensorrt-detector&lt;br /&gt;
&lt;br /&gt;
According to the official documentation, there are a long list of models that could be used. I find that the latest YOLO models work best, but they may run slower and require more GPU memory. The YOLOv4-tiny model works well as it is very fast and light on GPU memory, but it had trouble detecting cars in my garage. I eventually decided to go with the YOLOv7-320 model which is more accurate but slower which we&#039;ll set up now.&lt;br /&gt;
&lt;br /&gt;
First, we&#039;ll have to convert the YOLO models into a TensorRT model that works with our GPU. Keep in mind that the TensorRT &amp;lt;code&amp;gt;.trt&amp;lt;/code&amp;gt; model file has to be built on the same GPU that you will use for detection. We&#039;ll generate the model file with NVIDIA&#039;s tensorrt container image:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ## Store the output models and the tensorrt_demos repo&lt;br /&gt;
# mkdir trt-models tensorrt_demos&lt;br /&gt;
&lt;br /&gt;
## Launch the tensorrt container&lt;br /&gt;
# docker run --gpus=all -it -v `pwd`/trt-models:/tensorrt_models -v `pwd`/tensorrt_demos:/tensorrt_demos -v `pwd`/tensorrt_models.sh:/tensorrt_models.sh nvcr.io/nvidia/tensorrt:22.07-py3 bash&lt;br /&gt;
&lt;br /&gt;
## Download the tensorrt_models.sh script from Frigate and run it. This will download all the YOLO models and then generate the .trt model file.&lt;br /&gt;
## If you _don&#039;t_ want to download all the YOLO models, you&#039;ll have to interrupt the script and edit the &#039;download.yolo.sh&#039; &#039;script manually.&lt;br /&gt;
container# wget https://github.com/blakeblackshear/frigate/raw/master/docker/tensorrt_models.sh&lt;br /&gt;
container# YOLO_MODELS=&amp;quot;yolov7-320&amp;quot; bash tensorrt_models.sh&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
With the &amp;lt;code&amp;gt;yolov7-320.trt&amp;lt;/code&amp;gt; file generated, we&#039;ll configure the TensorRT based detector with the following configuration lines:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = detectors:&lt;br /&gt;
  path: /trt-models/yolov7x-320.trt&lt;br /&gt;
  width: 320&lt;br /&gt;
  height: 320&lt;br /&gt;
  input_tensor: nchw&lt;br /&gt;
  input_pixel_format: rgb&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;br /&gt;
If you are using a different model, you&#039;ll have to make sure the width/height sizes match what was used to train the model for best results.&lt;br /&gt;
&lt;br /&gt;
=== Tuning ===&lt;br /&gt;
If you&#039;re getting false positives, you may want to adjust the filter thresholds to help tune out the error rates. &lt;br /&gt;
&lt;br /&gt;
For example:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = cameras:&lt;br /&gt;
  front:&lt;br /&gt;
...&lt;br /&gt;
    detect:&lt;br /&gt;
      enabled: True&lt;br /&gt;
    objects:&lt;br /&gt;
      track:&lt;br /&gt;
        - person&lt;br /&gt;
        - bus&lt;br /&gt;
      filters:&lt;br /&gt;
        person:&lt;br /&gt;
          min_score: 0.6 # min score for object to initiate tracking (default: 0.5&lt;br /&gt;
          threshold: 0.8 # min decimal percentage for tracked object&#039;s computed score to be considered a true positive (default: 0.7)&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Alternatively, if you&#039;re using a GPU, you may want to swap out the TensorRT model for another one and see if that helps with the detection.&lt;br /&gt;
&lt;br /&gt;
You may also want to enable object masks to mask out areas that are causing the false positives.&lt;br /&gt;
&lt;br /&gt;
=== Go2rtc ===&lt;br /&gt;
go2rtc is a built-in service in the Frigate container that may be used to stream RTSP video from all your cameras. This way, your ffmpeg processes will stream from the internal go2rtc server rather than from the cameras directly. The added bonus with using this is that you may use the webrtc option in the Frigate web interface for higher FPS video streams.&lt;br /&gt;
&lt;br /&gt;
My configuration with my Wyze Cams running the RTSP beta firmware looks like this:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = go2rtc:&lt;br /&gt;
  streams:&lt;br /&gt;
    entrance: rtsp://admin:wyzecam@10.1.x.x:554/live&lt;br /&gt;
    backyard: rtsp://admin:wyzecam@10.1.x.x:554/live&lt;br /&gt;
    front: rtsp://admin:wyzecam@10.1.x.x:554/live&lt;br /&gt;
    garage: rtsp://admin:wyzecam@10.1.x.x:554/live&lt;br /&gt;
  webrtc:&lt;br /&gt;
    candidates:&lt;br /&gt;
      - 10.1.x.x:8555&lt;br /&gt;
      - stun:8555&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== General tips ==&lt;br /&gt;
Some tips I wished I knew earlier when using Frigate.&lt;br /&gt;
&lt;br /&gt;
* Use a GPU or Coral if possible. CPU is okay, but it uses a lot of CPU which in turn means higher power usage. A cheap Quadro is both faster and more efficient than a CPU. Plus, a GPU lets you use hardware acceleration on video encoding (with ffmpeg) which also reduces your CPU usage.&lt;br /&gt;
* Object detection only happens when Frigate detects motion (that is, changed pixels) in a frame. It then sends the portion of the frame that it detected motion on for object detection. As a result, using motion masks on areas you aren&#039;t interested in can reduce the amount of object detection being performed.&lt;br /&gt;
* If you want to save clips when a specific object appears (such as &#039;person&#039; appearing in the garage) while also wanting Frigate to also keep track of persistent objects but not record them (such as a &#039;car&#039; in the garage), the best approach I&#039;ve had is to create two cameras in Frigate: One for &#039;person&#039; with clip recording enabled and another for &#039;car&#039; with clip recording disabled.&lt;br /&gt;
** This was done so that I can count how many &#039;cars&#039; are in the garage for Home Assistant to act on. I do not want constant video clips of a stationary &#039;car&#039;, however.&lt;br /&gt;
* Enable go2rtc. You&#039;ll get higher framerates when viewing live video from the Frigate web interface which won&#039;t be limited to the objection detection fps.&lt;br /&gt;
* Enabling object detection will automatically have the camera report the objects in Home Assistant (also via MQTT). If you&#039;re monitoring for &#039;car&#039;, you&#039;ll get a &#039;car&#039; count in Home Assistant which you can automate on.&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== ffmpeg frequently OOMs ===&lt;br /&gt;
One of the camera&#039;s ffmpeg process periodically chews up all the memory assigned to the container and then gets OOM killed. This [https://github.com/blakeblackshear/frigate/issues/5924 issue seems to happen to others] as well. &lt;br /&gt;
&lt;br /&gt;
Things that I tried which did not help with the OOM are:&lt;br /&gt;
&lt;br /&gt;
* Disabled NVIDIA hwaccel flags to ffmpeg - ffmpeg continued to OOM periodically&lt;br /&gt;
* Updated go2rtc from 1.2.0 to 1.6.2 - no change&lt;br /&gt;
&lt;br /&gt;
What did stop the OOMs completely was to not have the frigate camera config use go2rtc but rather stream from the camera directly.&lt;br /&gt;
&lt;br /&gt;
Speculation: All the other cameras are all Wyze Cam v3 with the same firmware and they have no issues. The only difference would possibly be due to a weak Wi-Fi connection which is causing unexpected latency or corruption that&#039;s causing go2rtc and ffmpeg to go haywire?&lt;br /&gt;
&lt;br /&gt;
=== Frigate migration crashes ===&lt;br /&gt;
After my container image automatically updated, frigate now crashes on startup when it tries to do a migration.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = 2025-08-23 11:16:05.485451924  [2025-08-23 11:16:05] peewee_migrate.logs            INFO    : Starting migrations&lt;br /&gt;
2025-08-23 11:16:05.512447249  [2025-08-23 11:16:05] peewee_migrate.logs            INFO    : Migrate &amp;quot;030_create_user_review_status&amp;quot;&lt;br /&gt;
2025-08-23 11:16:05.512645584  [2025-08-23 11:16:05] peewee_migrate.logs            INFO    : sql (&#039;\n        CREATE TABLE IF NOT EXISTS &amp;quot;userreviewstatus&amp;quot; (\n            &amp;quot;id&amp;quot; INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n            &amp;quot;user_id&amp;quot; VARCHAR(30)&lt;br /&gt;
 NOT NULL,\n            &amp;quot;review_segment_id&amp;quot; VARCHAR(30) NOT NULL,\n            &amp;quot;has_been_reviewed&amp;quot; INTEGER NOT NULL DEFAULT 0,\n            FOREIGN KEY (&amp;quot;review_segment_id&amp;quot;) REFERENCES &amp;quot;reviewsegment&amp;quot; (&amp;quot;id&amp;quot;) ON DELETE CASCADE\n        )\n        &#039;,)&lt;br /&gt;
2025-08-23 11:16:05.513106209  [2025-08-23 11:16:05] peewee_migrate.logs            INFO    : sql (&#039;CREATE UNIQUE INDEX IF NOT EXISTS &amp;quot;userreviewstatus_user_segment&amp;quot; ON &amp;quot;userreviewstatus&amp;quot; (&amp;quot;user_id&amp;quot;, &amp;quot;review_segment_id&amp;quot;)&#039;,)&lt;br /&gt;
2025-08-23 11:16:05.513623581  [2025-08-23 11:16:05] peewee_migrate.logs            INFO    : Run &amp;lt;lambda&amp;gt;&lt;br /&gt;
2025-08-23 11:16:05.553604917  Traceback (most recent call last):&lt;br /&gt;
2025-08-23 11:16:05.553628185    File &amp;quot;/usr/local/lib/python3.11/dist-packages/peewee.py&amp;quot;, line 3322, in execute_sql&lt;br /&gt;
2025-08-23 11:16:05.553725739  [2025-08-23 11:16:05] peewee_migrate.logs            ERROR   : Migration failed: 030_create_user_review_status&lt;br /&gt;
2025-08-23 11:16:05.553728331  Traceback (most recent call last):&lt;br /&gt;
2025-08-23 11:16:05.553730349    File &amp;quot;/usr/local/lib/python3.11/dist-packages/peewee.py&amp;quot;, line 3322, in execute_sql&lt;br /&gt;
2025-08-23 11:16:05.553731736      cursor.execute(sql, params or ())&lt;br /&gt;
2025-08-23 11:16:05.553734113  pysqlite3.dbapi2.IntegrityError: UNIQUE constraint failed: userreviewstatus.user_id, userreviewstatus.review_segment_id&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
It appears that the update now creates a new userreviewstatus table that references reviewsegment...  review_segment_id has to be unique. Maybe something happened on my instance where there was a duplicate of review_segment_id and now the migration is failing.&lt;br /&gt;
&lt;br /&gt;
The fix here is to:&lt;br /&gt;
&lt;br /&gt;
# Stop frigate&lt;br /&gt;
# Edit the sqlite database: &amp;lt;code&amp;gt;sqlite3 frigate.db&amp;lt;/code&amp;gt;&lt;br /&gt;
# Delete the duplicate record in the reviewsegment table. I don&#039;t really care about the review feature, so I just run &amp;lt;code&amp;gt;delete from reviewsegment&amp;lt;/code&amp;gt;.&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Frigate&amp;diff=7708</id>
		<title>Frigate</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Frigate&amp;diff=7708"/>
		<updated>2025-08-23T20:49:51Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Frigate is a self hosted and open source video surveillance system.&lt;br /&gt;
&lt;br /&gt;
It is capable of:&lt;br /&gt;
&lt;br /&gt;
* streaming video from RTSP/RTMP&lt;br /&gt;
* performing real-time object detection or simply just motion detection&lt;br /&gt;
* keeping track of video clips and image snapshots with a retention policy&lt;br /&gt;
* MQTT integration for messaging events&lt;br /&gt;
&lt;br /&gt;
This page will contain some of my notes on using Frigate.&lt;br /&gt;
&lt;br /&gt;
== Setup ==&lt;br /&gt;
Frigate is distributed as a Docker image only and has to run as a Docker container. &lt;br /&gt;
&lt;br /&gt;
=== Object detection ===&lt;br /&gt;
The object detection system requires a detector set up. By default, Frigate will have a CPU detector enabled. This isn&#039;t recommended as the object detection uses a lot of CPU cycles while being slow. Frigate recommends using a USB Google Coral for this task, but  you may also choose to use a NVIDIA GPU with TensorRT (supported on post-Pascal GPUs only). &lt;br /&gt;
&lt;br /&gt;
Due to the chip shortage, Google Corals aren&#039;t easy to come by. The best alternative is to pick up a cheap NVIDIA Quadro GPU such as the P400. I got a second-hand P400 for around $80 CAD.&lt;br /&gt;
&lt;br /&gt;
==== TensorRT ====&lt;br /&gt;
See the documentation on this at: https://docs.frigate.video/configuration/object_detectors/#nvidia-tensorrt-detector&lt;br /&gt;
&lt;br /&gt;
According to the official documentation, there are a long list of models that could be used. I find that the latest YOLO models work best, but they may run slower and require more GPU memory. The YOLOv4-tiny model works well as it is very fast and light on GPU memory, but it had trouble detecting cars in my garage. I eventually decided to go with the YOLOv7-320 model which is more accurate but slower which we&#039;ll set up now.&lt;br /&gt;
&lt;br /&gt;
First, we&#039;ll have to convert the YOLO models into a TensorRT model that works with our GPU. Keep in mind that the TensorRT &amp;lt;code&amp;gt;.trt&amp;lt;/code&amp;gt; model file has to be built on the same GPU that you will use for detection. We&#039;ll generate the model file with NVIDIA&#039;s tensorrt container image:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ## Store the output models and the tensorrt_demos repo&lt;br /&gt;
# mkdir trt-models tensorrt_demos&lt;br /&gt;
&lt;br /&gt;
## Launch the tensorrt container&lt;br /&gt;
# docker run --gpus=all -it -v `pwd`/trt-models:/tensorrt_models -v `pwd`/tensorrt_demos:/tensorrt_demos -v `pwd`/tensorrt_models.sh:/tensorrt_models.sh nvcr.io/nvidia/tensorrt:22.07-py3 bash&lt;br /&gt;
&lt;br /&gt;
## Download the tensorrt_models.sh script from Frigate and run it. This will download all the YOLO models and then generate the .trt model file.&lt;br /&gt;
## If you _don&#039;t_ want to download all the YOLO models, you&#039;ll have to interrupt the script and edit the &#039;download.yolo.sh&#039; &#039;script manually.&lt;br /&gt;
container# wget https://github.com/blakeblackshear/frigate/raw/master/docker/tensorrt_models.sh&lt;br /&gt;
container# YOLO_MODELS=&amp;quot;yolov7-320&amp;quot; bash tensorrt_models.sh&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
With the &amp;lt;code&amp;gt;yolov7-320.trt&amp;lt;/code&amp;gt; file generated, we&#039;ll configure the TensorRT based detector with the following configuration lines:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = detectors:&lt;br /&gt;
  path: /trt-models/yolov7x-320.trt&lt;br /&gt;
  width: 320&lt;br /&gt;
  height: 320&lt;br /&gt;
  input_tensor: nchw&lt;br /&gt;
  input_pixel_format: rgb&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;br /&gt;
If you are using a different model, you&#039;ll have to make sure the width/height sizes match what was used to train the model for best results.&lt;br /&gt;
&lt;br /&gt;
=== Tuning ===&lt;br /&gt;
If you&#039;re getting false positives, you may want to adjust the filter thresholds to help tune out the error rates. &lt;br /&gt;
&lt;br /&gt;
For example:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = cameras:&lt;br /&gt;
  front:&lt;br /&gt;
...&lt;br /&gt;
    detect:&lt;br /&gt;
      enabled: True&lt;br /&gt;
    objects:&lt;br /&gt;
      track:&lt;br /&gt;
        - person&lt;br /&gt;
        - bus&lt;br /&gt;
      filters:&lt;br /&gt;
        person:&lt;br /&gt;
          min_score: 0.6 # min score for object to initiate tracking (default: 0.5&lt;br /&gt;
          threshold: 0.8 # min decimal percentage for tracked object&#039;s computed score to be considered a true positive (default: 0.7)&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Alternatively, if you&#039;re using a GPU, you may want to swap out the TensorRT model for another one and see if that helps with the detection.&lt;br /&gt;
&lt;br /&gt;
You may also want to enable object masks to mask out areas that are causing the false positives.&lt;br /&gt;
&lt;br /&gt;
=== Go2rtc ===&lt;br /&gt;
go2rtc is a built-in service in the Frigate container that may be used to stream RTSP video from all your cameras. This way, your ffmpeg processes will stream from the internal go2rtc server rather than from the cameras directly. The added bonus with using this is that you may use the webrtc option in the Frigate web interface for higher FPS video streams.&lt;br /&gt;
&lt;br /&gt;
My configuration with my Wyze Cams running the RTSP beta firmware looks like this:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = go2rtc:&lt;br /&gt;
  streams:&lt;br /&gt;
    entrance: rtsp://admin:wyzecam@10.1.x.x:554/live&lt;br /&gt;
    backyard: rtsp://admin:wyzecam@10.1.x.x:554/live&lt;br /&gt;
    front: rtsp://admin:wyzecam@10.1.x.x:554/live&lt;br /&gt;
    garage: rtsp://admin:wyzecam@10.1.x.x:554/live&lt;br /&gt;
  webrtc:&lt;br /&gt;
    candidates:&lt;br /&gt;
      - 10.1.x.x:8555&lt;br /&gt;
      - stun:8555&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== General tips ==&lt;br /&gt;
Some tips I wished I knew earlier when using Frigate.&lt;br /&gt;
&lt;br /&gt;
* Use a GPU or Coral if possible. CPU is okay, but it uses a lot of CPU which in turn means higher power usage. A cheap Quadro is both faster and more efficient than a CPU. Plus, a GPU lets you use hardware acceleration on video encoding (with ffmpeg) which also reduces your CPU usage.&lt;br /&gt;
* Object detection only happens when Frigate detects motion (that is, changed pixels) in a frame. It then sends the portion of the frame that it detected motion on for object detection. As a result, using motion masks on areas you aren&#039;t interested in can reduce the amount of object detection being performed.&lt;br /&gt;
* If you want to save clips when a specific object appears (such as &#039;person&#039; appearing in the garage) while also wanting Frigate to also keep track of persistent objects but not record them (such as a &#039;car&#039; in the garage), the best approach I&#039;ve had is to create two cameras in Frigate: One for &#039;person&#039; with clip recording enabled and another for &#039;car&#039; with clip recording disabled.&lt;br /&gt;
** This was done so that I can count how many &#039;cars&#039; are in the garage for Home Assistant to act on. I do not want constant video clips of a stationary &#039;car&#039;, however.&lt;br /&gt;
* Enable go2rtc. You&#039;ll get higher framerates when viewing live video from the Frigate web interface which won&#039;t be limited to the objection detection fps.&lt;br /&gt;
* Enabling object detection will automatically have the camera report the objects in Home Assistant (also via MQTT). If you&#039;re monitoring for &#039;car&#039;, you&#039;ll get a &#039;car&#039; count in Home Assistant which you can automate on.&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== ffmpeg frequently OOMs ===&lt;br /&gt;
One of the camera&#039;s ffmpeg process periodically chews up all the memory assigned to the container and then gets OOM killed. This [https://github.com/blakeblackshear/frigate/issues/5924 issue seems to happen to others] as well. &lt;br /&gt;
&lt;br /&gt;
Things that I tried which did not help with the OOM are:&lt;br /&gt;
&lt;br /&gt;
* Disabled NVIDIA hwaccel flags to ffmpeg - ffmpeg continued to OOM periodically&lt;br /&gt;
* Updated go2rtc from 1.2.0 to 1.6.2 - no change&lt;br /&gt;
&lt;br /&gt;
What did stop the OOMs completely was to not have the frigate camera config use go2rtc but rather stream from the camera directly.&lt;br /&gt;
&lt;br /&gt;
Speculation: All the other cameras are all Wyze Cam v3 with the same firmware and they have no issues. The only difference would possibly be due to a weak Wi-Fi connection which is causing unexpected latency or corruption that&#039;s causing go2rtc and ffmpeg to go haywire?&lt;br /&gt;
&lt;br /&gt;
=== Frigate migration crashes ===&lt;br /&gt;
After my container image automatically updated, frigate now crashes on startup when it tries to do a migration.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = 2025-08-23 11:16:05.485451924  [2025-08-23 11:16:05] peewee_migrate.logs            INFO    : Starting migrations&lt;br /&gt;
2025-08-23 11:16:05.512447249  [2025-08-23 11:16:05] peewee_migrate.logs            INFO    : Migrate &amp;quot;030_create_user_review_status&amp;quot;&lt;br /&gt;
2025-08-23 11:16:05.512645584  [2025-08-23 11:16:05] peewee_migrate.logs            INFO    : sql (&#039;\n        CREATE TABLE IF NOT EXISTS &amp;quot;userreviewstatus&amp;quot; (\n            &amp;quot;id&amp;quot; INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n            &amp;quot;user_id&amp;quot; VARCHAR(30)&lt;br /&gt;
 NOT NULL,\n            &amp;quot;review_segment_id&amp;quot; VARCHAR(30) NOT NULL,\n            &amp;quot;has_been_reviewed&amp;quot; INTEGER NOT NULL DEFAULT 0,\n            FOREIGN KEY (&amp;quot;review_segment_id&amp;quot;) REFERENCES &amp;quot;reviewsegment&amp;quot; (&amp;quot;id&amp;quot;) ON DELETE CASCADE\n        )\n        &#039;,)&lt;br /&gt;
2025-08-23 11:16:05.513106209  [2025-08-23 11:16:05] peewee_migrate.logs            INFO    : sql (&#039;CREATE UNIQUE INDEX IF NOT EXISTS &amp;quot;userreviewstatus_user_segment&amp;quot; ON &amp;quot;userreviewstatus&amp;quot; (&amp;quot;user_id&amp;quot;, &amp;quot;review_segment_id&amp;quot;)&#039;,)&lt;br /&gt;
2025-08-23 11:16:05.513623581  [2025-08-23 11:16:05] peewee_migrate.logs            INFO    : Run &amp;lt;lambda&amp;gt;&lt;br /&gt;
2025-08-23 11:16:05.553604917  Traceback (most recent call last):&lt;br /&gt;
2025-08-23 11:16:05.553628185    File &amp;quot;/usr/local/lib/python3.11/dist-packages/peewee.py&amp;quot;, line 3322, in execute_sql&lt;br /&gt;
2025-08-23 11:16:05.553725739  [2025-08-23 11:16:05] peewee_migrate.logs            ERROR   : Migration failed: 030_create_user_review_status&lt;br /&gt;
2025-08-23 11:16:05.553728331  Traceback (most recent call last):&lt;br /&gt;
2025-08-23 11:16:05.553730349    File &amp;quot;/usr/local/lib/python3.11/dist-packages/peewee.py&amp;quot;, line 3322, in execute_sql&lt;br /&gt;
2025-08-23 11:16:05.553731736      cursor.execute(sql, params or ())&lt;br /&gt;
2025-08-23 11:16:05.553734113  pysqlite3.dbapi2.IntegrityError: UNIQUE constraint failed: userreviewstatus.user_id, userreviewstatus.review_segment_id&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
It appears that the update now creates a new userreviewstatus table that references reviewsegment...  review_segment_id has to be unique. Maybe something happened on my instance where there was a duplicate of review_segment_id and now the migration is failing.&lt;br /&gt;
&lt;br /&gt;
The fix here is to:&lt;br /&gt;
&lt;br /&gt;
# Stop frigate&lt;br /&gt;
# Edit the sqlite database: &amp;lt;code&amp;gt;sqlite3 frigate.db&amp;lt;/code&amp;gt;&lt;br /&gt;
# Delete the duplicate record in the reviewsegment table. I don&#039;t really care about the review feature, so I just run &amp;lt;code&amp;gt;delete from reviewsegment&amp;lt;/code&amp;gt;.&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=MediaWiki&amp;diff=7707</id>
		<title>MediaWiki</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=MediaWiki&amp;diff=7707"/>
		<updated>2025-08-18T23:57:03Z</updated>

		<summary type="html">&lt;p&gt;Leo: /* Mariadb 12 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;MediaWiki is an opensource wiki engine written in PHP by the Wikimedia Foundation. It is used by both Wikipedia and this site.&lt;br /&gt;
&lt;br /&gt;
Visit the project website at:&lt;br /&gt;
&lt;br /&gt;
*https://www.mediawiki.org&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Running inside Docker==&lt;br /&gt;
You can run MediaWiki from a Docker container. A proof of concept can be found at:&lt;br /&gt;
&lt;br /&gt;
*https://git.steamr.com/docker/mediawiki&lt;br /&gt;
&lt;br /&gt;
In order to make a single MediaWiki container image which can be used regardless of custom skins or extensions, I intentionally separated custom skins and extensions into a separate directory. When the container first starts, a setup script will symlink these additional extensions and skins to the primary directory. Custom extensions and skins need to be placed in the extension and skins volume and then added to the LocalSettings.php configuration file. When adding a new extension or skin, you must restart the container for these changes to take effect. &lt;br /&gt;
&lt;br /&gt;
To get started with my image, create the container with volumes for:&lt;br /&gt;
&lt;br /&gt;
#Images / Uploads as /mediawiki/images&lt;br /&gt;
#Extensions as /extensions&lt;br /&gt;
#Skins as /skins&lt;br /&gt;
#The {{code|LocalSettings.php}} configuration file under /config&lt;br /&gt;
#Database (on a remote server or container), or a SQLite file on a volume&lt;br /&gt;
&lt;br /&gt;
An example docker-compose configuration running a wiki:&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| lang = yaml&lt;br /&gt;
| code = wiki:&lt;br /&gt;
    image: registry.steamr.com/docker/mediawiki:1.34&lt;br /&gt;
    networks:&lt;br /&gt;
      - traefik&lt;br /&gt;
      - db-net&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;8888&amp;quot;&lt;br /&gt;
    environment:&lt;br /&gt;
      - VIRTUAL_HOST=wiki.steamr.com&lt;br /&gt;
      - APP_ROOT=/mediawiki&lt;br /&gt;
      - DOCUMENT_ROOT=/mediawiki&lt;br /&gt;
      - DB_HOST=db&lt;br /&gt;
      - DB_DATABASE=wiki&lt;br /&gt;
      - DB_USERNAME=wiki&lt;br /&gt;
      - DB_PASSWORD=wiki&lt;br /&gt;
    restart: always&lt;br /&gt;
    volumes:&lt;br /&gt;
      - /var/volumes/wiki/config:/config&lt;br /&gt;
      - /var/volumes/wiki/extensions:/extensions&lt;br /&gt;
      - /var/volumes/wiki/skins:/skins&lt;br /&gt;
      - /var/volumes/wiki/images:/mediawiki/images&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==Configuration==&lt;br /&gt;
MediaWiki only has a single configuration file at {{code|LocalSettings.php}}.&lt;br /&gt;
&lt;br /&gt;
If you wish to run a wiki on the root of a domain, you need to set &amp;lt;code&amp;gt;$wgScriptPath&amp;lt;/code&amp;gt; empty like so:&lt;br /&gt;
{{highlight&lt;br /&gt;
| lang = php&lt;br /&gt;
| code = $wgScriptPath = &amp;quot;&amp;quot;;&lt;br /&gt;
$wgArticlePath = &amp;quot;/$1&amp;quot;;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
===Extensions===&lt;br /&gt;
Extensions are placed under the {{code|/extensions}} directory. Common extensions are bundled with the base installation of MediaWiki but not enabled by default. Extentions that are bundled can be enabled by adding a {{code|wfLoadExtention(&#039;extention&#039;)}} call to the {{code|LocalSettings.php}} file.&lt;br /&gt;
&lt;br /&gt;
There are some extensions that this wiki requires:&lt;br /&gt;
&lt;br /&gt;
;Intersection&lt;br /&gt;
:https://github.com/wikimedia/mediawiki-extensions-intersection.git&lt;br /&gt;
:Generates dynamic lists&lt;br /&gt;
;Scribunto&lt;br /&gt;
:https://github.com/wikimedia/mediawiki-extensions-Scribunto.git&lt;br /&gt;
:Generates scripted outputs using Lua&lt;br /&gt;
;Math&lt;br /&gt;
:https://github.com/wikimedia/mediawiki-extensions-Math&lt;br /&gt;
:Generates math formulas&lt;br /&gt;
;NativeSvgHandler&lt;br /&gt;
:https://github.com/wikimedia/mediawiki-extensions-NativeSvgHandler.git&lt;br /&gt;
:Embeds SVG files as an image for client-side rendering. Requires appending {{code|http://www.w3.org/tr/rec-rdf-syntax/}} to {{code|$validNamespaces}} in {{code|UploadBase.php}}.&lt;br /&gt;
;WikiSEO&lt;br /&gt;
:https://github.com/octfx/wiki-seo/&lt;br /&gt;
:Enables custom SEO meta keyword and description tags on the wiki.&lt;br /&gt;
;Score&lt;br /&gt;
:https://www.mediawiki.org/wiki/Extension:Score&lt;br /&gt;
: Lets you embed LilyPond music notation into your Wiki&lt;br /&gt;
&lt;br /&gt;
===Skins===&lt;br /&gt;
Skins are placed under the {{code|/skins}} directory. Modern skins are loaded with the {{code|wfLoadSkin(&#039;skin&#039;)}} call in {{code|LocalSettings.php}}, which reads the skin&#039;s {{code|skin.json}} manifest file in the skin&#039;s directory. The manifest file contains the skin&#039;s name, autoload class files, as well as which resource modules to load loaded, including the stylesheets and javascript files that are part of the skin.&lt;br /&gt;
&lt;br /&gt;
MediaWiki&#039;s guide on skinning is relatively up to date albeit a little confusing to understand at first.&lt;br /&gt;
&lt;br /&gt;
*https://www.mediawiki.org/wiki/Manual:Skinning_Part_2&lt;br /&gt;
&lt;br /&gt;
The Reader skin used on this wiki:&lt;br /&gt;
&lt;br /&gt;
*https://git.steamr.com/leo/mediawiki-reader-skin&lt;br /&gt;
&lt;br /&gt;
The {{code|OutputPage}} object is the thing that handles all HTML generation as well as linking javascript and CSS modules.  You can use this to inject HTML or certain code to the page with some method calls. See: https://www.mediawiki.org/wiki/Manual:OutputPage.php&lt;br /&gt;
&lt;br /&gt;
===Transcluded Pages===&lt;br /&gt;
There are some pages that are used by the MediaWiki software itself, including:&lt;br /&gt;
&lt;br /&gt;
*[[MediaWiki:Aboutsite]]&lt;br /&gt;
*[[MediaWiki:Disclaimers]]&lt;br /&gt;
*[[MediaWiki:Privacy]]&lt;br /&gt;
*[[MediaWiki:Toolbox]]&lt;br /&gt;
*[[MediaWiki:Sidebar]]&lt;br /&gt;
&lt;br /&gt;
Some resources are also loaded from pages including:&lt;br /&gt;
&lt;br /&gt;
*[[MediaWiki:Geshi.css]]&lt;br /&gt;
*[[MediaWiki:Common.css]]&lt;br /&gt;
*[[MediaWiki:Common.js]]&lt;br /&gt;
&lt;br /&gt;
Skins should be able to handle contents on these MediaWiki pages which are typically shown somewhere on the page. Of course, custom skins can also reference their own set of pages such as the bootstrap/reader skin I&#039;m currently using:&lt;br /&gt;
&lt;br /&gt;
*[[Bootstrap:Footer]]&lt;br /&gt;
*[[Bootstrap:Sidebar]]&lt;br /&gt;
*[[Bootstrap:Jumbotron]]&lt;br /&gt;
&lt;br /&gt;
==Tasks==&lt;br /&gt;
&lt;br /&gt;
===Enabling Visual Editor===&lt;br /&gt;
The Visual Editor has been included out of the box since MediaWiki 1.35. &#039;&#039;&#039;The following steps are no longer required.&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Visual Editor is an extension that enables the WYSIWYG editor. This extension requires Parsoid in order to properly save changes. &lt;br /&gt;
&lt;br /&gt;
Installing the Visual Editor extension is simple:&lt;br /&gt;
&lt;br /&gt;
#Download the extension to the extensions directory {{highlight&lt;br /&gt;
| lang = text&lt;br /&gt;
| code = $ cd extensions&lt;br /&gt;
$ wget https://extdist.wmflabs.org/dist/extensions/VisualEditor-REL1_34-74116a7.tar.gz&lt;br /&gt;
$ tar -xzf VisualEditor*gz&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
#Edit LocalSettings.php and enable the extension. {{Highlight&lt;br /&gt;
| lang = php&lt;br /&gt;
| code = wfLoadExtension(&#039;VisualEditor&#039;);&lt;br /&gt;
$wgDefaultUserOptions[&#039;visualeditor-enable&#039;] = 1;&lt;br /&gt;
$wgVirtualRestConfig[&#039;modules&#039;][&#039;parsoid&#039;] = array(&lt;br /&gt;
    // URL to the Parsoid instance&lt;br /&gt;
    // Use port 8142 if you use the Debian package&lt;br /&gt;
    &#039;url&#039; =&amp;gt; &#039;http://parsoid:8000&#039;,&lt;br /&gt;
    // Parsoid &amp;quot;domain&amp;quot;, see below (optional)&lt;br /&gt;
    &#039;domain&#039; =&amp;gt; &#039;wiki&#039;,&lt;br /&gt;
    # // Parsoid &amp;quot;prefix&amp;quot;, see below (optional)&lt;br /&gt;
    # &#039;prefix&#039; =&amp;gt; &#039;localhost&#039;&lt;br /&gt;
);&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Getting Parsoid running is slightly trickier.  I recommend running Parsoid in a docker container to simplify installation. There is one built at thenets/parsoid which works just fine. A clone of that project is available at https://git.steamr.com/docker/parsoid. The container takes in an environment variables to configure which domains Parsoid is to work for. An example docker-compose file with everything working is given below.&lt;br /&gt;
&lt;br /&gt;
{{highlight&lt;br /&gt;
| lang = yaml&lt;br /&gt;
| code = version: &#039;3.3&#039;&lt;br /&gt;
services:&lt;br /&gt;
  wiki:&lt;br /&gt;
    image: registry.steamr.com/docker/mediawiki:1.34&lt;br /&gt;
    labels:&lt;br /&gt;
      - &amp;quot;traefik.enable=true&amp;quot;&lt;br /&gt;
      - &amp;quot;traefik.port=8888&amp;quot;&lt;br /&gt;
      - &amp;quot;traefik.docker.network=traefik&amp;quot;&lt;br /&gt;
      - &amp;quot;traefik.frontend.rule=Host:wiki.steamr.com&amp;quot;&lt;br /&gt;
    networks:&lt;br /&gt;
      - traefik&lt;br /&gt;
      - db-net&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;8888&amp;quot;&lt;br /&gt;
    environment:&lt;br /&gt;
      - VIRTUAL_HOST=wiki.home.steamr.com&lt;br /&gt;
      - APP_ROOT=/mediawiki&lt;br /&gt;
      - DOCUMENT_ROOT=/mediawiki&lt;br /&gt;
      - DB_HOST=db&lt;br /&gt;
      - DB_DATABASE=wiki&lt;br /&gt;
      - DB_USERNAME=wiki&lt;br /&gt;
      - DB_PASSWORD=wiki&lt;br /&gt;
    restart: always&lt;br /&gt;
    volumes:&lt;br /&gt;
      - /var/volumes/wiki/config:/config&lt;br /&gt;
      - /var/volumes/wiki/extensions:/extensions&lt;br /&gt;
      - /var/volumes/wiki/skins:/skins&lt;br /&gt;
      - /var/volumes/wiki/images:/mediawiki/images&lt;br /&gt;
&lt;br /&gt;
  parsoid:&lt;br /&gt;
    image: registry.steamr.com/docker/parsoid:latest&lt;br /&gt;
    labels:&lt;br /&gt;
      - &amp;quot;traefik.enable=true&amp;quot;&lt;br /&gt;
      - &amp;quot;traefik.port=8000&amp;quot;&lt;br /&gt;
      - &amp;quot;traefik.docker.network=traefik&amp;quot;&lt;br /&gt;
      - &amp;quot;traefik.frontend.rule=Host:parsoid.steamr.com&amp;quot;&lt;br /&gt;
    environment:&lt;br /&gt;
      - PARSOID_DOMAIN_wiki=http://wiki:8888/api.php&lt;br /&gt;
    networks:&lt;br /&gt;
      - traefik&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;8000&amp;quot;&lt;br /&gt;
    restart: always&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
The Visual Editor should be functional at this point. If the editor does not start without any visual messages, check the javascript console for error messages.  There are certain skin requirements that need to be met in order for the Visual Editor to work outlined at https://www.mediawiki.org/wiki/VisualEditor/Skin_requirements.&lt;br /&gt;
&lt;br /&gt;
===Inserting a custom script in {{code|&amp;lt;head&amp;gt;}}===&lt;br /&gt;
If using a custom skin, use the {{code|OutputPage}} and call {{code|addHeadItem(&#039;name&#039;, &#039;&amp;lt;script&amp;gt;...&amp;lt;/script&#039;)}} to inject a custom script block within the document head.&lt;br /&gt;
&lt;br /&gt;
Alternatively, write a specific OutputPageBeforeHTML hook, and from there call addInlineScript.&lt;br /&gt;
&lt;br /&gt;
===Adding a custom button to WikiEditor Toolbar===&lt;br /&gt;
To add a custom button to the WikiEditor toolbar next to the existing bold and italic buttons, edit the {{code|MediaWiki:Common.js}} file. This file can only be changed by {{code|interface administrator}} users.  Membership to this group can be assigned at [[Special:UserRights/username]].&lt;br /&gt;
&lt;br /&gt;
The {{code|Common.js}} file used to add the Code and SyntaxHighlight templates used on this wiki is given below.&lt;br /&gt;
&lt;br /&gt;
{{highlight&lt;br /&gt;
| lang = js&lt;br /&gt;
| code = var customizeToolbar = function () {&lt;br /&gt;
	$(&#039;#wpTextbox1&#039;).wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
		&#039;section&#039;: &#039;main&#039;,&lt;br /&gt;
		&#039;group&#039;: &#039;format&#039;,&lt;br /&gt;
		&#039;tools&#039;: {&lt;br /&gt;
			&#039;code&#039;: {&lt;br /&gt;
				label: &#039;code&#039;,&lt;br /&gt;
				type: &#039;button&#039;,&lt;br /&gt;
				oouiIcon: &#039;code&#039;,&lt;br /&gt;
				action: {&lt;br /&gt;
					type: &#039;encapsulate&#039;,&lt;br /&gt;
					options: {&lt;br /&gt;
						pre: &amp;quot;{{code|&amp;quot;,&lt;br /&gt;
						post: &amp;quot;}}&amp;quot;&lt;br /&gt;
					}&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
	});&lt;br /&gt;
	$(&#039;#wpTextbox1&#039;).wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
		&#039;section&#039;: &#039;main&#039;,&lt;br /&gt;
		&#039;group&#039;: &#039;format&#039;,&lt;br /&gt;
		&#039;tools&#039;: {&lt;br /&gt;
			&#039;terminal&#039;: {&lt;br /&gt;
				label: &#039;highlight&#039;,&lt;br /&gt;
				type: &#039;button&#039;,&lt;br /&gt;
				oouiIcon: &#039;tag&#039;,&lt;br /&gt;
				action: {&lt;br /&gt;
					type: &#039;encapsulate&#039;,&lt;br /&gt;
					options: {&lt;br /&gt;
						pre: &amp;quot;&amp;lt;nowiki&amp;gt;{{&amp;lt;/nowiki&amp;gt;highlight{{!}}lang=terminal{{!}}code=\n&amp;quot;,&lt;br /&gt;
&amp;lt;nowiki&amp;gt;						post: &amp;quot;}}&amp;quot;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
					}&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
	});&lt;br /&gt;
};&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Documentation for this can be found at:&lt;br /&gt;
&lt;br /&gt;
*https://www.mediawiki.org/wiki/Extension:WikiEditor/Toolbar_customization#Basic_setup&lt;br /&gt;
&lt;br /&gt;
===Remove the &#039;Retrieved from&#039; footer message===&lt;br /&gt;
There are a few ways to hide the &#039;Retrieved from&#039; message that appears at the end of every article.&lt;br /&gt;
&lt;br /&gt;
#Edit the [[MediaWiki:Retrievedfrom]] page with an Administrator account. Comment out or remove the contents to suppress the message.&lt;br /&gt;
#Hide the content with CSS by adding to [[MediaWiki:Common.css]] the following rule: {{highlight&lt;br /&gt;
| lang = css&lt;br /&gt;
| code = /* hide the &amp;quot;Retrieved from&amp;quot; message */&lt;br /&gt;
.printfooter { display: none; &amp;lt;nowiki&amp;gt;}&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
}}&lt;br /&gt;
#Edit the template that your skin is using. It&#039;ll look something like: {{highlight&lt;br /&gt;
| lang = text&lt;br /&gt;
| code = {{#html-printfooter}}&amp;lt;div class=&amp;quot;printfooter&amp;quot;&amp;gt;{{{html-printfooter}}}&amp;lt;/div&amp;gt;{{/html-printfooter}}&lt;br /&gt;
}}. Either remove, or comment out the line using mustache &amp;lt;code&amp;gt;{{! ... }}&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===How to promote a user into a sysop, bureaucrat, or interface-admin group===&lt;br /&gt;
Certain actions on the Wiki are restricted to specific user groups. The default permissions are listed at https://www.mediawiki.org/wiki/Manual:User_rights. To add a user to a specific group:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = $ mysql -u $User -p$Password $Database&lt;br /&gt;
&lt;br /&gt;
## In MySQL prompt, determine the user&#039;s user_id&lt;br /&gt;
MariaDB [wiki]&amp;gt;&amp;lt;nowiki&amp;gt; SELECT user_id, user_name FROM user WHERE user_name = &#039;leo&#039;&lt;br /&gt;
+---------+-------------&lt;br /&gt;
| user_id | user_name&lt;br /&gt;
|       1 | Leo             &lt;br /&gt;
+---------+-------------&lt;br /&gt;
&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
## Add the user into the desired group using the user_id above.&lt;br /&gt;
MariaDB [wiki]&amp;gt; INSERT INTO `user_groups` VALUES (1, &#039;sysop&#039;), (1, &#039;bureaucrat&#039;), (1, &#039;interface-admin&#039;);&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Running the Docker container under a different prefix using Traefik ===&lt;br /&gt;
The official Docker image as well as my customized version based off of it expects the Wiki to be served from the domain root. However, if you want to serve the Wiki from a &#039;subdirectory&#039; like &amp;lt;code&amp;gt;/wiki&amp;lt;/code&amp;gt;, you need to do a few things:&lt;br /&gt;
&lt;br /&gt;
#In Traefik, your router should have the following rules: &amp;lt;code&amp;gt;traefik.http.routers.wiki-https.rule=(Host(`my-wiki-server.tld`) &amp;amp;&amp;amp; PathPrefix(`/wiki`))&amp;lt;/code&amp;gt;.&lt;br /&gt;
#In your Docker container, you need to symlink the &amp;lt;code&amp;gt;/wiki&amp;lt;/code&amp;gt; directory to &amp;lt;code&amp;gt;/var/www/html&amp;lt;/code&amp;gt;. &amp;lt;code&amp;gt;ln -s /var/www/html /var/www/html/wiki&amp;lt;/code&amp;gt;&lt;br /&gt;
#In your MediaWiki &amp;lt;code&amp;gt;LocalSettings.php&amp;lt;/code&amp;gt; configuration, you need to:&lt;br /&gt;
##Set &amp;lt;code&amp;gt;$wgVirtualRestConfig[&#039;modules&#039;][&#039;parsoid&#039;] = [&#039;url&#039; =&amp;gt; &#039;&amp;lt;nowiki&amp;gt;http://localhost:8080/wiki/rest.php&#039;&amp;lt;/nowiki&amp;gt;];&amp;lt;/code&amp;gt; so that Parsoid continues to work.&lt;br /&gt;
##Set &amp;lt;code&amp;gt;$wgScriptPath = &amp;quot;/wiki&amp;quot;;&amp;lt;/code&amp;gt;&lt;br /&gt;
## Set &amp;lt;code&amp;gt;$wgArticlePath = &amp;quot;/wiki/$1&amp;quot;;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Running MediaWiki behind a reverse proxy===&lt;br /&gt;
When running MediaWiki behind a reverse proxy like squid, nginx, or Traefik, edits made by users will appear as originating from reverse proxy server. &lt;br /&gt;
&lt;br /&gt;
To fix this issue, you need to tell MediaWiki which address ranges are from the reverse proxy and to have it use the X-Forwarded-For header instead. Since MediaWiki 1.35, this can be accomplished by adding [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgUsePrivateIPs $wgUsePrivateIPs] and [https://www.mediawiki.org/wiki/Manual:$wgCdnServersNoPurge $wgCdnServersNoPurge] in your &amp;lt;code&amp;gt;LocalSettings.php&amp;lt;/code&amp;gt; file:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = $wgUsePrivateIPs = true;&lt;br /&gt;
$wgUseCdn = true;&lt;br /&gt;
$wgCdnServersNoPurge = [];&lt;br /&gt;
$wgCdnServersNoPurge[] = &amp;quot;172.18.0.0/16&amp;quot;;&lt;br /&gt;
| lang = php&lt;br /&gt;
}}&lt;br /&gt;
Replace 172.18.0.0/16 with the CIDR of your reverse proxy networks (this CIDR is my internal Traefik network for my Docker stack).&lt;br /&gt;
&lt;br /&gt;
=== Adding the Score extension ===&lt;br /&gt;
The Score extension allows the rendering of music notation using the Lilypond project as the renderer. I ran into a series of hurdles getting this to work for MediaWiki 1.39.&lt;br /&gt;
&lt;br /&gt;
Issues are: &lt;br /&gt;
&lt;br /&gt;
#The extention highly recommends the use of ShellBox to isolate Lilypond because Lilypond&#039;s insecure and may allow remote execution. Since this Wiki is editable to the public, this has to be done. You&#039;ll have to set up a ShellBox instance (as a separate container). MediaWiki/Wikipedia has their own container image which doesn&#039;t work outside their infrastructure so I had to make my own.&lt;br /&gt;
# Lilypond has issues and breaks in my environment. I had to:&lt;br /&gt;
#*Modify &amp;lt;code&amp;gt;/usr/bin/lilypond&amp;lt;/code&amp;gt; to include &amp;lt;code&amp;gt;export PATH=&amp;quot;/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&amp;quot;&amp;amp;nbsp;&amp;lt;/code&amp;gt;otherwise it will fail silently in the extension&#039;s script. If you enable verbose mode on lilypond, you will see &amp;lt;code&amp;gt;warning: g_spawn_sync failed (0): gs: Failed to execute child process “gs” (No such file or directory)&amp;lt;/code&amp;gt;.&lt;br /&gt;
#*Symlink the lilypond fonts because on Debian Bullseye, the package versions for Lilypond and Lilypond-fonts are mismatched so the paths are broken (What the heck Debian?)&lt;br /&gt;
#*I tried to enable SVG which works after some tweaking to the generator script, but is a full page in size and can&#039;t be easily trimmed.&lt;br /&gt;
#The Score extension&#039;s included shell script is questionable...&lt;br /&gt;
#* The fuzzy png image apparently is from the ghostscript .ps to .png conversion. I&#039;m not sure why this is even done separately when lilypond also generates the pngs. I hacked this by copying &#039;file.png&#039; over &#039;file-page1.png&#039; and commented out the &amp;lt;code&amp;gt;runGhostscript&amp;lt;/code&amp;gt; function call in the &amp;lt;code&amp;gt;generatePngAndMidi.sh&amp;lt;/code&amp;gt; script. That seems to make the png not shrink down and be fuzzy.&lt;br /&gt;
After overcoming all the aforementioned issues, I think I&#039;ve finally got it working.&lt;br /&gt;
&lt;br /&gt;
* Use the following Dockerfiles&lt;br /&gt;
*Add the following docker-compose entries&lt;br /&gt;
*In the volume, run: {{Highlight&lt;br /&gt;
| code = $ git clone https://gerrit.wikimedia.org/r/mediawiki/libs/Shellbox shellbox&lt;br /&gt;
&lt;br /&gt;
## In the fpm container, run as the shellbox user:&lt;br /&gt;
# su shellbox&lt;br /&gt;
$ cd /srv/shellbox&lt;br /&gt;
$ /usr/local/bin/composer install --no-dev&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
*Enable the plugin:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = wfLoadExtension( &#039;Score&#039; );&lt;br /&gt;
$wgScoreTrim = true;&lt;br /&gt;
$wgScoreUseSvg = false;&lt;br /&gt;
$wgShellboxUrl = &#039;http://shellbox/shellbox&#039;;&lt;br /&gt;
$wgShellboxSecretKey = &#039;secret_key&#039;;&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
* You have to tweak [[MediaWiki:Common.css]] so that the embedded images aren&#039;t too big. I made them 4em max-height as I intend to only embed single staff snippets for my notes.&lt;br /&gt;
&lt;br /&gt;
==Troubleshooting==&lt;br /&gt;
&lt;br /&gt;
===Scribunto Lua Failures===&lt;br /&gt;
If templates cause this error:&lt;br /&gt;
{{highlight&lt;br /&gt;
| lang = text&lt;br /&gt;
| code = Lua error: Internal error: The interpreter exited with status 127.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
This likely means that you do not have Lua installed or it is not in the PATH. You will need to specify the Lua path in {{code|LocalSettings.php}} with this line:&lt;br /&gt;
{{highlight&lt;br /&gt;
| lang = php&lt;br /&gt;
| code = $wgScribuntoEngineConf[&#039;luastandalone&#039;][&#039;luaPath&#039;] = &amp;quot;/usr/bin/lua5.1&amp;quot;;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
===Database Import Incomplete ===&lt;br /&gt;
Database imports from MySQL 5.7.27 to a MariaDB 10.4.7 seems to fail. Imports only appear to complete if the database dump was made without {{code|Enclose export in a transaction}} enabled in PHPMyAdmin but subsequent edits on the destination wiki will result in this error message:&lt;br /&gt;
{{highlight&lt;br /&gt;
| lang = text&lt;br /&gt;
| code = The revision #0 of the page named &amp;quot;some-article&amp;quot; does not exist.&lt;br /&gt;
&lt;br /&gt;
This is usually caused by following an outdated history link to a page that has been deleted. Details can be found in the deletion log.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Solution====&lt;br /&gt;
It turns out the destination database server (mariadb:10.4.7, in docker) was not set up properly after being upgraded from MariaDB-10.1. On start up, it showed the following error messages:&lt;br /&gt;
{{highlight&lt;br /&gt;
| lang = text&lt;br /&gt;
| code = 2019-09-01 21:14:44 0 [Note] Server socket created on IP: &#039;::&#039;.&lt;br /&gt;
2019-09-01 21:14:44 0 [Warning] &#039;user&#039; entry &#039;root@localhost.localdomain&#039; ignored in --skip-name-resolve mode.&lt;br /&gt;
2019-09-01 21:14:44 0 [Warning] &#039;proxies_priv&#039; entry &#039;@% root@localhost.localdomain&#039; ignored in --skip-name-resolve mode.&lt;br /&gt;
2019-09-01 21:14:44 0 [ERROR] Missing system table mysql.roles_mapping; please run mysql_upgrade to create it&lt;br /&gt;
2019-09-01 21:14:44 0 [ERROR] Incorrect definition of table mysql.event: expected column &#039;sql_mode&#039; at position 14 to have type set(&#039;REAL_AS_FLOAT&#039;,&#039;PIPES_AS_CONCAT&#039;,&#039;ANSI_QUOTES&#039;,&#039;IGNORE_SPACE&#039;,&#039;IGNORE_BAD_TABLE_OPTIONS&#039;,&#039;ONLY_FULL_GROUP_BY&#039;,&#039;NO_UNSIGNED_SUBTRACTION&#039;,&#039;NO_DIR_IN_CREATE&#039;,&#039;POSTGRESQL&#039;,&#039;ORACLE&#039;,&#039;MSSQL&#039;,&#039;DB2&#039;,&#039;MAXDB&#039;,&#039;NO_KEY_OPTIONS&#039;,&#039;NO_TABLE_OPTIONS&#039;,&#039;NO_FIELD_OPTIONS&#039;,&#039;MYSQL323&#039;,&#039;MYSQL40&#039;,&#039;ANSI&#039;,&#039;NO_AUTO_VALUE_ON_ZERO&#039;,&#039;NO_BACKSLASH_ESCAPES&#039;,&#039;STRICT_TRANS_TABLES&#039;,&#039;STRICT_ALL_TABLES&#039;,&#039;NO_ZERO_IN_DATE&#039;,&#039;NO_ZERO_DATE&#039;,&#039;INVALID_DATES&#039;,&#039;ERROR_FOR_DIVISION_BY_ZERO&#039;,&#039;TRADITIONAL&#039;,&#039;NO_AUTO_CREATE_USER&#039;,&#039;HIGH_NOT_PRECEDENCE&#039;,&#039;NO_ENGINE_SUBSTITUTION&#039;,&#039;PAD_CHAR_TO_FULL_LENGTH&#039;,&#039;EMPTY_STRING_IS_NULL&#039;,&#039;SIMULTANEOUS_ASSIGNMENT&#039;), found type set(&#039;REAL_AS_FLOAT&#039;,&#039;PIPES_AS_CONCAT&#039;,&#039;ANSI_QUOTES&#039;,&#039;IGNORE_SPACE&#039;,&#039;IGNORE_BAD_TABLE_OPTIONS&#039;,&#039;ONLY_FULL_GROUP_BY&#039;,&#039;NO_UNSIGNED_SUBTRACTION&#039;,&#039;NO_DIR_IN_CREATE&#039;,&#039;POSTGRESQL&#039;,&#039;ORACLE&#039;,&#039;MSSQL&#039;,&#039;DB2&#039;,&#039;MAXDB&#039;,&#039;NO_KEY_OPTIONS&#039;,&#039;NO_TABLE_OPTIONS&#039;,&#039;NO_FIELD_OPTIONS&#039;,&#039;MYSQL323&#039;,&#039;MYSQL40&#039;,&#039;ANSI&#039;,&#039;NO_AUTO_VALU&lt;br /&gt;
2019-09-01 21:14:44 0 [ERROR] mysqld: Event Scheduler: An error occurred when initializing system tables. Disabling the Event Scheduler.&lt;br /&gt;
2019-09-01 21:14:44 6 [Warning] Failed to load slave replication state from table mysql.gtid_slave_pos: 1146: Table &#039;mysql.gtid_slave_pos&#039; doesn&#039;t exist&lt;br /&gt;
2019-09-01 21:14:44 0 [Note] Reading of all Master_info entries succeeded&lt;br /&gt;
2019-09-01 21:14:44 0 [Note] Added new Master_info &#039;&#039; to hash table&lt;br /&gt;
2019-09-01 21:14:44 0 [Note] mysqld: ready for connections.&lt;br /&gt;
Version: &#039;10.4.7-MariaDB-1:10.4.7+maria~bionic&#039;  socket: &#039;/var/run/mysqld/mysqld.sock&#039;  port: 3306  mariadb.org binary distribution&lt;br /&gt;
2019-09-01 21:14:46 0 [Note] InnoDB: Buffer pool(s) load completed at 190901 21:14:46&lt;br /&gt;
2019-09-01 21:14:49 8 [ERROR] InnoDB: Table `mysql`.`innodb_table_stats` not found.&lt;br /&gt;
2019-09-01 21:14:49 8 [ERROR] Transaction not registered for MariaDB 2PC, but transaction is active&lt;br /&gt;
2019-09-01 21:15:13 9 [ERROR] Transaction not registered for MariaDB 2PC, but transaction is active&lt;br /&gt;
2019-09-01 21:15:13 9 [ERROR] Transaction not registered for MariaDB 2PC, but transaction is active&lt;br /&gt;
2019-09-01 21:15:13 9 [ERROR] Transaction not registered for MariaDB 2PC, but transaction is active&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Running {{code|mysql_upgrade}} fixed these errors and a subsequent database import was successful.&lt;br /&gt;
&lt;br /&gt;
===Math Extension===&lt;br /&gt;
Using the latest Math extension, formulas constantly return errors similar to:&lt;br /&gt;
{{highlight&lt;br /&gt;
| lang = text&lt;br /&gt;
| code = Failed to parse (MathML with SVG or PNG fallback (recommended for modern browsers and accessibility tools): Invalid response (&amp;quot;Math extension cannot connect to Restbase.&amp;quot;) from server &amp;quot;https://wikimedia.org/api/rest_v1/&amp;quot;:): {\displaystyle V=IR&amp;lt;nowiki&amp;gt;}&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
I installed Mathoid and tried to set the {{code|$wgMathFullRestbaseURL}} to the service to no avail since {{code|$wgMathFullRestbaseURL}} required the Restbase API rather than the Mathoid API. I did not want to install Restbase for a simple wiki and requiring Restbase will make hosting it on a shared hosting environment tricky.&lt;br /&gt;
&lt;br /&gt;
====Solution====&lt;br /&gt;
&amp;lt;s&amp;gt;It turns out that the Math extension versions 1.30 and prior works.&amp;lt;/s&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Save yourself the headache and use MathML, then set the RestbaseURL and MathML URL to Wikipedia&#039;s.&lt;br /&gt;
{{highlight&lt;br /&gt;
| lang = terminal&lt;br /&gt;
| code = $wgDefaultUserOptions[&#039;math&#039;] = &#039;mathml&#039;;&lt;br /&gt;
$wgMathFullRestbaseURL = &#039;https://en.wikipedia.org/api/rest_&#039;;&lt;br /&gt;
$wgMathMathMLUrl = &#039;https://mathoid-beta.wmflabs.org/&#039;;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
===Visual Editor===&lt;br /&gt;
&lt;br /&gt;
====All templates are puzzle pieces====&lt;br /&gt;
All templates within the Visual Editor view appear as puzzle pieces. Investigate by looking at the Parsoid logs. The issue I had which was not obvious was that the template expansion was failing because I referenced the unencrypted HTTP URL rather than the HTTPS one. The 301 HTTPS redirect rule I had caused Parsoid to fail which resulted in templates being rendered as puzzle pieces.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = {&amp;quot;name&amp;quot;:&amp;quot;parsoid&amp;quot;,&amp;quot;hostname&amp;quot;:&amp;quot;c2774ece52ab&amp;quot;,&amp;quot;pid&amp;quot;:9,&amp;quot;level&amp;quot;:40,&amp;quot;logType&amp;quot;:&amp;quot;warn&amp;quot;,&amp;quot;wiki&amp;quot;:&amp;quot;wiki$0&amp;quot;,&amp;quot;title&amp;quot;:&amp;quot;Kubernetes&amp;quot;,&amp;quot;oldId&amp;quot;:3829,&amp;quot;reqId&amp;quot;:null,&amp;quot;userAgent&amp;quot;:&amp;quot;VisualEditor-MediaWiki/1.34.1&amp;quot;,&amp;quot;msg&amp;quot;:&amp;quot;non-200 response: 301 &amp;lt;!DOCTYPE HTML PUBLIC \&amp;quot;-//IETF//DTD HTML 2.0//EN\&amp;quot;&amp;gt;\n&amp;lt;html&amp;gt;&amp;lt;head&amp;gt;\n&amp;lt;title&amp;gt;301 Moved Permanently&amp;lt;/title&amp;gt;\n&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;\n&amp;lt;h1&amp;gt;Moved Permanently&amp;lt;/h1&amp;gt;\n&amp;lt;p&amp;gt;The document has moved &amp;lt;a href=\&amp;quot;https://leo.leung.xyz/wiki/api.php\&amp;quot;&amp;gt;here&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;\n&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;\n&amp;quot;,&amp;quot;longMsg&amp;quot;:&amp;quot;non-200 response:\n301\n&amp;lt;!DOCTYPE HTML PUBLIC \&amp;quot;-//IETF//DTD HTML 2.0//EN\&amp;quot;&amp;gt;\n&amp;lt;html&amp;gt;&amp;lt;head&amp;gt;\n&amp;lt;title&amp;gt;301 Moved Permanently&amp;lt;/title&amp;gt;\n&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;\n&amp;lt;h1&amp;gt;Moved Permanently&amp;lt;/h1&amp;gt;\n&amp;lt;p&amp;gt;The document has moved &amp;lt;a href=\&amp;quot;https://leo.leung.xyz/wiki/api.php\&amp;quot;&amp;gt;here&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;\n&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;\n&amp;quot;,&amp;quot;levelPath&amp;quot;:&amp;quot;warn&amp;quot;,&amp;quot;time&amp;quot;:&amp;quot;2020-05-24T13:02:41.718Z&amp;quot;,&amp;quot;v&amp;quot;:0}&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
If you are using the docker image I referenced above, the {{Code|docker-compose.yml}} file should have the environment variable {{Code|PARSOID_DOMAIN_domain}} reference the appropriate https version of the URL.&lt;br /&gt;
&lt;br /&gt;
====Error contacting the Parsoid/RESTBase server (HTTP 412)====&lt;br /&gt;
A private wiki had this error. The error was caused by a traefik middleware used for authentication. I believe the script used for the middleware was generating output which somehow interfered with the rest.php request. Disabling (and later fixing) the middleware fixed this issue.&lt;br /&gt;
&lt;br /&gt;
===The DynamicPageList extension doesn&#039;t sort by lastedit properly===&lt;br /&gt;
As mentioned in [https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:DynamicPageList_(Wikimedia)#ordermethod the DynamicPageList extension documentation], &#039;lastedit&#039; sort method doesn&#039;t actually work as you&#039;d expect:&lt;br /&gt;
{{Quote|quote=It should be noted, that lastedit really sorts by the last time the page was touched. In some cases this is not equivalent to the last edit (for example, this includes permission changes, creation or deletion of linked pages, and alteration of contained templates).|source=https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:DynamicPageList_(Wikimedia)#ordermethod}}&lt;br /&gt;
Worse yet, editing &amp;lt;code&amp;gt;LocalSettings.php&amp;lt;/code&amp;gt; would cause all pages to be &#039;touched&#039; and render the generated list to be a random tossup of all pages within the Wiki and rendering the list to be completely useless.&lt;br /&gt;
&lt;br /&gt;
A fix would be to tweak the query for &#039;lastedit&#039; in &amp;lt;code&amp;gt;DynamicPageListHooks.php&amp;lt;/code&amp;gt;:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = // From:&lt;br /&gt;
case &#039;lastedit&#039;:&lt;br /&gt;
	$sqlSort = &#039;page_touched&#039;;&lt;br /&gt;
&lt;br /&gt;
// To:&lt;br /&gt;
case &#039;lastedit&#039;:&lt;br /&gt;
	$fields[&#039;rc_timestamp&#039;] = &#039;MAX(recentchanges.rc_timestamp)&#039;;&lt;br /&gt;
	$tables[&#039;recentchanges&#039;] = &#039;recentchanges&#039;;&lt;br /&gt;
	$join[&#039;recentchanges&#039;] = [&#039;LEFT JOIN&#039;, &#039;recentchanges.rc_title = page_title&#039;];&lt;br /&gt;
	$options[&#039;GROUP BY&#039;] = &amp;quot;page_id&amp;quot;;&lt;br /&gt;
	$sqlSort = &#039;rc_timestamp&#039;;&lt;br /&gt;
| lang = php&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
===Updating from 1.34 to 1.35===&lt;br /&gt;
Version 1.35 includes a built-in PHP based parser to replace Parsoid and bundles the VisualEditor extension.&lt;br /&gt;
&lt;br /&gt;
I had issues getting the 1.34 Docker container to work after updating it to 1.35 because the wiki wasn&#039;t able to reach its REST API via localhost within the container. It was only after setting the servername to the public IP address in /etc/hosts that the API was able to reach its REST API and then have visual editor work properly. This is stupid however because REST API traffic will leave the container, out to my ISP, then come right back in to the same container via the reverse proxy.&lt;br /&gt;
&lt;br /&gt;
Changes I had to make for VisualEditor to work:&lt;br /&gt;
&lt;br /&gt;
# Add wiki.home.steamr.com to my external IP address in /etc/hosts.&lt;br /&gt;
# Add to nginx.conf after the &amp;lt;code&amp;gt;location /&amp;lt;/code&amp;gt; section: {{Highlight&lt;br /&gt;
| code = location /rest.php/ {&lt;br /&gt;
        try_files $uri $uri/ /rest.php?$query_string;&lt;br /&gt;
}&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
===Updating from 1.35 to 1.37===&lt;br /&gt;
I tried updating from 1.35 to 1.37 using the official Docker image and had issues getting Parsoid and the VisualEditor to work. Using my existing &amp;lt;code&amp;gt;LocalSettings.php&amp;lt;/code&amp;gt; config, I ran into 404 errors when launching the VisualEditor. This was resolved by defining the URL in &amp;lt;code&amp;gt;$wgVirtualRestConfig[&#039;modules&#039;][&#039;parsoid&#039;]&amp;lt;/code&amp;gt; to point to the Docker container itself: &lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = $wgVirtualRestConfig[&#039;modules&#039;][&#039;parsoid&#039;] = array(&lt;br /&gt;
    // URL to the Parsoid instance&lt;br /&gt;
    &#039;url&#039; =&amp;gt; &#039;http://localhost:8080/rest.php&#039;,&lt;br /&gt;
);&lt;br /&gt;
| lang = php&lt;br /&gt;
}}&lt;br /&gt;
This however resulted in another error 400 which looking at the web browser developer console shows &amp;quot;The requested relative path (...) did not match any known handler&amp;quot;. I did see the rest.php calls land on the web server of the container. This was eventually fixed by loading the Parsoid extension in LocalSettings.php. What I have in my LocalSettings.php includes the following lines:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = wfLoadExtension(&#039;VisualEditor&#039;);&lt;br /&gt;
wfLoadExtension( &#039;Parsoid&#039;, &#039;vendor/wikimedia/parsoid/extension.json&#039; );&lt;br /&gt;
$wgDefaultUserOptions[&#039;visualeditor-enable&#039;] = 1;&lt;br /&gt;
| lang = php&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
===Updating from 1.37 to 1.39===&lt;br /&gt;
Skin deprecation issues. To hide deprecation notices, I added the following lines to LocalSettings.php:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = $wgShowExceptionDetails = false;&lt;br /&gt;
$wgDeprecationReleaseLimit = &#039;1.30&#039;;&lt;br /&gt;
| lang = php&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Corrupt objectcache table ===&lt;br /&gt;
For some odd reason, my Wiki&#039;s objectcache table is randomly getting corrupted. This can be recovered from if you drop and re-create the objectcache table.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # mariadb -u root -p$MYSQL_ROOT_PASSWORD&lt;br /&gt;
MariaDB [(none)]&amp;gt; use wiki&lt;br /&gt;
MariaDB [wiki]&amp;gt;  DROP TABLE IF EXISTS `objectcache`;&lt;br /&gt;
MariaDB [wiki]&amp;gt;  CREATE TABLE `objectcache` (&lt;br /&gt;
  `keyname` varbinary(255) NOT NULL DEFAULT &#039;&#039;,&lt;br /&gt;
  `value` mediumblob DEFAULT NULL,&lt;br /&gt;
  `exptime` binary(14) NOT NULL,&lt;br /&gt;
  `modtoken` varbinary(17) NOT NULL DEFAULT &#039;00000000000000000&#039;,&lt;br /&gt;
  `flags` int(10) unsigned DEFAULT NULL,&lt;br /&gt;
  PRIMARY KEY (`keyname`),&lt;br /&gt;
  KEY `exptime` (`exptime`)&lt;br /&gt;
) ENGINE=InnoDB DEFAULT CHARSET=binary;&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Mariadb 12 ===&lt;br /&gt;
Mariadb 12 is not supported by MediaWiki (as of the time of writing, MediaWiki 1.44). I had some docker-compose files which references &amp;lt;code&amp;gt;mariadb:latest&amp;lt;/code&amp;gt; which was recently changed over to verion 12 and this caused MediaWiki to think the database is in read-only mode. This resulted in this warning when trying to edit a page:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = The primary database server is running in read-only mode.&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
The fix is to downgrade the docker image back down to Mariadb 11. Fortunately, everything works even after Mariadb 12&#039;s database migration.&lt;br /&gt;
&lt;br /&gt;
This bug is tracked at https://phabricator.wikimedia.org/T401570&amp;lt;nowiki/&amp;gt;{{Navbox Web}}&lt;br /&gt;
[[Category:WebApp]]&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=MediaWiki&amp;diff=7706</id>
		<title>MediaWiki</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=MediaWiki&amp;diff=7706"/>
		<updated>2025-08-18T23:56:39Z</updated>

		<summary type="html">&lt;p&gt;Leo: /* Troubleshooting */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;MediaWiki is an opensource wiki engine written in PHP by the Wikimedia Foundation. It is used by both Wikipedia and this site.&lt;br /&gt;
&lt;br /&gt;
Visit the project website at:&lt;br /&gt;
&lt;br /&gt;
*https://www.mediawiki.org&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Running inside Docker==&lt;br /&gt;
You can run MediaWiki from a Docker container. A proof of concept can be found at:&lt;br /&gt;
&lt;br /&gt;
*https://git.steamr.com/docker/mediawiki&lt;br /&gt;
&lt;br /&gt;
In order to make a single MediaWiki container image which can be used regardless of custom skins or extensions, I intentionally separated custom skins and extensions into a separate directory. When the container first starts, a setup script will symlink these additional extensions and skins to the primary directory. Custom extensions and skins need to be placed in the extension and skins volume and then added to the LocalSettings.php configuration file. When adding a new extension or skin, you must restart the container for these changes to take effect. &lt;br /&gt;
&lt;br /&gt;
To get started with my image, create the container with volumes for:&lt;br /&gt;
&lt;br /&gt;
#Images / Uploads as /mediawiki/images&lt;br /&gt;
#Extensions as /extensions&lt;br /&gt;
#Skins as /skins&lt;br /&gt;
#The {{code|LocalSettings.php}} configuration file under /config&lt;br /&gt;
#Database (on a remote server or container), or a SQLite file on a volume&lt;br /&gt;
&lt;br /&gt;
An example docker-compose configuration running a wiki:&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| lang = yaml&lt;br /&gt;
| code = wiki:&lt;br /&gt;
    image: registry.steamr.com/docker/mediawiki:1.34&lt;br /&gt;
    networks:&lt;br /&gt;
      - traefik&lt;br /&gt;
      - db-net&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;8888&amp;quot;&lt;br /&gt;
    environment:&lt;br /&gt;
      - VIRTUAL_HOST=wiki.steamr.com&lt;br /&gt;
      - APP_ROOT=/mediawiki&lt;br /&gt;
      - DOCUMENT_ROOT=/mediawiki&lt;br /&gt;
      - DB_HOST=db&lt;br /&gt;
      - DB_DATABASE=wiki&lt;br /&gt;
      - DB_USERNAME=wiki&lt;br /&gt;
      - DB_PASSWORD=wiki&lt;br /&gt;
    restart: always&lt;br /&gt;
    volumes:&lt;br /&gt;
      - /var/volumes/wiki/config:/config&lt;br /&gt;
      - /var/volumes/wiki/extensions:/extensions&lt;br /&gt;
      - /var/volumes/wiki/skins:/skins&lt;br /&gt;
      - /var/volumes/wiki/images:/mediawiki/images&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==Configuration==&lt;br /&gt;
MediaWiki only has a single configuration file at {{code|LocalSettings.php}}.&lt;br /&gt;
&lt;br /&gt;
If you wish to run a wiki on the root of a domain, you need to set &amp;lt;code&amp;gt;$wgScriptPath&amp;lt;/code&amp;gt; empty like so:&lt;br /&gt;
{{highlight&lt;br /&gt;
| lang = php&lt;br /&gt;
| code = $wgScriptPath = &amp;quot;&amp;quot;;&lt;br /&gt;
$wgArticlePath = &amp;quot;/$1&amp;quot;;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
===Extensions===&lt;br /&gt;
Extensions are placed under the {{code|/extensions}} directory. Common extensions are bundled with the base installation of MediaWiki but not enabled by default. Extentions that are bundled can be enabled by adding a {{code|wfLoadExtention(&#039;extention&#039;)}} call to the {{code|LocalSettings.php}} file.&lt;br /&gt;
&lt;br /&gt;
There are some extensions that this wiki requires:&lt;br /&gt;
&lt;br /&gt;
;Intersection&lt;br /&gt;
:https://github.com/wikimedia/mediawiki-extensions-intersection.git&lt;br /&gt;
:Generates dynamic lists&lt;br /&gt;
;Scribunto&lt;br /&gt;
:https://github.com/wikimedia/mediawiki-extensions-Scribunto.git&lt;br /&gt;
:Generates scripted outputs using Lua&lt;br /&gt;
;Math&lt;br /&gt;
:https://github.com/wikimedia/mediawiki-extensions-Math&lt;br /&gt;
:Generates math formulas&lt;br /&gt;
;NativeSvgHandler&lt;br /&gt;
:https://github.com/wikimedia/mediawiki-extensions-NativeSvgHandler.git&lt;br /&gt;
:Embeds SVG files as an image for client-side rendering. Requires appending {{code|http://www.w3.org/tr/rec-rdf-syntax/}} to {{code|$validNamespaces}} in {{code|UploadBase.php}}.&lt;br /&gt;
;WikiSEO&lt;br /&gt;
:https://github.com/octfx/wiki-seo/&lt;br /&gt;
:Enables custom SEO meta keyword and description tags on the wiki.&lt;br /&gt;
;Score&lt;br /&gt;
:https://www.mediawiki.org/wiki/Extension:Score&lt;br /&gt;
: Lets you embed LilyPond music notation into your Wiki&lt;br /&gt;
&lt;br /&gt;
===Skins===&lt;br /&gt;
Skins are placed under the {{code|/skins}} directory. Modern skins are loaded with the {{code|wfLoadSkin(&#039;skin&#039;)}} call in {{code|LocalSettings.php}}, which reads the skin&#039;s {{code|skin.json}} manifest file in the skin&#039;s directory. The manifest file contains the skin&#039;s name, autoload class files, as well as which resource modules to load loaded, including the stylesheets and javascript files that are part of the skin.&lt;br /&gt;
&lt;br /&gt;
MediaWiki&#039;s guide on skinning is relatively up to date albeit a little confusing to understand at first.&lt;br /&gt;
&lt;br /&gt;
*https://www.mediawiki.org/wiki/Manual:Skinning_Part_2&lt;br /&gt;
&lt;br /&gt;
The Reader skin used on this wiki:&lt;br /&gt;
&lt;br /&gt;
*https://git.steamr.com/leo/mediawiki-reader-skin&lt;br /&gt;
&lt;br /&gt;
The {{code|OutputPage}} object is the thing that handles all HTML generation as well as linking javascript and CSS modules.  You can use this to inject HTML or certain code to the page with some method calls. See: https://www.mediawiki.org/wiki/Manual:OutputPage.php&lt;br /&gt;
&lt;br /&gt;
===Transcluded Pages===&lt;br /&gt;
There are some pages that are used by the MediaWiki software itself, including:&lt;br /&gt;
&lt;br /&gt;
*[[MediaWiki:Aboutsite]]&lt;br /&gt;
*[[MediaWiki:Disclaimers]]&lt;br /&gt;
*[[MediaWiki:Privacy]]&lt;br /&gt;
*[[MediaWiki:Toolbox]]&lt;br /&gt;
*[[MediaWiki:Sidebar]]&lt;br /&gt;
&lt;br /&gt;
Some resources are also loaded from pages including:&lt;br /&gt;
&lt;br /&gt;
*[[MediaWiki:Geshi.css]]&lt;br /&gt;
*[[MediaWiki:Common.css]]&lt;br /&gt;
*[[MediaWiki:Common.js]]&lt;br /&gt;
&lt;br /&gt;
Skins should be able to handle contents on these MediaWiki pages which are typically shown somewhere on the page. Of course, custom skins can also reference their own set of pages such as the bootstrap/reader skin I&#039;m currently using:&lt;br /&gt;
&lt;br /&gt;
*[[Bootstrap:Footer]]&lt;br /&gt;
*[[Bootstrap:Sidebar]]&lt;br /&gt;
*[[Bootstrap:Jumbotron]]&lt;br /&gt;
&lt;br /&gt;
==Tasks==&lt;br /&gt;
&lt;br /&gt;
===Enabling Visual Editor===&lt;br /&gt;
The Visual Editor has been included out of the box since MediaWiki 1.35. &#039;&#039;&#039;The following steps are no longer required.&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Visual Editor is an extension that enables the WYSIWYG editor. This extension requires Parsoid in order to properly save changes. &lt;br /&gt;
&lt;br /&gt;
Installing the Visual Editor extension is simple:&lt;br /&gt;
&lt;br /&gt;
#Download the extension to the extensions directory {{highlight&lt;br /&gt;
| lang = text&lt;br /&gt;
| code = $ cd extensions&lt;br /&gt;
$ wget https://extdist.wmflabs.org/dist/extensions/VisualEditor-REL1_34-74116a7.tar.gz&lt;br /&gt;
$ tar -xzf VisualEditor*gz&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
#Edit LocalSettings.php and enable the extension. {{Highlight&lt;br /&gt;
| lang = php&lt;br /&gt;
| code = wfLoadExtension(&#039;VisualEditor&#039;);&lt;br /&gt;
$wgDefaultUserOptions[&#039;visualeditor-enable&#039;] = 1;&lt;br /&gt;
$wgVirtualRestConfig[&#039;modules&#039;][&#039;parsoid&#039;] = array(&lt;br /&gt;
    // URL to the Parsoid instance&lt;br /&gt;
    // Use port 8142 if you use the Debian package&lt;br /&gt;
    &#039;url&#039; =&amp;gt; &#039;http://parsoid:8000&#039;,&lt;br /&gt;
    // Parsoid &amp;quot;domain&amp;quot;, see below (optional)&lt;br /&gt;
    &#039;domain&#039; =&amp;gt; &#039;wiki&#039;,&lt;br /&gt;
    # // Parsoid &amp;quot;prefix&amp;quot;, see below (optional)&lt;br /&gt;
    # &#039;prefix&#039; =&amp;gt; &#039;localhost&#039;&lt;br /&gt;
);&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Getting Parsoid running is slightly trickier.  I recommend running Parsoid in a docker container to simplify installation. There is one built at thenets/parsoid which works just fine. A clone of that project is available at https://git.steamr.com/docker/parsoid. The container takes in an environment variables to configure which domains Parsoid is to work for. An example docker-compose file with everything working is given below.&lt;br /&gt;
&lt;br /&gt;
{{highlight&lt;br /&gt;
| lang = yaml&lt;br /&gt;
| code = version: &#039;3.3&#039;&lt;br /&gt;
services:&lt;br /&gt;
  wiki:&lt;br /&gt;
    image: registry.steamr.com/docker/mediawiki:1.34&lt;br /&gt;
    labels:&lt;br /&gt;
      - &amp;quot;traefik.enable=true&amp;quot;&lt;br /&gt;
      - &amp;quot;traefik.port=8888&amp;quot;&lt;br /&gt;
      - &amp;quot;traefik.docker.network=traefik&amp;quot;&lt;br /&gt;
      - &amp;quot;traefik.frontend.rule=Host:wiki.steamr.com&amp;quot;&lt;br /&gt;
    networks:&lt;br /&gt;
      - traefik&lt;br /&gt;
      - db-net&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;8888&amp;quot;&lt;br /&gt;
    environment:&lt;br /&gt;
      - VIRTUAL_HOST=wiki.home.steamr.com&lt;br /&gt;
      - APP_ROOT=/mediawiki&lt;br /&gt;
      - DOCUMENT_ROOT=/mediawiki&lt;br /&gt;
      - DB_HOST=db&lt;br /&gt;
      - DB_DATABASE=wiki&lt;br /&gt;
      - DB_USERNAME=wiki&lt;br /&gt;
      - DB_PASSWORD=wiki&lt;br /&gt;
    restart: always&lt;br /&gt;
    volumes:&lt;br /&gt;
      - /var/volumes/wiki/config:/config&lt;br /&gt;
      - /var/volumes/wiki/extensions:/extensions&lt;br /&gt;
      - /var/volumes/wiki/skins:/skins&lt;br /&gt;
      - /var/volumes/wiki/images:/mediawiki/images&lt;br /&gt;
&lt;br /&gt;
  parsoid:&lt;br /&gt;
    image: registry.steamr.com/docker/parsoid:latest&lt;br /&gt;
    labels:&lt;br /&gt;
      - &amp;quot;traefik.enable=true&amp;quot;&lt;br /&gt;
      - &amp;quot;traefik.port=8000&amp;quot;&lt;br /&gt;
      - &amp;quot;traefik.docker.network=traefik&amp;quot;&lt;br /&gt;
      - &amp;quot;traefik.frontend.rule=Host:parsoid.steamr.com&amp;quot;&lt;br /&gt;
    environment:&lt;br /&gt;
      - PARSOID_DOMAIN_wiki=http://wiki:8888/api.php&lt;br /&gt;
    networks:&lt;br /&gt;
      - traefik&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;8000&amp;quot;&lt;br /&gt;
    restart: always&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
The Visual Editor should be functional at this point. If the editor does not start without any visual messages, check the javascript console for error messages.  There are certain skin requirements that need to be met in order for the Visual Editor to work outlined at https://www.mediawiki.org/wiki/VisualEditor/Skin_requirements.&lt;br /&gt;
&lt;br /&gt;
===Inserting a custom script in {{code|&amp;lt;head&amp;gt;}}===&lt;br /&gt;
If using a custom skin, use the {{code|OutputPage}} and call {{code|addHeadItem(&#039;name&#039;, &#039;&amp;lt;script&amp;gt;...&amp;lt;/script&#039;)}} to inject a custom script block within the document head.&lt;br /&gt;
&lt;br /&gt;
Alternatively, write a specific OutputPageBeforeHTML hook, and from there call addInlineScript.&lt;br /&gt;
&lt;br /&gt;
===Adding a custom button to WikiEditor Toolbar===&lt;br /&gt;
To add a custom button to the WikiEditor toolbar next to the existing bold and italic buttons, edit the {{code|MediaWiki:Common.js}} file. This file can only be changed by {{code|interface administrator}} users.  Membership to this group can be assigned at [[Special:UserRights/username]].&lt;br /&gt;
&lt;br /&gt;
The {{code|Common.js}} file used to add the Code and SyntaxHighlight templates used on this wiki is given below.&lt;br /&gt;
&lt;br /&gt;
{{highlight&lt;br /&gt;
| lang = js&lt;br /&gt;
| code = var customizeToolbar = function () {&lt;br /&gt;
	$(&#039;#wpTextbox1&#039;).wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
		&#039;section&#039;: &#039;main&#039;,&lt;br /&gt;
		&#039;group&#039;: &#039;format&#039;,&lt;br /&gt;
		&#039;tools&#039;: {&lt;br /&gt;
			&#039;code&#039;: {&lt;br /&gt;
				label: &#039;code&#039;,&lt;br /&gt;
				type: &#039;button&#039;,&lt;br /&gt;
				oouiIcon: &#039;code&#039;,&lt;br /&gt;
				action: {&lt;br /&gt;
					type: &#039;encapsulate&#039;,&lt;br /&gt;
					options: {&lt;br /&gt;
						pre: &amp;quot;{{code|&amp;quot;,&lt;br /&gt;
						post: &amp;quot;}}&amp;quot;&lt;br /&gt;
					}&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
	});&lt;br /&gt;
	$(&#039;#wpTextbox1&#039;).wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
		&#039;section&#039;: &#039;main&#039;,&lt;br /&gt;
		&#039;group&#039;: &#039;format&#039;,&lt;br /&gt;
		&#039;tools&#039;: {&lt;br /&gt;
			&#039;terminal&#039;: {&lt;br /&gt;
				label: &#039;highlight&#039;,&lt;br /&gt;
				type: &#039;button&#039;,&lt;br /&gt;
				oouiIcon: &#039;tag&#039;,&lt;br /&gt;
				action: {&lt;br /&gt;
					type: &#039;encapsulate&#039;,&lt;br /&gt;
					options: {&lt;br /&gt;
						pre: &amp;quot;&amp;lt;nowiki&amp;gt;{{&amp;lt;/nowiki&amp;gt;highlight{{!}}lang=terminal{{!}}code=\n&amp;quot;,&lt;br /&gt;
&amp;lt;nowiki&amp;gt;						post: &amp;quot;}}&amp;quot;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
					}&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
	});&lt;br /&gt;
};&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Documentation for this can be found at:&lt;br /&gt;
&lt;br /&gt;
*https://www.mediawiki.org/wiki/Extension:WikiEditor/Toolbar_customization#Basic_setup&lt;br /&gt;
&lt;br /&gt;
===Remove the &#039;Retrieved from&#039; footer message===&lt;br /&gt;
There are a few ways to hide the &#039;Retrieved from&#039; message that appears at the end of every article.&lt;br /&gt;
&lt;br /&gt;
#Edit the [[MediaWiki:Retrievedfrom]] page with an Administrator account. Comment out or remove the contents to suppress the message.&lt;br /&gt;
#Hide the content with CSS by adding to [[MediaWiki:Common.css]] the following rule: {{highlight&lt;br /&gt;
| lang = css&lt;br /&gt;
| code = /* hide the &amp;quot;Retrieved from&amp;quot; message */&lt;br /&gt;
.printfooter { display: none; &amp;lt;nowiki&amp;gt;}&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
}}&lt;br /&gt;
#Edit the template that your skin is using. It&#039;ll look something like: {{highlight&lt;br /&gt;
| lang = text&lt;br /&gt;
| code = {{#html-printfooter}}&amp;lt;div class=&amp;quot;printfooter&amp;quot;&amp;gt;{{{html-printfooter}}}&amp;lt;/div&amp;gt;{{/html-printfooter}}&lt;br /&gt;
}}. Either remove, or comment out the line using mustache &amp;lt;code&amp;gt;{{! ... }}&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===How to promote a user into a sysop, bureaucrat, or interface-admin group===&lt;br /&gt;
Certain actions on the Wiki are restricted to specific user groups. The default permissions are listed at https://www.mediawiki.org/wiki/Manual:User_rights. To add a user to a specific group:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = $ mysql -u $User -p$Password $Database&lt;br /&gt;
&lt;br /&gt;
## In MySQL prompt, determine the user&#039;s user_id&lt;br /&gt;
MariaDB [wiki]&amp;gt;&amp;lt;nowiki&amp;gt; SELECT user_id, user_name FROM user WHERE user_name = &#039;leo&#039;&lt;br /&gt;
+---------+-------------&lt;br /&gt;
| user_id | user_name&lt;br /&gt;
|       1 | Leo             &lt;br /&gt;
+---------+-------------&lt;br /&gt;
&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
## Add the user into the desired group using the user_id above.&lt;br /&gt;
MariaDB [wiki]&amp;gt; INSERT INTO `user_groups` VALUES (1, &#039;sysop&#039;), (1, &#039;bureaucrat&#039;), (1, &#039;interface-admin&#039;);&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Running the Docker container under a different prefix using Traefik ===&lt;br /&gt;
The official Docker image as well as my customized version based off of it expects the Wiki to be served from the domain root. However, if you want to serve the Wiki from a &#039;subdirectory&#039; like &amp;lt;code&amp;gt;/wiki&amp;lt;/code&amp;gt;, you need to do a few things:&lt;br /&gt;
&lt;br /&gt;
#In Traefik, your router should have the following rules: &amp;lt;code&amp;gt;traefik.http.routers.wiki-https.rule=(Host(`my-wiki-server.tld`) &amp;amp;&amp;amp; PathPrefix(`/wiki`))&amp;lt;/code&amp;gt;.&lt;br /&gt;
#In your Docker container, you need to symlink the &amp;lt;code&amp;gt;/wiki&amp;lt;/code&amp;gt; directory to &amp;lt;code&amp;gt;/var/www/html&amp;lt;/code&amp;gt;. &amp;lt;code&amp;gt;ln -s /var/www/html /var/www/html/wiki&amp;lt;/code&amp;gt;&lt;br /&gt;
#In your MediaWiki &amp;lt;code&amp;gt;LocalSettings.php&amp;lt;/code&amp;gt; configuration, you need to:&lt;br /&gt;
##Set &amp;lt;code&amp;gt;$wgVirtualRestConfig[&#039;modules&#039;][&#039;parsoid&#039;] = [&#039;url&#039; =&amp;gt; &#039;&amp;lt;nowiki&amp;gt;http://localhost:8080/wiki/rest.php&#039;&amp;lt;/nowiki&amp;gt;];&amp;lt;/code&amp;gt; so that Parsoid continues to work.&lt;br /&gt;
##Set &amp;lt;code&amp;gt;$wgScriptPath = &amp;quot;/wiki&amp;quot;;&amp;lt;/code&amp;gt;&lt;br /&gt;
## Set &amp;lt;code&amp;gt;$wgArticlePath = &amp;quot;/wiki/$1&amp;quot;;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Running MediaWiki behind a reverse proxy===&lt;br /&gt;
When running MediaWiki behind a reverse proxy like squid, nginx, or Traefik, edits made by users will appear as originating from reverse proxy server. &lt;br /&gt;
&lt;br /&gt;
To fix this issue, you need to tell MediaWiki which address ranges are from the reverse proxy and to have it use the X-Forwarded-For header instead. Since MediaWiki 1.35, this can be accomplished by adding [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgUsePrivateIPs $wgUsePrivateIPs] and [https://www.mediawiki.org/wiki/Manual:$wgCdnServersNoPurge $wgCdnServersNoPurge] in your &amp;lt;code&amp;gt;LocalSettings.php&amp;lt;/code&amp;gt; file:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = $wgUsePrivateIPs = true;&lt;br /&gt;
$wgUseCdn = true;&lt;br /&gt;
$wgCdnServersNoPurge = [];&lt;br /&gt;
$wgCdnServersNoPurge[] = &amp;quot;172.18.0.0/16&amp;quot;;&lt;br /&gt;
| lang = php&lt;br /&gt;
}}&lt;br /&gt;
Replace 172.18.0.0/16 with the CIDR of your reverse proxy networks (this CIDR is my internal Traefik network for my Docker stack).&lt;br /&gt;
&lt;br /&gt;
=== Adding the Score extension ===&lt;br /&gt;
The Score extension allows the rendering of music notation using the Lilypond project as the renderer. I ran into a series of hurdles getting this to work for MediaWiki 1.39.&lt;br /&gt;
&lt;br /&gt;
Issues are: &lt;br /&gt;
&lt;br /&gt;
#The extention highly recommends the use of ShellBox to isolate Lilypond because Lilypond&#039;s insecure and may allow remote execution. Since this Wiki is editable to the public, this has to be done. You&#039;ll have to set up a ShellBox instance (as a separate container). MediaWiki/Wikipedia has their own container image which doesn&#039;t work outside their infrastructure so I had to make my own.&lt;br /&gt;
# Lilypond has issues and breaks in my environment. I had to:&lt;br /&gt;
#*Modify &amp;lt;code&amp;gt;/usr/bin/lilypond&amp;lt;/code&amp;gt; to include &amp;lt;code&amp;gt;export PATH=&amp;quot;/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&amp;quot;&amp;amp;nbsp;&amp;lt;/code&amp;gt;otherwise it will fail silently in the extension&#039;s script. If you enable verbose mode on lilypond, you will see &amp;lt;code&amp;gt;warning: g_spawn_sync failed (0): gs: Failed to execute child process “gs” (No such file or directory)&amp;lt;/code&amp;gt;.&lt;br /&gt;
#*Symlink the lilypond fonts because on Debian Bullseye, the package versions for Lilypond and Lilypond-fonts are mismatched so the paths are broken (What the heck Debian?)&lt;br /&gt;
#*I tried to enable SVG which works after some tweaking to the generator script, but is a full page in size and can&#039;t be easily trimmed.&lt;br /&gt;
#The Score extension&#039;s included shell script is questionable...&lt;br /&gt;
#* The fuzzy png image apparently is from the ghostscript .ps to .png conversion. I&#039;m not sure why this is even done separately when lilypond also generates the pngs. I hacked this by copying &#039;file.png&#039; over &#039;file-page1.png&#039; and commented out the &amp;lt;code&amp;gt;runGhostscript&amp;lt;/code&amp;gt; function call in the &amp;lt;code&amp;gt;generatePngAndMidi.sh&amp;lt;/code&amp;gt; script. That seems to make the png not shrink down and be fuzzy.&lt;br /&gt;
After overcoming all the aforementioned issues, I think I&#039;ve finally got it working.&lt;br /&gt;
&lt;br /&gt;
* Use the following Dockerfiles&lt;br /&gt;
*Add the following docker-compose entries&lt;br /&gt;
*In the volume, run: {{Highlight&lt;br /&gt;
| code = $ git clone https://gerrit.wikimedia.org/r/mediawiki/libs/Shellbox shellbox&lt;br /&gt;
&lt;br /&gt;
## In the fpm container, run as the shellbox user:&lt;br /&gt;
# su shellbox&lt;br /&gt;
$ cd /srv/shellbox&lt;br /&gt;
$ /usr/local/bin/composer install --no-dev&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
*Enable the plugin:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = wfLoadExtension( &#039;Score&#039; );&lt;br /&gt;
$wgScoreTrim = true;&lt;br /&gt;
$wgScoreUseSvg = false;&lt;br /&gt;
$wgShellboxUrl = &#039;http://shellbox/shellbox&#039;;&lt;br /&gt;
$wgShellboxSecretKey = &#039;secret_key&#039;;&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
* You have to tweak [[MediaWiki:Common.css]] so that the embedded images aren&#039;t too big. I made them 4em max-height as I intend to only embed single staff snippets for my notes.&lt;br /&gt;
&lt;br /&gt;
==Troubleshooting==&lt;br /&gt;
&lt;br /&gt;
===Scribunto Lua Failures===&lt;br /&gt;
If templates cause this error:&lt;br /&gt;
{{highlight&lt;br /&gt;
| lang = text&lt;br /&gt;
| code = Lua error: Internal error: The interpreter exited with status 127.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
This likely means that you do not have Lua installed or it is not in the PATH. You will need to specify the Lua path in {{code|LocalSettings.php}} with this line:&lt;br /&gt;
{{highlight&lt;br /&gt;
| lang = php&lt;br /&gt;
| code = $wgScribuntoEngineConf[&#039;luastandalone&#039;][&#039;luaPath&#039;] = &amp;quot;/usr/bin/lua5.1&amp;quot;;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
===Database Import Incomplete ===&lt;br /&gt;
Database imports from MySQL 5.7.27 to a MariaDB 10.4.7 seems to fail. Imports only appear to complete if the database dump was made without {{code|Enclose export in a transaction}} enabled in PHPMyAdmin but subsequent edits on the destination wiki will result in this error message:&lt;br /&gt;
{{highlight&lt;br /&gt;
| lang = text&lt;br /&gt;
| code = The revision #0 of the page named &amp;quot;some-article&amp;quot; does not exist.&lt;br /&gt;
&lt;br /&gt;
This is usually caused by following an outdated history link to a page that has been deleted. Details can be found in the deletion log.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Solution====&lt;br /&gt;
It turns out the destination database server (mariadb:10.4.7, in docker) was not set up properly after being upgraded from MariaDB-10.1. On start up, it showed the following error messages:&lt;br /&gt;
{{highlight&lt;br /&gt;
| lang = text&lt;br /&gt;
| code = 2019-09-01 21:14:44 0 [Note] Server socket created on IP: &#039;::&#039;.&lt;br /&gt;
2019-09-01 21:14:44 0 [Warning] &#039;user&#039; entry &#039;root@localhost.localdomain&#039; ignored in --skip-name-resolve mode.&lt;br /&gt;
2019-09-01 21:14:44 0 [Warning] &#039;proxies_priv&#039; entry &#039;@% root@localhost.localdomain&#039; ignored in --skip-name-resolve mode.&lt;br /&gt;
2019-09-01 21:14:44 0 [ERROR] Missing system table mysql.roles_mapping; please run mysql_upgrade to create it&lt;br /&gt;
2019-09-01 21:14:44 0 [ERROR] Incorrect definition of table mysql.event: expected column &#039;sql_mode&#039; at position 14 to have type set(&#039;REAL_AS_FLOAT&#039;,&#039;PIPES_AS_CONCAT&#039;,&#039;ANSI_QUOTES&#039;,&#039;IGNORE_SPACE&#039;,&#039;IGNORE_BAD_TABLE_OPTIONS&#039;,&#039;ONLY_FULL_GROUP_BY&#039;,&#039;NO_UNSIGNED_SUBTRACTION&#039;,&#039;NO_DIR_IN_CREATE&#039;,&#039;POSTGRESQL&#039;,&#039;ORACLE&#039;,&#039;MSSQL&#039;,&#039;DB2&#039;,&#039;MAXDB&#039;,&#039;NO_KEY_OPTIONS&#039;,&#039;NO_TABLE_OPTIONS&#039;,&#039;NO_FIELD_OPTIONS&#039;,&#039;MYSQL323&#039;,&#039;MYSQL40&#039;,&#039;ANSI&#039;,&#039;NO_AUTO_VALUE_ON_ZERO&#039;,&#039;NO_BACKSLASH_ESCAPES&#039;,&#039;STRICT_TRANS_TABLES&#039;,&#039;STRICT_ALL_TABLES&#039;,&#039;NO_ZERO_IN_DATE&#039;,&#039;NO_ZERO_DATE&#039;,&#039;INVALID_DATES&#039;,&#039;ERROR_FOR_DIVISION_BY_ZERO&#039;,&#039;TRADITIONAL&#039;,&#039;NO_AUTO_CREATE_USER&#039;,&#039;HIGH_NOT_PRECEDENCE&#039;,&#039;NO_ENGINE_SUBSTITUTION&#039;,&#039;PAD_CHAR_TO_FULL_LENGTH&#039;,&#039;EMPTY_STRING_IS_NULL&#039;,&#039;SIMULTANEOUS_ASSIGNMENT&#039;), found type set(&#039;REAL_AS_FLOAT&#039;,&#039;PIPES_AS_CONCAT&#039;,&#039;ANSI_QUOTES&#039;,&#039;IGNORE_SPACE&#039;,&#039;IGNORE_BAD_TABLE_OPTIONS&#039;,&#039;ONLY_FULL_GROUP_BY&#039;,&#039;NO_UNSIGNED_SUBTRACTION&#039;,&#039;NO_DIR_IN_CREATE&#039;,&#039;POSTGRESQL&#039;,&#039;ORACLE&#039;,&#039;MSSQL&#039;,&#039;DB2&#039;,&#039;MAXDB&#039;,&#039;NO_KEY_OPTIONS&#039;,&#039;NO_TABLE_OPTIONS&#039;,&#039;NO_FIELD_OPTIONS&#039;,&#039;MYSQL323&#039;,&#039;MYSQL40&#039;,&#039;ANSI&#039;,&#039;NO_AUTO_VALU&lt;br /&gt;
2019-09-01 21:14:44 0 [ERROR] mysqld: Event Scheduler: An error occurred when initializing system tables. Disabling the Event Scheduler.&lt;br /&gt;
2019-09-01 21:14:44 6 [Warning] Failed to load slave replication state from table mysql.gtid_slave_pos: 1146: Table &#039;mysql.gtid_slave_pos&#039; doesn&#039;t exist&lt;br /&gt;
2019-09-01 21:14:44 0 [Note] Reading of all Master_info entries succeeded&lt;br /&gt;
2019-09-01 21:14:44 0 [Note] Added new Master_info &#039;&#039; to hash table&lt;br /&gt;
2019-09-01 21:14:44 0 [Note] mysqld: ready for connections.&lt;br /&gt;
Version: &#039;10.4.7-MariaDB-1:10.4.7+maria~bionic&#039;  socket: &#039;/var/run/mysqld/mysqld.sock&#039;  port: 3306  mariadb.org binary distribution&lt;br /&gt;
2019-09-01 21:14:46 0 [Note] InnoDB: Buffer pool(s) load completed at 190901 21:14:46&lt;br /&gt;
2019-09-01 21:14:49 8 [ERROR] InnoDB: Table `mysql`.`innodb_table_stats` not found.&lt;br /&gt;
2019-09-01 21:14:49 8 [ERROR] Transaction not registered for MariaDB 2PC, but transaction is active&lt;br /&gt;
2019-09-01 21:15:13 9 [ERROR] Transaction not registered for MariaDB 2PC, but transaction is active&lt;br /&gt;
2019-09-01 21:15:13 9 [ERROR] Transaction not registered for MariaDB 2PC, but transaction is active&lt;br /&gt;
2019-09-01 21:15:13 9 [ERROR] Transaction not registered for MariaDB 2PC, but transaction is active&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Running {{code|mysql_upgrade}} fixed these errors and a subsequent database import was successful.&lt;br /&gt;
&lt;br /&gt;
===Math Extension===&lt;br /&gt;
Using the latest Math extension, formulas constantly return errors similar to:&lt;br /&gt;
{{highlight&lt;br /&gt;
| lang = text&lt;br /&gt;
| code = Failed to parse (MathML with SVG or PNG fallback (recommended for modern browsers and accessibility tools): Invalid response (&amp;quot;Math extension cannot connect to Restbase.&amp;quot;) from server &amp;quot;https://wikimedia.org/api/rest_v1/&amp;quot;:): {\displaystyle V=IR&amp;lt;nowiki&amp;gt;}&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
I installed Mathoid and tried to set the {{code|$wgMathFullRestbaseURL}} to the service to no avail since {{code|$wgMathFullRestbaseURL}} required the Restbase API rather than the Mathoid API. I did not want to install Restbase for a simple wiki and requiring Restbase will make hosting it on a shared hosting environment tricky.&lt;br /&gt;
&lt;br /&gt;
====Solution====&lt;br /&gt;
&amp;lt;s&amp;gt;It turns out that the Math extension versions 1.30 and prior works.&amp;lt;/s&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Save yourself the headache and use MathML, then set the RestbaseURL and MathML URL to Wikipedia&#039;s.&lt;br /&gt;
{{highlight&lt;br /&gt;
| lang = terminal&lt;br /&gt;
| code = $wgDefaultUserOptions[&#039;math&#039;] = &#039;mathml&#039;;&lt;br /&gt;
$wgMathFullRestbaseURL = &#039;https://en.wikipedia.org/api/rest_&#039;;&lt;br /&gt;
$wgMathMathMLUrl = &#039;https://mathoid-beta.wmflabs.org/&#039;;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
===Visual Editor===&lt;br /&gt;
&lt;br /&gt;
====All templates are puzzle pieces====&lt;br /&gt;
All templates within the Visual Editor view appear as puzzle pieces. Investigate by looking at the Parsoid logs. The issue I had which was not obvious was that the template expansion was failing because I referenced the unencrypted HTTP URL rather than the HTTPS one. The 301 HTTPS redirect rule I had caused Parsoid to fail which resulted in templates being rendered as puzzle pieces.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = {&amp;quot;name&amp;quot;:&amp;quot;parsoid&amp;quot;,&amp;quot;hostname&amp;quot;:&amp;quot;c2774ece52ab&amp;quot;,&amp;quot;pid&amp;quot;:9,&amp;quot;level&amp;quot;:40,&amp;quot;logType&amp;quot;:&amp;quot;warn&amp;quot;,&amp;quot;wiki&amp;quot;:&amp;quot;wiki$0&amp;quot;,&amp;quot;title&amp;quot;:&amp;quot;Kubernetes&amp;quot;,&amp;quot;oldId&amp;quot;:3829,&amp;quot;reqId&amp;quot;:null,&amp;quot;userAgent&amp;quot;:&amp;quot;VisualEditor-MediaWiki/1.34.1&amp;quot;,&amp;quot;msg&amp;quot;:&amp;quot;non-200 response: 301 &amp;lt;!DOCTYPE HTML PUBLIC \&amp;quot;-//IETF//DTD HTML 2.0//EN\&amp;quot;&amp;gt;\n&amp;lt;html&amp;gt;&amp;lt;head&amp;gt;\n&amp;lt;title&amp;gt;301 Moved Permanently&amp;lt;/title&amp;gt;\n&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;\n&amp;lt;h1&amp;gt;Moved Permanently&amp;lt;/h1&amp;gt;\n&amp;lt;p&amp;gt;The document has moved &amp;lt;a href=\&amp;quot;https://leo.leung.xyz/wiki/api.php\&amp;quot;&amp;gt;here&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;\n&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;\n&amp;quot;,&amp;quot;longMsg&amp;quot;:&amp;quot;non-200 response:\n301\n&amp;lt;!DOCTYPE HTML PUBLIC \&amp;quot;-//IETF//DTD HTML 2.0//EN\&amp;quot;&amp;gt;\n&amp;lt;html&amp;gt;&amp;lt;head&amp;gt;\n&amp;lt;title&amp;gt;301 Moved Permanently&amp;lt;/title&amp;gt;\n&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;\n&amp;lt;h1&amp;gt;Moved Permanently&amp;lt;/h1&amp;gt;\n&amp;lt;p&amp;gt;The document has moved &amp;lt;a href=\&amp;quot;https://leo.leung.xyz/wiki/api.php\&amp;quot;&amp;gt;here&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;\n&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;\n&amp;quot;,&amp;quot;levelPath&amp;quot;:&amp;quot;warn&amp;quot;,&amp;quot;time&amp;quot;:&amp;quot;2020-05-24T13:02:41.718Z&amp;quot;,&amp;quot;v&amp;quot;:0}&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
If you are using the docker image I referenced above, the {{Code|docker-compose.yml}} file should have the environment variable {{Code|PARSOID_DOMAIN_domain}} reference the appropriate https version of the URL.&lt;br /&gt;
&lt;br /&gt;
====Error contacting the Parsoid/RESTBase server (HTTP 412)====&lt;br /&gt;
A private wiki had this error. The error was caused by a traefik middleware used for authentication. I believe the script used for the middleware was generating output which somehow interfered with the rest.php request. Disabling (and later fixing) the middleware fixed this issue.&lt;br /&gt;
&lt;br /&gt;
===The DynamicPageList extension doesn&#039;t sort by lastedit properly===&lt;br /&gt;
As mentioned in [https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:DynamicPageList_(Wikimedia)#ordermethod the DynamicPageList extension documentation], &#039;lastedit&#039; sort method doesn&#039;t actually work as you&#039;d expect:&lt;br /&gt;
{{Quote|quote=It should be noted, that lastedit really sorts by the last time the page was touched. In some cases this is not equivalent to the last edit (for example, this includes permission changes, creation or deletion of linked pages, and alteration of contained templates).|source=https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:DynamicPageList_(Wikimedia)#ordermethod}}&lt;br /&gt;
Worse yet, editing &amp;lt;code&amp;gt;LocalSettings.php&amp;lt;/code&amp;gt; would cause all pages to be &#039;touched&#039; and render the generated list to be a random tossup of all pages within the Wiki and rendering the list to be completely useless.&lt;br /&gt;
&lt;br /&gt;
A fix would be to tweak the query for &#039;lastedit&#039; in &amp;lt;code&amp;gt;DynamicPageListHooks.php&amp;lt;/code&amp;gt;:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = // From:&lt;br /&gt;
case &#039;lastedit&#039;:&lt;br /&gt;
	$sqlSort = &#039;page_touched&#039;;&lt;br /&gt;
&lt;br /&gt;
// To:&lt;br /&gt;
case &#039;lastedit&#039;:&lt;br /&gt;
	$fields[&#039;rc_timestamp&#039;] = &#039;MAX(recentchanges.rc_timestamp)&#039;;&lt;br /&gt;
	$tables[&#039;recentchanges&#039;] = &#039;recentchanges&#039;;&lt;br /&gt;
	$join[&#039;recentchanges&#039;] = [&#039;LEFT JOIN&#039;, &#039;recentchanges.rc_title = page_title&#039;];&lt;br /&gt;
	$options[&#039;GROUP BY&#039;] = &amp;quot;page_id&amp;quot;;&lt;br /&gt;
	$sqlSort = &#039;rc_timestamp&#039;;&lt;br /&gt;
| lang = php&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
===Updating from 1.34 to 1.35===&lt;br /&gt;
Version 1.35 includes a built-in PHP based parser to replace Parsoid and bundles the VisualEditor extension.&lt;br /&gt;
&lt;br /&gt;
I had issues getting the 1.34 Docker container to work after updating it to 1.35 because the wiki wasn&#039;t able to reach its REST API via localhost within the container. It was only after setting the servername to the public IP address in /etc/hosts that the API was able to reach its REST API and then have visual editor work properly. This is stupid however because REST API traffic will leave the container, out to my ISP, then come right back in to the same container via the reverse proxy.&lt;br /&gt;
&lt;br /&gt;
Changes I had to make for VisualEditor to work:&lt;br /&gt;
&lt;br /&gt;
# Add wiki.home.steamr.com to my external IP address in /etc/hosts.&lt;br /&gt;
# Add to nginx.conf after the &amp;lt;code&amp;gt;location /&amp;lt;/code&amp;gt; section: {{Highlight&lt;br /&gt;
| code = location /rest.php/ {&lt;br /&gt;
        try_files $uri $uri/ /rest.php?$query_string;&lt;br /&gt;
}&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
===Updating from 1.35 to 1.37===&lt;br /&gt;
I tried updating from 1.35 to 1.37 using the official Docker image and had issues getting Parsoid and the VisualEditor to work. Using my existing &amp;lt;code&amp;gt;LocalSettings.php&amp;lt;/code&amp;gt; config, I ran into 404 errors when launching the VisualEditor. This was resolved by defining the URL in &amp;lt;code&amp;gt;$wgVirtualRestConfig[&#039;modules&#039;][&#039;parsoid&#039;]&amp;lt;/code&amp;gt; to point to the Docker container itself: &lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = $wgVirtualRestConfig[&#039;modules&#039;][&#039;parsoid&#039;] = array(&lt;br /&gt;
    // URL to the Parsoid instance&lt;br /&gt;
    &#039;url&#039; =&amp;gt; &#039;http://localhost:8080/rest.php&#039;,&lt;br /&gt;
);&lt;br /&gt;
| lang = php&lt;br /&gt;
}}&lt;br /&gt;
This however resulted in another error 400 which looking at the web browser developer console shows &amp;quot;The requested relative path (...) did not match any known handler&amp;quot;. I did see the rest.php calls land on the web server of the container. This was eventually fixed by loading the Parsoid extension in LocalSettings.php. What I have in my LocalSettings.php includes the following lines:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = wfLoadExtension(&#039;VisualEditor&#039;);&lt;br /&gt;
wfLoadExtension( &#039;Parsoid&#039;, &#039;vendor/wikimedia/parsoid/extension.json&#039; );&lt;br /&gt;
$wgDefaultUserOptions[&#039;visualeditor-enable&#039;] = 1;&lt;br /&gt;
| lang = php&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
===Updating from 1.37 to 1.39===&lt;br /&gt;
Skin deprecation issues. To hide deprecation notices, I added the following lines to LocalSettings.php:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = $wgShowExceptionDetails = false;&lt;br /&gt;
$wgDeprecationReleaseLimit = &#039;1.30&#039;;&lt;br /&gt;
| lang = php&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Corrupt objectcache table ===&lt;br /&gt;
For some odd reason, my Wiki&#039;s objectcache table is randomly getting corrupted. This can be recovered from if you drop and re-create the objectcache table.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # mariadb -u root -p$MYSQL_ROOT_PASSWORD&lt;br /&gt;
MariaDB [(none)]&amp;gt; use wiki&lt;br /&gt;
MariaDB [wiki]&amp;gt;  DROP TABLE IF EXISTS `objectcache`;&lt;br /&gt;
MariaDB [wiki]&amp;gt;  CREATE TABLE `objectcache` (&lt;br /&gt;
  `keyname` varbinary(255) NOT NULL DEFAULT &#039;&#039;,&lt;br /&gt;
  `value` mediumblob DEFAULT NULL,&lt;br /&gt;
  `exptime` binary(14) NOT NULL,&lt;br /&gt;
  `modtoken` varbinary(17) NOT NULL DEFAULT &#039;00000000000000000&#039;,&lt;br /&gt;
  `flags` int(10) unsigned DEFAULT NULL,&lt;br /&gt;
  PRIMARY KEY (`keyname`),&lt;br /&gt;
  KEY `exptime` (`exptime`)&lt;br /&gt;
) ENGINE=InnoDB DEFAULT CHARSET=binary;&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Mariadb 12 ===&lt;br /&gt;
Mariadb 12 is not supported by MediaWiki (as of the time of writing, MediaWiki 1.44). I had some docker-compose files which references &amp;lt;code&amp;gt;mariadb:latest&amp;lt;/code&amp;gt; which was recently changed over to verion 12 and this caused MediaWiki to think the database is in read-only mode. This resulted in this warning when trying to edit a page:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = The primary database server is running in read-only mode.&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
The fix is to downgrade the docker image back down to Mariadb 11. Fortunately, everything works even after Mariadb 12&#039;s database migration.{{Navbox Web}}&lt;br /&gt;
[[Category:WebApp]]&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Proxmox&amp;diff=7705</id>
		<title>Proxmox</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Proxmox&amp;diff=7705"/>
		<updated>2025-08-11T20:48:42Z</updated>

		<summary type="html">&lt;p&gt;Leo: PCIe bus error resolved&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Proxmox Virtual Environment (VE) is an open source virtualization management software that allows deployment and management of KVM-based virtual machines and LXC containers. The platform itself run on a Debian based distribution and supports filesystems such as ZFS and Ceph. &lt;br /&gt;
&lt;br /&gt;
In terms of functionality, Proxmox VE is similar to VMware ESXi with some overlap with vSphere. You can have a cluster of Proxmox servers controlled and managed through a singular web console, command line tools, or via the provided REST API.&lt;br /&gt;
&lt;br /&gt;
==Installation==&lt;br /&gt;
Installation is as simple as installing any other Linux distribution. Download the Proxmox iso image from https://www.proxmox.com/en/downloads. After installation, you should be able to access the web console via HTTPS on port 8006.&lt;br /&gt;
&lt;br /&gt;
==Tasks and How-Tos==&lt;br /&gt;
&lt;br /&gt;
===Remove the subscription nag===&lt;br /&gt;
If your Proxmox server has no active subscription, you will be nagged every time you log in to the web interface. This can be disabled by running the following as root in the server&#039;s console:&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # sed -Ezi.bak &amp;quot;s/(Ext.Msg.show\(\{\s+title: gettext\(&#039;No valid sub)/void\(\{ \/\/\1/g&amp;quot; /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
You will have to do this after updating Proxmox again. &lt;br /&gt;
&lt;br /&gt;
There is also an ansible role which does this at: https://github.com/FuzzyMistborn/ansible-role-proxmox-nag-removal/blob/master/tasks/remove-nag.yml&lt;br /&gt;
&lt;br /&gt;
===Make a VM or LXC start automatically at boot===&lt;br /&gt;
You can enable automatic startup and the startup order for VMs and LXCs under the &#039;Options&#039; panel. [[File:Proxmox start VM at boot.png|alt=Proxmox start VM or container at boot|none|thumb|Proxmox start VM or container at boot]]&lt;br /&gt;
&lt;br /&gt;
===Adding additional LXC templates===&lt;br /&gt;
Proxmox officially supports about a dozen different Linux distributions and provides up to date LXC templates through their repositories. These LXC templates can be downloaded using the &amp;lt;code&amp;gt;pveam&amp;lt;/code&amp;gt; tool or through the Proxmox web interface.&lt;br /&gt;
&lt;br /&gt;
====Adding templates using the web interface====&lt;br /&gt;
[[File:Proxmox CT Templates.png|alt=Download additional Proxmox CT Templates via the web interface|thumb|Download additional Proxmox CT Templates via the web interface]]&lt;br /&gt;
Go to your local storage and click on CT Templates. Click on the Templates button to see available templates.&lt;br /&gt;
&lt;br /&gt;
====Adding templates using the pveam utility====&lt;br /&gt;
The Proxmox VE Appliance Manager (&amp;lt;code&amp;gt;pvadm&amp;lt;/code&amp;gt;) tool is available to manage container templates from Proxmox&#039;s repository. More information at https://pve.proxmox.com/pve-docs/pveam.1.html.&lt;br /&gt;
&lt;br /&gt;
Run &amp;lt;code&amp;gt;pveam available&amp;lt;/code&amp;gt; to list all available templates, then run &amp;lt;code&amp;gt;pveadm download $storage $template&amp;lt;/code&amp;gt; to download a template to a storage pool. For example:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = root@server:~# pveam available&lt;br /&gt;
mail            proxmox-mailgateway-6.4-standard_6.4-1_amd64.tar.gz&lt;br /&gt;
mail            proxmox-mailgateway-7.0-standard_7.0-1_amd64.tar.gz&lt;br /&gt;
system          almalinux-8-default_20210928_amd64.tar.xz&lt;br /&gt;
system          alpine-3.12-default_20200823_amd64.tar.xz&lt;br /&gt;
system          alpine-3.13-default_20210419_amd64.tar.xz&lt;br /&gt;
system          alpine-3.14-default_20210623_amd64.tar.xz&lt;br /&gt;
system          alpine-3.15-default_20211202_amd64.tar.xz&lt;br /&gt;
system          archlinux-base_20210420-1_amd64.tar.gz&lt;br /&gt;
&lt;br /&gt;
root@server:~# pveam download local fedora-35-default_20211111_amd64.tar.xz&lt;br /&gt;
downloading http://download.proxmox.com/images/system/fedora-35-default_20211111_amd64.tar.xz to /var/lib/vz/template/cache/fedora-35-default_20211111_amd64.tar.xz&lt;br /&gt;
--2021-12-29 15:17:28--  http://download.proxmox.com/images/system/fedora-35-default_20211111_amd64.tar.xz&lt;br /&gt;
Resolving download.proxmox.com (download.proxmox.com)... 144.217.225.162, 2607:5300:203:7dc2::162&lt;br /&gt;
Connecting to download.proxmox.com (download.proxmox.com){{!}}144.217.225.162{{!}}:80... connected.&lt;br /&gt;
HTTP request sent, awaiting response... 200 OK&lt;br /&gt;
Length: 89702020 (86M) [application/octet-stream]&lt;br /&gt;
Saving to: &#039;/var/lib/vz/template/cache/fedora-35-default_20211111_amd64.tar.xz.tmp.1053836&#039;&lt;br /&gt;
     0K ........ ........ ........ ........ 37% 2.59M 21s&lt;br /&gt;
 32768K ........ ........ ........ ........ 74% 13.6M 5s&lt;br /&gt;
 65536K ........ ........ .....            100% 20.5M=16s&lt;br /&gt;
2021-12-29 15:17:44 (5.42 MB/s) - &#039;/var/lib/vz/template/cache/fedora-35-default_20211111_amd64.tar.xz.tmp.1053836&#039; saved [89702020/89702020]&lt;br /&gt;
calculating checksum...OK, checksum verified&lt;br /&gt;
download of &#039;http://download.proxmox.com/images/system/fedora-35-default_20211111_amd64.tar.xz&#039; to &#039;/var/lib/vz/template/cache/fedora-35-default_20211111_amd64.tar.xz&#039; finished&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
===Using Terraform with Proxmox===&lt;br /&gt;
You can use Terraform to manage VMs on Proxmox. More information on how to set this up on the [[Terraform]] page. Setup is relatively straight forward and the Terraform plugin for Proxmox is feature rich.&lt;br /&gt;
&lt;br /&gt;
On a related note, you can also use Packer to build VM images. The Packer-Proxmox plugin supports keyboard autotyping which makes automatically building VMs all the more simple.&lt;br /&gt;
&lt;br /&gt;
=== Install SSL certificates ===&lt;br /&gt;
Out of the box, Proxmox will generate a self signed SSL certificate for its management interfaces. This is sufficient from a security standpoint but will result in a SSL certificate warning every time you connect. You may wish to install a signed certificate from a trusted CA to correct this issue.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Note:&#039;&#039;&#039; Changing the SSL certificates prevented my SPICE clients from connecting. I had to revert back to the self generated certificates for them to work again. If you are using SPICE for remote desktkop, you may need to do further testing before rolling it out into production.&lt;br /&gt;
&lt;br /&gt;
To install a new set of SSL certificates, log in to Proxmox via SSH and copy your SSL private key and certificates to &amp;lt;code&amp;gt;/etc/pve/nodes/$node/pve-ssl.{pem,key}&amp;lt;/code&amp;gt;. Replace &#039;$node&#039; with the hostname of your Proxmox machine:&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;pve-ssl.pem&amp;lt;/code&amp;gt; file should contain your full certificate chain. This can be generated by concatenating your primary certificate, followed by any intermediate certificates.&lt;br /&gt;
* The &amp;lt;code&amp;gt;pve-ssl.key&amp;lt;/code&amp;gt; file should contain your private key without a password.&lt;br /&gt;
&lt;br /&gt;
Restart the pveproxy service to apply. If you have any problems, check the service&#039;s status with &amp;lt;code&amp;gt;systemctl status pveproxy&amp;lt;/code&amp;gt;.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # cp fullchain.pem /etc/pve/nodes/&amp;lt;node&amp;gt;/pve-ssl.pem&lt;br /&gt;
# cp private-key.pem /etc/pve/nodes/&amp;lt;node&amp;gt;/pve-ssl.key&lt;br /&gt;
# systemctl restart pveproxy&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
=== Reduce ZFS ARC memory requirement ===&lt;br /&gt;
By default, ZFS&#039;s ARC uses 50% of the host&#039;s memory. If you are running VMs using a ZFS storage pool that exceeds 50% of your system&#039;s available memory, you will need to tweak the amount of memory assigned to ARC to prevent out of memory issues.&lt;br /&gt;
&lt;br /&gt;
The minimum memory assigned to ARC as per Proxmox&#039;s documentation is 2GB + 1GB per TB of storage.&lt;br /&gt;
&lt;br /&gt;
To change the ARC max size to 3 GB: &amp;lt;code&amp;gt;echo &amp;quot;$[3 * 1024*1024*1024]&amp;quot; &amp;gt; /sys/module/zfs/parameters/zfs_arc_max&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To make this permanent, edit &amp;lt;code&amp;gt;/etc/modprobe.d/zfs.conf&amp;lt;/code&amp;gt; and add &amp;lt;code&amp;gt;options zfs zfs_arc_max=3221225472&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Enable QEMU VNC server on a guest VM ===&lt;br /&gt;
There are a few solutions when you want to remote desktop into a VM such as running a RDP/VNC server within the VM or leverage the SPICE display/connection option built into Proxmox. These have their own advantages and disadvantages.&lt;br /&gt;
&lt;br /&gt;
I have recently started using the built-in VNC server that can be enabled on the KVM/QEMU config. The benefit with this approach is that you can control the VM during the boot process as you&#039;re seeing and controlling the VM as you would in the Proxmox web interface.&lt;br /&gt;
&lt;br /&gt;
To enable VNC on a particular VM:&lt;br /&gt;
&lt;br /&gt;
# Edit &amp;lt;code&amp;gt;/etc/pve/local/qemu-server/&amp;lt;vmid&amp;gt;.conf&amp;lt;/code&amp;gt; and the following &amp;lt;code&amp;gt;args&amp;lt;/code&amp;gt; line to enable the VNC server on port 5901 (The port here is the VNC display port and you typically just add 5900 to it. Therefore, port 1 is really TCP port 5901). You may change this port number if you have other VNC servers running. &amp;lt;code&amp;gt;args: -vnc 0.0.0.0:1,password=on&amp;lt;/code&amp;gt;&lt;br /&gt;
# In the same .conf file, add the following line to set the VNC password when the VM starts up. &amp;lt;code&amp;gt;hookscript: local:snippets/set_vnc_password.sh&amp;lt;/code&amp;gt;&lt;br /&gt;
# Create the snippet by creating a file at &amp;lt;code&amp;gt;/var/lib/vz/snippets/set_vnc_password.sh&amp;lt;/code&amp;gt;. Put the following in the script. Note the VNC password is set in plain text here.{{Highlight&lt;br /&gt;
| code = #!/bin/bash&lt;br /&gt;
&lt;br /&gt;
if [[ &amp;quot;$2&amp;quot; != &amp;quot;post-start&amp;quot; ]] ; then&lt;br /&gt;
        echo &amp;quot;Not at post-start stage. Skipping&amp;quot;&lt;br /&gt;
        exit 0&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
VM_ID=$1&lt;br /&gt;
VNC_PASSWORD=myvncpassword&lt;br /&gt;
&lt;br /&gt;
expect -c &amp;quot;&lt;br /&gt;
set timeout 5&lt;br /&gt;
spawn qm monitor $VM_ID&lt;br /&gt;
expect \&amp;quot;qm&amp;gt;\&amp;quot;&lt;br /&gt;
send \&amp;quot;set_password vnc $VNC_PASSWORD -d vnc2\r\&amp;quot;&lt;br /&gt;
expect \&amp;quot;qm&amp;gt;\&amp;quot;&lt;br /&gt;
send \&amp;quot;exit\r\&amp;quot;&lt;br /&gt;
exit&lt;br /&gt;
&amp;quot;&lt;br /&gt;
&lt;br /&gt;
echo&lt;br /&gt;
echo &amp;quot;VNC password set.&amp;quot;&lt;br /&gt;
| lang = bash&lt;br /&gt;
}}&lt;br /&gt;
# Test this by booting the VM. You should be able to connect to the Proxmox server on port 5901 using VNC and authenticate using the password set in the script.&lt;br /&gt;
&lt;br /&gt;
==== VNC server listening on a local socket ====&lt;br /&gt;
Alternatively, you may have the VNC server listen on a local socket. To do so, adjust the args with the following:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ## Listen on a socket (worked for me)&lt;br /&gt;
args: -object secret,id=vncpass,data=password -vnc unix:/var/run/qemu-server/$vmid-secondary.vnc,password-secret=vncpass&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
You may need to use the same snippet script above to set the password since libvirt/Proxmox may set a random passphrase on the VNC server.&lt;br /&gt;
&lt;br /&gt;
With the VNC server running and listening on a socket, you may expose the socket as a TCP port using something like &amp;lt;code&amp;gt;socat&amp;lt;/code&amp;gt;:{{Highlight&lt;br /&gt;
| code = # socat tcp4-listen:5915,fork,reuseaddr unix-connect:/var/run/qemu-server/115-secondary.vnc&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Attach a disk from another VM ===&lt;br /&gt;
To attach a disk to another VM, you must first detach the disk from the source VM. To do so, navigate to the VM&#039;s hardware panel, select the disk to detach, then click on &#039;Disk Actions&#039; and then &#039;Detach&#039;. Then, go to &#039;Disk Action&#039; -&amp;gt; &#039;Reassign Disk&#039; and select the destination VM.&lt;br /&gt;
&lt;br /&gt;
==== The manual process ====&lt;br /&gt;
&#039;&#039;&#039;Note:&#039;&#039;&#039; Not recommended now. This was done before the reassign disk was a feature. These steps are here for legacy reasons.&lt;br /&gt;
&lt;br /&gt;
If you are using ZFS for the VM disks, you can attach a disk from the source VM to another target VM by renaming the disk&#039;s VM ID to the target VM. You can run &amp;lt;code&amp;gt;zfs list&amp;lt;/code&amp;gt; on the Proxmox server to see what disks are available. Once the disk is renamed, the disk can be made visible as an unused disk by running &amp;lt;code&amp;gt;qm rescan&amp;lt;/code&amp;gt; to rescan and update the VM configs. From the Proxmox console, you should then be able to attach the unused disk under the hardware page.&lt;br /&gt;
&lt;br /&gt;
For example, to attach a VM disk which was originally connected to VM 101 to VM 102, I had to rename the disk &amp;lt;code&amp;gt;vm-101-disk-0&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;vm-102-disk-1&amp;lt;/code&amp;gt; by running: &amp;lt;code&amp;gt;zfs rename data/vm-101-disk-0 data/vm-102-disk-1&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Renumber VM IDs ===&lt;br /&gt;
Use the following script to renumber a VM&#039;s ID. This script supports VMs that are stored on both LVM and ZFS. &#039;&#039;&#039;Power off the VM before proceeding&#039;&#039;&#039;. Proxmox should be able to see the VM being renamed automatically.&lt;br /&gt;
&lt;br /&gt;
Be aware that renumbering a VM may cause Cloud-Init to re-run again as it may think it&#039;s on a new instance. This could have unintended side effects such as having your system&#039;s SSH host keys wiped and accounts&#039; passwords reset.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = #!/bin/bash&lt;br /&gt;
Old=&amp;quot;$1&amp;quot;&lt;br /&gt;
New=&amp;quot;$2&amp;quot;&lt;br /&gt;
&lt;br /&gt;
usage(){&lt;br /&gt;
        echo &amp;quot;Usage: $0 old-vmid new-vmid&amp;quot;&lt;br /&gt;
        exit 2&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
if [ -z &amp;quot;$Old&amp;quot; ] {{!}}{{!}} ! expr &amp;quot;$Old&amp;quot; : &#039;^[0-9]\+$&#039; &amp;gt;/dev/null ; then&lt;br /&gt;
        echo &amp;quot;Error: Invalid old-vimd&amp;quot;&lt;br /&gt;
        usage&lt;br /&gt;
&lt;br /&gt;
        if [ ! -f /etc/pve/qemu-server/$Old.conf ] ; then&lt;br /&gt;
                echo &amp;quot;Error: $Old is not a valid VM ID&amp;quot;&lt;br /&gt;
                exit 1&lt;br /&gt;
        fi&lt;br /&gt;
fi&lt;br /&gt;
if [ -z &amp;quot;$New&amp;quot; ] {{!}}{{!}} ! expr &amp;quot;$New&amp;quot; : &#039;^[0-9]\+$&#039; &amp;gt;/dev/null ; then&lt;br /&gt;
        echo &amp;quot;Error: Invalid new-vimd&amp;quot;&lt;br /&gt;
        usage&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Found these disks in the config:&amp;quot;&lt;br /&gt;
cat /etc/pve/qemu-server/$Old.conf {{!}} grep -i -- vm-$Old- {{!}} awk -F, &#039;{print $1}&#039; {{!}} awk &#039;{print $2}&#039;&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Found these disks on LVM:&amp;quot;&lt;br /&gt;
lvs --noheadings -o lv_name,vg_name {{!}} grep &amp;quot;vm-$Old-&amp;quot;&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Found these disks on ZFS:&amp;quot;&lt;br /&gt;
zfs list {{!}} grep &amp;quot;vm-$Old-&amp;quot; {{!}} awk &#039;{print $1}&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Will execute the following:&amp;quot;&lt;br /&gt;
echo&lt;br /&gt;
lvs --noheadings -o lv_name,vg_name {{!}} grep &amp;quot;vm-$Old-&amp;quot; {{!}} while read lv vg ; do&lt;br /&gt;
        echo lvrename $vg/$lv $vg/$(echo $lv {{!}} sed &amp;quot;s/$Old/$New/g&amp;quot;)&lt;br /&gt;
done&lt;br /&gt;
zfs list {{!}} grep &amp;quot;vm-$Old-&amp;quot; {{!}} awk &#039;{print $1}&#039; {{!}} while read i ; do&lt;br /&gt;
        echo zfs rename $i $(echo $i {{!}} sed &amp;quot;s/$Old/$New/g&amp;quot;)&lt;br /&gt;
done&lt;br /&gt;
echo sed -i &amp;quot;s/$Old/$New/g&amp;quot; /etc/pve/qemu-server/$Old.conf&lt;br /&gt;
echo mv /etc/pve/qemu-server/$Old.conf /etc/pve/qemu-server/$New.conf&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
echo&lt;br /&gt;
read -p &amp;quot;Proceed with the rename? (y/n) &amp;quot; answer&lt;br /&gt;
case $answer in&lt;br /&gt;
  [yY]* ) echo &amp;quot;Proceeding...&amp;quot;;;&lt;br /&gt;
  * ) echo &amp;quot;Aborting...&amp;quot;; exit;;&lt;br /&gt;
esac&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Task: Rename LVS data volumes&amp;quot;&lt;br /&gt;
lvs --noheadings -o lv_name,vg_name {{!}} grep &amp;quot;vm-$Old-&amp;quot; {{!}} while read lv vg ; do&lt;br /&gt;
        lvrename $vg/$lv $vg/$(echo $lv {{!}} sed &amp;quot;s/$Old/$New/g&amp;quot;)&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Task: Rename ZFS datasets&amp;quot;&lt;br /&gt;
zfs list {{!}} grep &amp;quot;vm-$Old-&amp;quot; {{!}} awk &#039;{print $1}&#039; {{!}} while read i ; do&lt;br /&gt;
        zfs rename $i $(echo $i {{!}} sed &amp;quot;s/$Old/$New/g&amp;quot;)&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Renumbering IDs&amp;quot;&lt;br /&gt;
sed -i &amp;quot;s/$Old/$New/g&amp;quot; /etc/pve/qemu-server/$Old.conf&lt;br /&gt;
mv /etc/pve/qemu-server/$Old.conf /etc/pve/qemu-server/$New.conf&lt;br /&gt;
| lang = bash&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Remove a single node cluster ===&lt;br /&gt;
If you created a single node cluster and want to undo it, run:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # systemctl stop pve-cluster corosync&lt;br /&gt;
# pmxcfs -l&lt;br /&gt;
# rm /etc/corosync/*&lt;br /&gt;
# rm /etc/pve/corosync.conf&lt;br /&gt;
# killall pmxcfs&lt;br /&gt;
# systemctl start pve-cluster&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
This was copied from: https://forum.proxmox.com/threads/proxmox-ve-6-removing-cluster-configuration.56259/&lt;br /&gt;
&lt;br /&gt;
=== Run Docker in LXC ===&lt;br /&gt;
Docker works in Proxmox LXC but only if the nesting option is enabled. Docker works on either privileged LXC or unprivileged LXC but each has their own caveats:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Feature&lt;br /&gt;
!Privileged&lt;br /&gt;
!Unprivileged&lt;br /&gt;
|-&lt;br /&gt;
|&#039;&#039;&#039;NFS&#039;&#039;&#039;&lt;br /&gt;
|Can mount NFS; requires nfs=1 enabled in LXC configuration&lt;br /&gt;
|Cannot mount NFS unless you use a bind mountpoint&lt;br /&gt;
|-&lt;br /&gt;
|&#039;&#039;&#039;Security&#039;&#039;&#039;&lt;br /&gt;
|UIDs are not mapped; may be dangerous&lt;br /&gt;
|UIDs/GIDs are mapped&lt;br /&gt;
|-&lt;br /&gt;
|&#039;&#039;&#039;OverlayFS&#039;&#039;&#039;&lt;br /&gt;
|ZFS backing filesystem for &amp;lt;code&amp;gt;/var/lib/docker&amp;lt;/code&amp;gt; should have no issues.&lt;br /&gt;
|&amp;lt;code&amp;gt;/var/lib/docker&amp;lt;/code&amp;gt; should use EXT or XFS (or some non-ZFS filesystem, more on this below.)&lt;br /&gt;
|}&lt;br /&gt;
It&#039;s recommended that you do not run Docker in LXC unless you have a good reason such as: a) hardware passthrough on systems that cannot support it through VMs, or b) tight memory constraints where containerized workloads benefit from shared memory.&lt;br /&gt;
&lt;br /&gt;
==== Issues with /var/lib/docker on a ZFS backed storage ====&lt;br /&gt;
Docker uses overlayfs to handle the container layers. On ZFS backed storage, it requires additional privileges to allow it to work properly. For LXC that are unprivileged, certain docker image pulls may fail if the image data is stored on ZFS storage. To work around this issue, you will have to use a different filesystem for &amp;lt;code&amp;gt;/var/lib/docker&amp;lt;/code&amp;gt; by making a zvol and then formatting it with a different filesystem (see https://du.nkel.dev/blog/2021-03-25_proxmox_docker/). Alternatively, put the &amp;lt;code&amp;gt;/var/lib/docker&amp;lt;/code&amp;gt; volume on a non-ZFS backed storage such as a LVM backed storage.  &lt;br /&gt;
&lt;br /&gt;
This issue manifests itself as issues when pulling some (but not all) container images with errors similar to: &lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = [root@nixos:~]# docker-compose pull&lt;br /&gt;
[+] Pulling 8/10&lt;br /&gt;
 ⠋ pihole 9 layers [⣿⣿⣿⣿⣿⣿⣿⣿⣿] 23.91MB/23.91MB Pulling                                                                 &lt;br /&gt;
   ✔ f03b40093957 Pull complete                                                                                        &lt;br /&gt;
   ✔ 8063479210c7 Pull complete                                                                                        &lt;br /&gt;
   ✔ 4f4fb700ef54 Pull complete                                                                                        &lt;br /&gt;
   ✔ 061a6a1d9010 Pull complete                                                                                        &lt;br /&gt;
   ✔ 8b1e64a56394 Pull complete                                                                                        &lt;br /&gt;
   ✔ 8dabcf07e578 Pull complete                                                                                        &lt;br /&gt;
   ⠿ bdec3efaf98a Extracting      [==================================================&amp;gt;]  23.91MB/23.91MB               &lt;br /&gt;
   ✔ 40cba0bade6e Download complete                                                                                    &lt;br /&gt;
   ✔ 9b797b6be3f3 Download complete                                                                                    &lt;br /&gt;
failed to register layer: ApplyLayer exit status 1 stdout:  stderr: unlinkat /var/cache/apt/archives: invalid argument&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Switching &amp;lt;code&amp;gt;/var/lib/docker&amp;lt;/code&amp;gt; to a ext backed filesystem fixed the issue. This can be done with the following steps:&lt;br /&gt;
&lt;br /&gt;
# Create the zvol:  &amp;lt;code&amp;gt;zfs create -V 8G rpool/data/ctvol-111-disk-1&amp;lt;/code&amp;gt;&lt;br /&gt;
# Format the zvol: &amp;lt;code&amp;gt;mkfs.ext4 /dev/zvol/rpool/data/ctvol-111-disk-1&amp;lt;/code&amp;gt;&lt;br /&gt;
# Edit the LXC config under &amp;lt;code&amp;gt;/etc/pve/nodes/pve/lxc/111.conf&amp;lt;/code&amp;gt; and add the following mountpoint config: &lt;br /&gt;
#* &amp;lt;code&amp;gt;mp0: /dev/zvol/rpool/data/ctvol-111-disk-1,mp=/var/lib/docker&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Install NVIDIA driver in Proxmox ===&lt;br /&gt;
The NVIDIA Linux driver requires the following dependencies:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # apt install gcc make pve-headers&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
As part of the installation, the driver will ensure nouveau is disabled. It&#039;ll blacklist the nouveau kernel module for you but you will have to reboot (or &amp;lt;code&amp;gt;rmmod&amp;lt;/code&amp;gt; it) before running the installer again.&lt;br /&gt;
&lt;br /&gt;
Once you have the NVIDIA drivers installed, to hardware accelerate your VM:&lt;br /&gt;
&lt;br /&gt;
* Set the graphics card of the VM to VirGL GPU&lt;br /&gt;
* Install the needed dependencies: &amp;lt;code&amp;gt;apt install libgl1 libegl1&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Setup LXC with NVIDIA GPU ===&lt;br /&gt;
You can share your NVIDIA GPU with one or more LXC in Proxmox. This is possible only if the following conditions are met:&lt;br /&gt;
&lt;br /&gt;
# NVIDIA drivers are installed on Proxmox. {{Highlight&lt;br /&gt;
| code = # wget https://us.download.nvidia.com/XFree86/Linux-x86_64/535.54.03/NVIDIA-Linux-x86_64-535.54.03.run&lt;br /&gt;
# chmod 755 NVIDIA-Linux-x86_64-535.54.03.run&lt;br /&gt;
# ./NVIDIA-Linux-x86_64-535.54.03.run&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# The &amp;lt;u&amp;gt;exact same&amp;lt;/u&amp;gt; NVIDIA drivers are installed in the LXC. (Basically, run the same thing as above. If you&#039;re using NixOS, you might have to do some overrides or match the drivers on Proxmox to what was pulled in when you do nixos-rebuild switch).&lt;br /&gt;
# The NVIDIA devices is passed through to the LXC using the &amp;lt;code&amp;gt;lxc.mount.entry&amp;lt;/code&amp;gt; setting. Do a listing of all the &amp;lt;code&amp;gt;/dev/nvidia*&amp;lt;/code&amp;gt; devices and ensure that you catch all the device numbers.{{Highlight&lt;br /&gt;
| code = lxc.cgroup2.devices.allow: c 226:* rwm&lt;br /&gt;
lxc.cgroup2.devices.allow: c 195:* rwm&lt;br /&gt;
lxc.cgroup2.devices.allow: c 508:* rwm&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
# LXC is allowed to read the NVIDIA devices using the &amp;lt;code&amp;gt;lxc.cgroups2.devices.allow&amp;lt;/code&amp;gt; setting. Ensure that you capture all the &amp;lt;code&amp;gt;/dev/nvidia*&amp;lt;/code&amp;gt; devices that&#039;s appropriate for your card.{{Highlight&lt;br /&gt;
| code = lxc.mount.entry: /dev/dri/card1 dev/dri/card0 none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/dri/renderD129 dev/dri/renderD128 none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia-caps dev/nvidia-caps none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
# Start the LXC. You should be able to run &#039;&amp;lt;code&amp;gt;nvidia-smi&amp;lt;/code&amp;gt;&#039; from inside the container and have it see the status of the card. If it doesn&#039;t, doublecheck everything above: ensure your driver version numbers are matching (it would tell you if it didn&#039;t) and that you have the appropriate device numbers allowed.&lt;br /&gt;
&lt;br /&gt;
==== How to set up Jellyfin on a Ubuntu based LXC ====&lt;br /&gt;
I used to run Jellyfin under a Docker VM with a NVIDIA GPU using PCIe passthrough, but I wanted to try running jellyfin on another machine which doesn&#039;t support iommu. To work around this, I attempted to run Jellyfin within a LXC. &lt;br /&gt;
&lt;br /&gt;
What I did:&lt;br /&gt;
&lt;br /&gt;
# Create a LXC as a privileged container. Two reasons for this: 1) The device passthrough sort of needs it and 2) I need to mount my Jellyfin media files via NFS&lt;br /&gt;
# Set up LXC with a Ubuntu image. Install Jellyfin as per the https://jellyfin.org/docs/general/installation/linux/#ubuntu instructions. Additionally, install the &amp;lt;code&amp;gt;jellyfin-ffmpeg5 libnvidia-decode-525 libnvidia-encode-525 nvidia-utils-525&amp;lt;/code&amp;gt; packages for GPU support. I used version 525 as that&#039;s the most recent stable version offered by NVIDIA. Note that because we&#039;re using these pre-packaged binaries, the driver you install on the PVE must match the exact version.&lt;br /&gt;
# Look at the specific version of the nvidia packages that was installed in the previous step. You can also do a apt search libnvidia-encode to see what versions are available. Download and install the NVIDIA drivers on the Proxmox server itself while ensuring you get the exact version that matches the packages you installed in the LXC environment.&lt;br /&gt;
# Manually edit the LXC config under /etc/pve/lxc/&amp;lt;id&amp;gt;.conf and add: {{Highlight&lt;br /&gt;
| code = lxc.cgroup2.devices.allow: c 226:* rwm&lt;br /&gt;
lxc.cgroup2.devices.allow: c 195:* rwm&lt;br /&gt;
lxc.mount.entry: /dev/dri/card1 dev/dri/card0 none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/dri/renderD129 dev/dri/renderD128 none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia-caps dev/nvidia-caps none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file&lt;br /&gt;
| lang = text&lt;br /&gt;
}} /dev/dri/card1 and /dev/dri/renderD129 are the devices that appeared after installing the NVIDIA drivers on the PVE which made it obvious that they were what I wanted. If you&#039;re unsure, you could try unloading the Nvidia driver and see what disappears.&lt;br /&gt;
# Restart the LXC environment and see if nvidia-smi works. If it doesn&#039;t, ensure your version numbers are matching (it would tell you if it didn&#039;t) and that you have the appropriate device bind mounted into the LXC.&lt;br /&gt;
# Continue with the jellyfin installation as usual. Run: &amp;lt;code&amp;gt;wget -O- https://repo.jellyfin.org/install-debuntu.sh | sudo bash&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== How to run Docker on a NixOS based LXC ====&lt;br /&gt;
I later moved Jellyfin to NixOS running docker in LXC. This was done to simplify the Jellyfin update process and to leverage my existing docker-compose infrastructure. The NixOS LXC is privileged with nesting enabled (as both are required by Docker).&lt;br /&gt;
&lt;br /&gt;
Here is the NixOS config to get Docker installed and with the NVIDIA drivers and runtime set up:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = virtualisation.docker = {&lt;br /&gt;
    enable = true;&lt;br /&gt;
    enableNvidia = true;&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  # Make sure opengl is enabled&lt;br /&gt;
  hardware.opengl = {&lt;br /&gt;
    enable = true;&lt;br /&gt;
    driSupport = true;&lt;br /&gt;
    driSupport32Bit = true;&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  # NVIDIA drivers are unfree.&lt;br /&gt;
  nixpkgs.config.allowUnfreePredicate = pkg:&lt;br /&gt;
    builtins.elem (lib.getName pkg) [&lt;br /&gt;
      &amp;quot;nvidia-x11&amp;quot;&lt;br /&gt;
      &amp;quot;nvidia-settings&amp;quot;&lt;br /&gt;
      &amp;quot;nvidia-persistenced&amp;quot;&lt;br /&gt;
    ];&lt;br /&gt;
&lt;br /&gt;
  # Tell Xorg to use the nvidia driver&lt;br /&gt;
  services.xserver.videoDrivers = [&amp;quot;nvidia&amp;quot;];&lt;br /&gt;
&lt;br /&gt;
  hardware.nvidia = {&lt;br /&gt;
      package = config.boot.kernelPackages.nvidiaPackages.production;&lt;br /&gt;
   };&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
After doing a NixOS rebuild, docker works and docker containers with the nvidia runtime also works.&lt;br /&gt;
&lt;br /&gt;
==== Troubleshooting NVIDIA LXC issues ====&lt;br /&gt;
&lt;br /&gt;
===== Missing nvidia-uvm =====&lt;br /&gt;
You also need the cuda device &amp;lt;code&amp;gt;nvidia-uvm&amp;lt;/code&amp;gt; passed through. &lt;br /&gt;
&lt;br /&gt;
Symptoms of having this device missing are the ffmpeg bundled with Jellyfin with cuda support will fail with this error message:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = [AVHWDeviceContext @ 0x55eea5d88f80] cu-&amp;gt;cuInit(0) failed -&amp;gt; CUDA_ERROR_UNKNOWN: unknown error&lt;br /&gt;
Device creation failed: -542398533.&lt;br /&gt;
Failed to set value &#039;cuda=cu:0&#039; for option &#039;init_hw_device&#039;: Generic error in an external library&lt;br /&gt;
Error parsing global options: Generic error in an external library&lt;br /&gt;
| lang = text&lt;br /&gt;
}}If the device isn&#039;t missing but something is misconfigured, strace would show the process attempting to read but failing:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = openat(AT_FDCWD, &amp;quot;/dev/nvidia-uvm&amp;quot;, O_RDWR{{!}}O_CLOEXEC) = -1 EPERM (Operation not permitted)&lt;br /&gt;
openat(AT_FDCWD, &amp;quot;/dev/nvidia-uvm&amp;quot;, O_RDWR) = -1 EPERM (Operation not permitted)&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
====== The fix ======&lt;br /&gt;
If the device &amp;lt;code&amp;gt;/dev/nvidia-uvm&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;/dev/nvidia-uvm-tools&amp;lt;/code&amp;gt; isn&#039;t available on the PVE (ie. it&#039;s missing), then you will have to run this:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ## Get the device number&lt;br /&gt;
# grep nvidia-uvm /proc/devices {{!}} awk &#039;{print $1}&#039;&lt;br /&gt;
&lt;br /&gt;
## Then create the device node using the device number above (508 for me, but this can change!)&lt;br /&gt;
#  mknod -m 666 /dev/nvidia-uvm c 508 0&lt;br /&gt;
#  mknod -m 666 /dev/nvidia-uvm-tools c 508 0&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
See also: https://old.reddit.com/r/qnap/comments/s7bbv6/fix_for_missing_nvidiauvm_device_devnvidiauvm/&lt;br /&gt;
&lt;br /&gt;
If you&#039;re using LXC, you also have to edit the LXC config file (at &amp;lt;code&amp;gt;/etc/pve/nodes/pve/lxc/###.conf&amp;lt;/code&amp;gt;) and add the following. Adjust the device numbers as needed.&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = lxc.cgroup2.devices.allow: c 226:* rwm&lt;br /&gt;
lxc.cgroup2.devices.allow: c 195:* rwm&lt;br /&gt;
lxc.cgroup2.devices.allow: c 510:* rwm&lt;br /&gt;
lxc.mount.entry: /dev/dri/card1 dev/dri/card0 none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/dri/renderD129 dev/dri/renderD128 none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia-caps dev/nvidia-caps none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Randomly losing NVIDIA device access ====&lt;br /&gt;
Whenever you change a LXC setting (such as memory or CPU change), it will break access to the NVIDIA GPUs from within the LXC. Any existing processes with the NVIDIA GPU opened will continue to work, but any new processes trying to access the GPU will fail. The only fix is to restart the LXC.&lt;br /&gt;
&lt;br /&gt;
=== Replace a failed ZFS mirror boot device ===&lt;br /&gt;
If you are using ZFS mirror as the OS boot device and need to replace one of the disks in the mirror, keep in mind the following:&lt;br /&gt;
&lt;br /&gt;
* Each disk in the ZFS mirror has 3 partitions: 1. BIOS boot, 2. grub, 3. ZFS block device. When you replace a disk, you will have to recreate this partition scheme before you can replace the ZFS device using the 3rd partition.&lt;br /&gt;
* Grub is installed on the 2nd partition on all disks in the ZFS mirror. If you are replacing the disk that your BIOS uses to boot, you should still  be able to boot Proxmox by booting off the other disks from the boot menu. After replacing the failed disk, you can reinstall the grub bootloader.&lt;br /&gt;
&lt;br /&gt;
The steps to replace a failed disk in a ZFS mirror that&#039;s used as the boot device on Proxmox are:&lt;br /&gt;
&lt;br /&gt;
# Remove the failed device and replace it with the new device&lt;br /&gt;
# Once you see the new device on your system (with lsblk), set up the partition table: &amp;lt;code&amp;gt;sgdisk &amp;lt;healthy bootable device&amp;gt; -R &amp;lt;new device&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
# Randomize the GUIDs &amp;lt;code&amp;gt;sgdisk -G &amp;lt;healthy bootable device&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
# Replace the failed device in your zpool. Check the status with &amp;lt;code&amp;gt;zpool status&amp;lt;/code&amp;gt; and then replace the failed device with &amp;lt;code&amp;gt;zpool replace -f &amp;lt;pool&amp;gt; &amp;lt;old zfs partition&amp;gt; &amp;lt;new zfs partition&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
# Reinstall the bootloader on the grub partition on the new device by formatting the partition: &amp;lt;code&amp;gt;proxmox-boot-tool format /dev/sda2&amp;lt;/code&amp;gt;,&lt;br /&gt;
# Then reinstall grub: &amp;lt;code&amp;gt;proxmox-boot-tool init /dev/sda2&amp;lt;/code&amp;gt;&lt;br /&gt;
See Proxmox&#039;s sysadmin documentation on this topic at: https://pve.proxmox.com/pve-docs/chapter-sysadmin.html#sysboot_proxmox_boot_setup&lt;br /&gt;
&lt;br /&gt;
=== Migrating VMs from a Proxmox OS disk ===&lt;br /&gt;
I wanted to reinstall Proxmox on another disk on the same machine while preserving the VMs and containers. This required installing a clean copy of Proxmox on the new drive and then migrating all existing VMs and containers to the new install from the existing disk.&lt;br /&gt;
&lt;br /&gt;
I was able to get this all done by:&lt;br /&gt;
&lt;br /&gt;
* On the old install (while it&#039;s still running), Tar up &amp;lt;code&amp;gt;/etc/pve&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;/var/lib/rrdcached/db&amp;lt;/code&amp;gt; from the old disk. You have to do this while the old version is running since the &amp;lt;code&amp;gt;.conf&amp;lt;/code&amp;gt; files seem to disappear when the system&#039;s off (I didn&#039;t investigate why due to time constraints). Restore it to the new Proxmox install.&lt;br /&gt;
* Add the old disk back as storage on the new Proxmox install. You do not and should not need to format anything. For LVM storage, just add a LVM storage and select the appropriate device. Your VM disks should appear and you should then be able to migrate it / attach it to the VMs which you imported in the previous step.&lt;br /&gt;
&lt;br /&gt;
==Troubleshooting==&lt;br /&gt;
===LXC containers does not start networking===&lt;br /&gt;
I have a Fedora based LXC container which takes a long time to start up. After the tty does start, the only network adapter isn&#039;t up. &amp;lt;code&amp;gt;systemd-networkd&amp;lt;/code&amp;gt; is also in a a failed state with a bad exit code and status &amp;lt;code&amp;gt;226/namespace&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
This was resolved by enabling nesting on this container with the following commands: &lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # pct set $CTID -features nesting=1 &lt;br /&gt;
# pct reboot $CTID&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== PCIe passthrough fails due to platform RMRR ===&lt;br /&gt;
This is covered in more details at [[HP DL380 G6#PCIe passthrough fails due to platform RMRR requirement|HP_DL380_page on PCIe_passthrough_failing]]. Basically, the workaround for this is to install a patched kernel that doesn&#039;t respect the RMRR restrictions.&lt;br /&gt;
&lt;br /&gt;
If you want to build your own patched kernel, you might be interested in this repo: https://github.com/sarrchri/relax-intel-rmrr&lt;br /&gt;
&lt;br /&gt;
=== NVIDIA driver crashes with KVM using VirGL GPU ===&lt;br /&gt;
On Proxmox 7.4 with NVIDIA driver version 535.54.03 installed, starting a virtual machine with a VirGL GPU device results in KVM segfaulting:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = [Wed Jul 26 11:13:38 2023] kvm[559117]: segfault at 0 ip 00007ff13a39ca23 sp 00007ffd4eb76450 error 4 in libnvidia-eglcore.so.535.54.03[7ff1398bd000+164e000]&lt;br /&gt;
[Wed Jul 26 11:13:38 2023] Code: 48 8b 45 10 44 8b a0 04 0d 00 00 e9 44 fe ff ff 0f 1f 80 00 00 00 00 48 8b 77 08 48 89 df e8 e4 4d 78 ff 48 8b 7d 10 48 89 de &amp;lt;48&amp;gt; 8b 07 ff 90 98 01 00 00 48 8b 43 38 e9 f2 fd ff ff 0f 1f 00 48&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
A reboot of the Proxmox server did not help.&lt;br /&gt;
&lt;br /&gt;
Fix: I re-installed the NVIDIA drivers but also answered &#039;yes&#039; for the 32bit libraries to be installed. Perhaps that was the fix, or the act of re-installing the driver recompiled the driver on the current running kernel which fixed the issue.&lt;br /&gt;
&lt;br /&gt;
=== Network interface reset: Detected Hardware Unit Hang ===&lt;br /&gt;
I have a machine with an Intel Corporation 82579V Gigabit network adapter which periodically stutters especially when the server is under heavy load (mainly I/O with some CPU) with the following kernel message being triggered:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = [Fri Aug 18 00:05:03 2023] e1000e 0000:00:19.0 eno1: Detected Hardware Unit Hang:&lt;br /&gt;
                             TDH                  &amp;lt;1d&amp;gt;&lt;br /&gt;
                             TDT                  &amp;lt;77&amp;gt;&lt;br /&gt;
                             next_to_use          &amp;lt;77&amp;gt;&lt;br /&gt;
                             next_to_clean        &amp;lt;1c&amp;gt;&lt;br /&gt;
                           buffer_info[next_to_clean]:&lt;br /&gt;
                             time_stamp           &amp;lt;105642dfd&amp;gt;&lt;br /&gt;
                             next_to_watch        &amp;lt;1d&amp;gt;&lt;br /&gt;
                             jiffies              &amp;lt;1056437b0&amp;gt;&lt;br /&gt;
                             next_to_watch.status &amp;lt;0&amp;gt;&lt;br /&gt;
                           MAC Status             &amp;lt;40080083&amp;gt;&lt;br /&gt;
                           PHY Status             &amp;lt;796d&amp;gt;&lt;br /&gt;
                           PHY 1000BASE-T Status  &amp;lt;3800&amp;gt;&lt;br /&gt;
                           PHY Extended Status    &amp;lt;3000&amp;gt;&lt;br /&gt;
                           PCI Status             &amp;lt;10&amp;gt;&lt;br /&gt;
[Fri Aug 18 00:05:03 2023] e1000e 0000:00:19.0 eno1: Reset adapter unexpectedly&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
When this happens, all networking pauses temporarily and may cause connections to break.&lt;br /&gt;
&lt;br /&gt;
Some online research suggests the issue could be with the e1000e driver having some sort of bug which in conjunction with TCP segmentation offloading (TSO) may cause the interface to hang. The workaround is to disable hardware TSO which you can do with &amp;lt;code&amp;gt;ethool&amp;lt;/code&amp;gt;:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ethtool -K eno1 tso off gso off&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
This change will revert on the next reboot. If it helps with your situation, you can make it persistent by editing &amp;lt;code&amp;gt;/etc/network/interfaces&amp;lt;/code&amp;gt; with a &amp;lt;code&amp;gt;post-up&amp;lt;/code&amp;gt; command:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = auto eno1&lt;br /&gt;
iface eno1 inet static&lt;br /&gt;
  address 10.1.2.2&lt;br /&gt;
  netmask 255.255.252.0&lt;br /&gt;
  gateway 10.1.1.1&lt;br /&gt;
  post-up /sbin/ethtool -K eno1 tso off gso off&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== NVidia PCIe Bus Error ===&lt;br /&gt;
On my X99-E WS + Xeon E5-1660 + NVidia Quadro P400 machine running kernel 6.8.12-8-pve, which is currently my primary Proxmox box, I&#039;m now seeing a never ending stream of correctable errors from the GPU. The GPU&#039;s plugged into the first slot. Kernel messages with these errors look like this:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = root@pve:~# dmesg -T {{!}} tail&lt;br /&gt;
[Sat Aug  2 22:10:12 2025] pcieport 0000:00:03.0: AER: Correctable error message received from 0000:05:00.0&lt;br /&gt;
[Sat Aug  2 22:10:12 2025] nvidia 0000:05:00.0: PCIe Bus Error: severity=Correctable, type=Physical Layer, (Receiver ID)&lt;br /&gt;
[Sat Aug  2 22:10:12 2025] nvidia 0000:05:00.0:   device [10de:1cb3] error status/mask=00000001/0000a000&lt;br /&gt;
[Sat Aug  2 22:10:12 2025] nvidia 0000:05:00.0:    [ 0] RxErr                  (First)&lt;br /&gt;
[Sat Aug  2 22:10:14 2025] pcieport 0000:00:03.0: AER: Correctable error message received from 0000:05:00.0&lt;br /&gt;
[Sat Aug  2 22:10:14 2025] nvidia 0000:05:00.0: PCIe Bus Error: severity=Correctable, type=Physical Layer, (Receiver ID)&lt;br /&gt;
[Sat Aug  2 22:10:14 2025] nvidia 0000:05:00.0:   device [10de:1cb3] error status/mask=00000001/0000a000&lt;br /&gt;
[Sat Aug  2 22:10:14 2025] nvidia 0000:05:00.0:    [ 0] RxErr                  (First)&lt;br /&gt;
&lt;br /&gt;
root@pve:~# lspci -nnn {{!}} grep 05:00&lt;br /&gt;
05:00.0 VGA compatible controller [0300]: NVIDIA Corporation GP107GL [Quadro P400] [10de:1cb3] (rev a1)&lt;br /&gt;
05:00.1 Audio device [0403]: NVIDIA Corporation GP107GL High Definition Audio Controller [10de:0fb9] (rev a1)&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
One possible fix is to disable PCIe Active State Power Management by adding &amp;lt;code&amp;gt;pcie_aspm=off&amp;lt;/code&amp;gt; to the kernel boot arguments in &amp;lt;code&amp;gt;/etc/default/grub&amp;lt;/code&amp;gt;, then apply it to grub by running &amp;lt;code&amp;gt;update-grub&amp;lt;/code&amp;gt; before rebooting.&lt;br /&gt;
&lt;br /&gt;
A side note: adding &amp;lt;code&amp;gt;acpi_enforce_resources=no&amp;lt;/code&amp;gt; to the kernel args and rebooting did not help. There are also some suggestions to add &amp;lt;code&amp;gt;pci=noaer&amp;lt;/code&amp;gt; which will turn off error reporting. I did no try this after disabling ASPM.{{Navbox Linux}}&lt;br /&gt;
[[Category:Linux]]&lt;br /&gt;
[[Category:LinuxUtilities]]&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Thingino&amp;diff=7704</id>
		<title>Thingino</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Thingino&amp;diff=7704"/>
		<updated>2025-08-04T05:18:06Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Thingino is an open-source firmware for Ingenic based IP cameras. This is a good alternative firmware for the Wyze Cam (v2, v3, but not the v4) as it has RTSP support with an up-to-date kernel/OS.&lt;br /&gt;
&lt;br /&gt;
== Installation ==&lt;br /&gt;
Josh at WL Tech Blog has made Thingino installers for all the supported cameras available from his GitHub repo:&lt;br /&gt;
&lt;br /&gt;
* https://github.com/wltechblog/thingino-installers&lt;br /&gt;
&lt;br /&gt;
Simply find the installer for your camera and follow the instructions provided.&lt;br /&gt;
&lt;br /&gt;
=== Installation using Cloner ===&lt;br /&gt;
If you &#039;brick&#039; your T31 based camera through a bad update, you can use the Ingenic USB Cloner tool to reflash the firmware. More information at https://github.com/themactep/thingino-firmware/wiki/Ingenic-USB-Cloner&lt;br /&gt;
&lt;br /&gt;
In short, the cloner tool can re-flash the flash memory without needing the device to boot into an operating system. This works only if you put the T31 SoC into USB-boot mode (think of it like DFU mode for IOS) which happens if the flash memory isn&#039;t bootable. The simplest way to achive this is to open the camera and to short out the flash memory chip on power up to render it unreadable by the SoC.&lt;br /&gt;
&lt;br /&gt;
Steps to use cloner:&lt;br /&gt;
&lt;br /&gt;
# Download cloner for Windows at: https://github.com/gtxaspec/ingenic-cloner-profiles/releases/download/legacy/cloner-2.5.43-windows_thingino.7z&lt;br /&gt;
# Install the cloner app and the USB driver.&lt;br /&gt;
# Open the camera and locate the flash chip. This is typically an 8 pin chip labelled something like 25Q64 or 25Q128. Read the thingino-firmware wiki linked above for pictures. You need to short out pins 5+6 (pins on opposite corner of where there is an embossed dot on the chip)&lt;br /&gt;
# Plug the camera to your PC while shorting the pins and then release the short.&lt;br /&gt;
# If this is working, you should see a new device in device manager called &#039;usb cloner device&#039; [[File:Ingenic usb cloner device.png|alt=Ingenic usb cloner device|center|thumb|227x227px|Ingenic usb cloner device]]&lt;br /&gt;
# Run the cloner utility. &lt;br /&gt;
# Click &#039;Config&#039;. Set platform to T, t31x. Select Board: t31x_sfc_nor_writer_full.cfg, &lt;br /&gt;
# Click the Policy tab, select full_image, SFC_NOR at offset 0x0 with the full thingino firmware file as the attribute. Save this policy as something new&lt;br /&gt;
# Click Start. The cloner program is now listening for a new cloner device to become available.&lt;br /&gt;
# Repeat step #3-4 or trigger the device to reappear (such as if you attach it to a VM and then detach it)&lt;br /&gt;
# The cloner program should automatically erase and reprogram the camera once it becomes available. It will reboot the camera automatically.&lt;br /&gt;
&lt;br /&gt;
=== Wyze cam v3 ===&lt;br /&gt;
Installation on the Wyze Cam v3 is simple: You simply need to copy a few files to your SD card and then reboot the camera. You don&#039;t even need to touch it if you already have wz_mini_hacks installed as you just need to copy the files via SSH and reboot.&lt;br /&gt;
&lt;br /&gt;
# Download the latest installer files from https://github.com/wltechblog/thingino-installers/tree/main/wyze-cam-3&lt;br /&gt;
# Extract the files and then upload it to the SD card. if you&#039;re using wz_mini_hacks with SSH enabled, you can just copy it via SSH: {{Highlight&lt;br /&gt;
| code = $ for i in  factory_t31_ZMC6tiIDQN  thingino-wyze_cam3_t31al_gc2053_atbm6031.bin  thingino-wyze_cam3_t31x_gc2053_atbm6031.bin  thingino-wyze_cam3_t31x_gc2053_rtl8189ftv.bin ; do cat &amp;quot;$i&amp;quot;  {{!}} ssh root@wyze-camera &amp;quot;cat - &amp;gt; /media/mmc/$i&amp;quot; ; done&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Reboot the camera and wait 2-3 minutes.&lt;br /&gt;
# You should see a wireless access point show up once this is done named &amp;quot;THINGINO-XXXX&amp;quot;. Connect to it and then go to http://172.16.0.1/ to complete the setup. After you enter your password/wifi settings, you will see the MAC address of the device.&lt;br /&gt;
&lt;br /&gt;
== Frigate integration ==&lt;br /&gt;
Getting Thingino and Frigate with nvidia hardware acceleration seems to be a little tempermental when configured with a specific combination of configuration settings that leads to the detection and recording processes to eventually misbehave and crash.  When this misbehavior happens, ffmpeg uses 100% GPU and rapidly eats up as much system memory as it can get before OOM kills it. While this happens, the preview shows corrupt frames like this:&lt;br /&gt;
[[File:Frigate misbehaving.jpg|alt=Frigate + nvidia acceleration + Thingino misbehaving which causes frames to scramble until ffmpeg crashes.|none|thumb|Frigate + nvidia acceleration + Thingino misbehaving which causes frames to scramble until ffmpeg crashes.]]&lt;br /&gt;
A few things that I had to do to make it work:&lt;br /&gt;
&lt;br /&gt;
# If you use the sub stream, you shouldn&#039;t use hwaccel_args : preset-nvidia or else ffmpeg goes off the rails and then crashes&lt;br /&gt;
# If you want to use hwaccel_args: preset-nvidia, then don&#039;t use the sub stream as an input. You can still use it as a live stream.&lt;br /&gt;
&lt;br /&gt;
This seems to work for me:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = garage:&lt;br /&gt;
    ffmpeg:&lt;br /&gt;
      hwaccel_args: preset-nvidia&lt;br /&gt;
      output_args:&lt;br /&gt;
        record: preset-record-generic-audio-aac&lt;br /&gt;
      inputs:&lt;br /&gt;
        - path: rtsp://127.0.0.1:8554/wyze-garage?timeout=30&lt;br /&gt;
          input_args: preset-rtsp-restream&lt;br /&gt;
          roles:&lt;br /&gt;
            - record&lt;br /&gt;
            - detect&lt;br /&gt;
            - audio&lt;br /&gt;
    live:&lt;br /&gt;
      stream_name: wyze-garage_sub&lt;br /&gt;
go2rtc:&lt;br /&gt;
  streams:&lt;br /&gt;
    wyze-garage:&lt;br /&gt;
       - rtsp://thingino:thingino@ip:554/ch0#timeout=30&lt;br /&gt;
    wyze-garage_sub:&lt;br /&gt;
       - rtsp://thingino:thingino@ip:554/ch1#timeout=30&lt;br /&gt;
&lt;br /&gt;
  webrtc:&lt;br /&gt;
    candidates:&lt;br /&gt;
      - 10.1.2.30:8555&lt;br /&gt;
      - stun:8555&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Support ASIX AX88179 ==&lt;br /&gt;
I bought a cheap USB 3.0 gigabit adapter from Amazon &amp;quot;TP-Link USB to Ethernet Adapter (UE306)&amp;quot; for CAD$14. This comes with a AX88179 chip which isn&#039;t supported by Thingino out of the box. In order to make this work, you will need to rebuild the firmware and enable this feature.&lt;br /&gt;
&lt;br /&gt;
The Thingino build is quite straight forward. Follow the instructions outlined at https://github.com/themactep/thingino-firmware/wiki/Building-from-sources.&lt;br /&gt;
&lt;br /&gt;
In summary, the steps are:&lt;br /&gt;
&lt;br /&gt;
# On an Ubuntu based system, install the dependencies: &amp;lt;code&amp;gt;apt install bison cmake flex gawk libncurses u-boot-tools git bash dialog&amp;lt;/code&amp;gt;. Ensure that you have at least 7 GB of storage available to store the repo and the output directory. The build script puts everything into &amp;lt;code&amp;gt;$HOME/output&amp;lt;/code&amp;gt;, so if you don&#039;t want that (e.g. if your home directory is on a slow NFS server), symlink it somewhere before you start.&lt;br /&gt;
# Clone the git repo: &amp;lt;code&amp;gt;git clone --depth=1 --recurse-submodules --shallow-submodules &amp;lt;nowiki&amp;gt;https://github.com/themactep/thingino-firmware&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
# Go into the firmware and then run the &amp;lt;code&amp;gt;user-menu.sh&amp;lt;/code&amp;gt; script. &lt;br /&gt;
# Next, enable the ASIX AX88179 module. &lt;br /&gt;
## Select &amp;quot;&amp;lt;code&amp;gt;Main Menu&amp;lt;/code&amp;gt;&amp;quot;, &lt;br /&gt;
## On the &#039;&amp;lt;code&amp;gt;Thingino Buildroot&amp;lt;/code&amp;gt;&#039; menu, select &#039;&amp;lt;code&amp;gt;menuconfig&amp;lt;/code&amp;gt;&#039;, then &#039;&amp;lt;code&amp;gt;menuconfig&amp;lt;/code&amp;gt;&#039;, then select your device (&amp;lt;code&amp;gt;wyze_cam3_t31x_gc2053_rtl8189ftv&amp;lt;/code&amp;gt;), &lt;br /&gt;
## On the &#039;&amp;lt;code&amp;gt;Buildroot xxxxxx Configuration&amp;lt;/code&amp;gt;&#039; menu, you can select settings specific to this build. Selet &#039;&amp;lt;code&amp;gt;External options&amp;lt;/code&amp;gt;&#039;, &amp;lt;code&amp;gt;Thingino Firmware&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;USB Networking options&amp;lt;/code&amp;gt;, select &#039;&amp;lt;code&amp;gt;ASIX AX88179_178A USB&amp;lt;/code&amp;gt;&#039; to enable it. &lt;br /&gt;
## Exit out until you see the main &#039;&amp;lt;code&amp;gt;Thingino Buildroot&amp;lt;/code&amp;gt;&#039; menu again and then select &#039;&amp;lt;code&amp;gt;make fast&amp;lt;/code&amp;gt;&#039; to compile.&lt;br /&gt;
# Your firmware should be placed in your home directory &amp;lt;code&amp;gt;~/output&amp;lt;/code&amp;gt; directory. You can do a OTA update or copy the .bin file and manually flash it through the web interface. If you somehow chose something bad and the device boot-loops... you&#039;ll have to use the cloner program to recover.&lt;br /&gt;
&lt;br /&gt;
If everything works out, you should see &amp;lt;code&amp;gt;eth0&amp;lt;/code&amp;gt; after the device comes online. Enable &amp;lt;code&amp;gt;eth0&amp;lt;/code&amp;gt; in the web interface and then reboot.&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Thingino&amp;diff=7703</id>
		<title>Thingino</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Thingino&amp;diff=7703"/>
		<updated>2025-08-04T05:14:32Z</updated>

		<summary type="html">&lt;p&gt;Leo: /* Support ASIX AX88179 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Thingino is an open-source firmware for Ingenic based IP cameras. This is a good alternative firmware for the Wyze Cam (v2, v3, but not the v4) as it has RTSP support with an up-to-date kernel/OS.&lt;br /&gt;
&lt;br /&gt;
== Installation ==&lt;br /&gt;
Josh at WL Tech Blog has made Thingino installers for all the supported cameras available from his GitHub repo:&lt;br /&gt;
&lt;br /&gt;
* https://github.com/wltechblog/thingino-installers&lt;br /&gt;
&lt;br /&gt;
Simply find the installer for your camera and follow the instructions provided.&lt;br /&gt;
&lt;br /&gt;
=== Installation using Cloner ===&lt;br /&gt;
If you &#039;brick&#039; your T31 based camera through a bad update, you can use the Ingenic USB Cloner tool to reflash the firmware. More information at https://github.com/themactep/thingino-firmware/wiki/Ingenic-USB-Cloner&lt;br /&gt;
&lt;br /&gt;
In short, the cloner tool can re-flash the flash memory without needing the device to boot into an operating system. This works only if you put the T31 SoC into USB-boot mode (think of it like DFU mode for IOS) which happens if the flash memory isn&#039;t bootable. The simplest way to achive this is to open the camera and to short out the flash memory chip on power up to render it unreadable by the SoC.&lt;br /&gt;
&lt;br /&gt;
Steps to use cloner:&lt;br /&gt;
&lt;br /&gt;
# Download cloner for Windows at: https://github.com/gtxaspec/ingenic-cloner-profiles/releases/download/legacy/cloner-2.5.43-windows_thingino.7z&lt;br /&gt;
# Install the cloner app and the USB driver.&lt;br /&gt;
# Open the camera and locate the flash chip. This is typically an 8 pin chip labelled something like 25Q64 or 25Q128. Read the thingino-firmware wiki linked above for pictures. You need to short out pins 5+6 (pins on opposite corner of where there is an embossed dot on the chip)&lt;br /&gt;
# Plug the camera to your PC while shorting the pins and then release the short.&lt;br /&gt;
# If this is working, you should see a new device in device manager called &#039;usb cloner device&#039; [[File:Ingenic usb cloner device.png|alt=Ingenic usb cloner device|center|thumb|227x227px|Ingenic usb cloner device]]&lt;br /&gt;
# Run the cloner utility. &lt;br /&gt;
# Click &#039;Config&#039;. Set platform to T, t31x. Select Board: t31x_sfc_nor_writer_full.cfg, &lt;br /&gt;
# Click the Policy tab, select full_image, SFC_NOR at offset 0x0 with the full thingino firmware file as the attribute. Save this policy as something new&lt;br /&gt;
# Click Start. The cloner program is now listening for a new cloner device to become available.&lt;br /&gt;
# Repeat step #3-4 or trigger the device to reappear (such as if you attach it to a VM and then detach it)&lt;br /&gt;
# The cloner program should automatically erase and reprogram the camera once it becomes available. It will reboot the camera automatically.&lt;br /&gt;
&lt;br /&gt;
=== Wyze cam v3 ===&lt;br /&gt;
Installation on the Wyze Cam v3 is simple: You simply need to copy a few files to your SD card and then reboot the camera. You don&#039;t even need to touch it if you already have wz_mini_hacks installed as you just need to copy the files via SSH and reboot.&lt;br /&gt;
&lt;br /&gt;
# Download the latest installer files from https://github.com/wltechblog/thingino-installers/tree/main/wyze-cam-3&lt;br /&gt;
# Extract the files and then upload it to the SD card. if you&#039;re using wz_mini_hacks with SSH enabled, you can just copy it via SSH: {{Highlight&lt;br /&gt;
| code = $ for i in  factory_t31_ZMC6tiIDQN  thingino-wyze_cam3_t31al_gc2053_atbm6031.bin  thingino-wyze_cam3_t31x_gc2053_atbm6031.bin  thingino-wyze_cam3_t31x_gc2053_rtl8189ftv.bin ; do cat &amp;quot;$i&amp;quot;  {{!}} ssh root@wyze-camera &amp;quot;cat - &amp;gt; /media/mmc/$i&amp;quot; ; done&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Reboot the camera and wait 2-3 minutes.&lt;br /&gt;
# You should see a wireless access point show up once this is done named &amp;quot;THINGINO-XXXX&amp;quot;. Connect to it and then go to http://172.16.0.1/ to complete the setup. After you enter your password/wifi settings, you will see the MAC address of the device.&lt;br /&gt;
&lt;br /&gt;
== Frigate integration ==&lt;br /&gt;
Getting Thingino and Frigate with nvidia hardware acceleration seems to be a little tempermental when configured with a specific combination of configuration settings that leads to the detection and recording processes to eventually misbehave and crash.  When this misbehavior happens, ffmpeg uses 100% GPU and rapidly eats up as much system memory as it can get before OOM kills it. While this happens, the preview shows corrupt frames like this:&lt;br /&gt;
[[File:Frigate misbehaving.jpg|alt=Frigate + nvidia acceleration + Thingino misbehaving which causes frames to scramble until ffmpeg crashes.|none|thumb|Frigate + nvidia acceleration + Thingino misbehaving which causes frames to scramble until ffmpeg crashes.]]&lt;br /&gt;
A few things that I had to do to make it work:&lt;br /&gt;
&lt;br /&gt;
# If you use the sub stream, you shouldn&#039;t use hwaccel_args : preset-nvidia or else ffmpeg goes off the rails and then crashes&lt;br /&gt;
# If you want to use hwaccel_args: preset-nvidia, then don&#039;t use the sub stream as an input. You can still use it as a live stream.&lt;br /&gt;
&lt;br /&gt;
This seems to work for me:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = garage:&lt;br /&gt;
    ffmpeg:&lt;br /&gt;
      hwaccel_args: preset-nvidia&lt;br /&gt;
      output_args:&lt;br /&gt;
        record: preset-record-generic-audio-aac&lt;br /&gt;
      inputs:&lt;br /&gt;
        - path: rtsp://127.0.0.1:8554/wyze-garage?timeout=30&lt;br /&gt;
          input_args: preset-rtsp-restream&lt;br /&gt;
          roles:&lt;br /&gt;
            - record&lt;br /&gt;
            - detect&lt;br /&gt;
            - audio&lt;br /&gt;
    live:&lt;br /&gt;
      stream_name: wyze-garage_sub&lt;br /&gt;
go2rtc:&lt;br /&gt;
  streams:&lt;br /&gt;
    wyze-garage:&lt;br /&gt;
       - rtsp://thingino:thingino@ip:554/ch0#timeout=30&lt;br /&gt;
    wyze-garage_sub:&lt;br /&gt;
       - rtsp://thingino:thingino@ip:554/ch1#timeout=30&lt;br /&gt;
&lt;br /&gt;
  webrtc:&lt;br /&gt;
    candidates:&lt;br /&gt;
      - 10.1.2.30:8555&lt;br /&gt;
      - stun:8555&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Support ASIX AX88179 ==&lt;br /&gt;
I bought a cheap USB 3.0 gigabit adapter from Amazon &amp;quot;TP-Link USB to Ethernet Adapter (UE306)&amp;quot; for CAD$14. This comes with a AX88179 chip which isn&#039;t supported by Thingino out of the box. In order to make this work, you will need to rebuild the firmware and enable this feature.&lt;br /&gt;
&lt;br /&gt;
The Thingino build is quite straight forward. Follow the instructions outlined at https://github.com/themactep/thingino-firmware/wiki/Building-from-sources. The entire build process is mostly automated already.&lt;br /&gt;
&lt;br /&gt;
In summary, the steps are:&lt;br /&gt;
&lt;br /&gt;
# On an Ubuntu based system, install the dependencies: &amp;lt;code&amp;gt;apt install bison cmake flex gawk libncurses u-boot-tools git bash dialog&amp;lt;/code&amp;gt;&lt;br /&gt;
# Clone the git repo: &amp;lt;code&amp;gt;git clone --depth=1 --recurse-submodules --shallow-submodules &amp;lt;nowiki&amp;gt;https://github.com/themactep/thingino-firmware&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
# Go into the firmware and then run the &amp;lt;code&amp;gt;user-menu.sh&amp;lt;/code&amp;gt; script. &lt;br /&gt;
# Next, enable the ASIX AX88179 module. &lt;br /&gt;
## Select &amp;quot;&amp;lt;code&amp;gt;Main Menu&amp;lt;/code&amp;gt;&amp;quot;, &lt;br /&gt;
## On the &#039;&amp;lt;code&amp;gt;Thingino Buildroot&amp;lt;/code&amp;gt;&#039; menu, select &#039;&amp;lt;code&amp;gt;menuconfig&amp;lt;/code&amp;gt;&#039;, then &#039;&amp;lt;code&amp;gt;menuconfig&amp;lt;/code&amp;gt;&#039;, then select your device (&amp;lt;code&amp;gt;wyze_cam3_t31x_gc2053_rtl8189ftv&amp;lt;/code&amp;gt;), &lt;br /&gt;
## On the &#039;&amp;lt;code&amp;gt;Buildroot xxxxxx Configuration&amp;lt;/code&amp;gt;&#039; menu, you can select settings specific to this build. Selet &#039;&amp;lt;code&amp;gt;External options&amp;lt;/code&amp;gt;&#039;, &amp;lt;code&amp;gt;Thingino Firmware&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;USB Networking options&amp;lt;/code&amp;gt;, select &#039;&amp;lt;code&amp;gt;ASIX AX88179_178A USB&amp;lt;/code&amp;gt;&#039; to enable it. &lt;br /&gt;
## Exit out until you see the main &#039;&amp;lt;code&amp;gt;Thingino Buildroot&amp;lt;/code&amp;gt;&#039; menu again and then select &#039;&amp;lt;code&amp;gt;make fast&amp;lt;/code&amp;gt;&#039; to compile.&lt;br /&gt;
# Your firmware should be placed in your home directory &amp;lt;code&amp;gt;~/output&amp;lt;/code&amp;gt; directory. You can do a OTA update or copy the .bin file and manually flash it through the web interface. If you somehow chose something bad and the device boot-loops... you&#039;ll have to use the cloner program to recover.&lt;br /&gt;
&lt;br /&gt;
If everything works out, you should see &amp;lt;code&amp;gt;eth0&amp;lt;/code&amp;gt; after the device comes online. Enable &amp;lt;code&amp;gt;eth0&amp;lt;/code&amp;gt; in the web interface and then reboot.&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Thingino&amp;diff=7702</id>
		<title>Thingino</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Thingino&amp;diff=7702"/>
		<updated>2025-08-04T05:11:21Z</updated>

		<summary type="html">&lt;p&gt;Leo: /* Installation using Cloner */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Thingino is an open-source firmware for Ingenic based IP cameras. This is a good alternative firmware for the Wyze Cam (v2, v3, but not the v4) as it has RTSP support with an up-to-date kernel/OS.&lt;br /&gt;
&lt;br /&gt;
== Installation ==&lt;br /&gt;
Josh at WL Tech Blog has made Thingino installers for all the supported cameras available from his GitHub repo:&lt;br /&gt;
&lt;br /&gt;
* https://github.com/wltechblog/thingino-installers&lt;br /&gt;
&lt;br /&gt;
Simply find the installer for your camera and follow the instructions provided.&lt;br /&gt;
&lt;br /&gt;
=== Installation using Cloner ===&lt;br /&gt;
If you &#039;brick&#039; your T31 based camera through a bad update, you can use the Ingenic USB Cloner tool to reflash the firmware. More information at https://github.com/themactep/thingino-firmware/wiki/Ingenic-USB-Cloner&lt;br /&gt;
&lt;br /&gt;
In short, the cloner tool can re-flash the flash memory without needing the device to boot into an operating system. This works only if you put the T31 SoC into USB-boot mode (think of it like DFU mode for IOS) which happens if the flash memory isn&#039;t bootable. The simplest way to achive this is to open the camera and to short out the flash memory chip on power up to render it unreadable by the SoC.&lt;br /&gt;
&lt;br /&gt;
Steps to use cloner:&lt;br /&gt;
&lt;br /&gt;
# Download cloner for Windows at: https://github.com/gtxaspec/ingenic-cloner-profiles/releases/download/legacy/cloner-2.5.43-windows_thingino.7z&lt;br /&gt;
# Install the cloner app and the USB driver.&lt;br /&gt;
# Open the camera and locate the flash chip. This is typically an 8 pin chip labelled something like 25Q64 or 25Q128. Read the thingino-firmware wiki linked above for pictures. You need to short out pins 5+6 (pins on opposite corner of where there is an embossed dot on the chip)&lt;br /&gt;
# Plug the camera to your PC while shorting the pins and then release the short.&lt;br /&gt;
# If this is working, you should see a new device in device manager called &#039;usb cloner device&#039; [[File:Ingenic usb cloner device.png|alt=Ingenic usb cloner device|center|thumb|227x227px|Ingenic usb cloner device]]&lt;br /&gt;
# Run the cloner utility. &lt;br /&gt;
# Click &#039;Config&#039;. Set platform to T, t31x. Select Board: t31x_sfc_nor_writer_full.cfg, &lt;br /&gt;
# Click the Policy tab, select full_image, SFC_NOR at offset 0x0 with the full thingino firmware file as the attribute. Save this policy as something new&lt;br /&gt;
# Click Start. The cloner program is now listening for a new cloner device to become available.&lt;br /&gt;
# Repeat step #3-4 or trigger the device to reappear (such as if you attach it to a VM and then detach it)&lt;br /&gt;
# The cloner program should automatically erase and reprogram the camera once it becomes available. It will reboot the camera automatically.&lt;br /&gt;
&lt;br /&gt;
=== Wyze cam v3 ===&lt;br /&gt;
Installation on the Wyze Cam v3 is simple: You simply need to copy a few files to your SD card and then reboot the camera. You don&#039;t even need to touch it if you already have wz_mini_hacks installed as you just need to copy the files via SSH and reboot.&lt;br /&gt;
&lt;br /&gt;
# Download the latest installer files from https://github.com/wltechblog/thingino-installers/tree/main/wyze-cam-3&lt;br /&gt;
# Extract the files and then upload it to the SD card. if you&#039;re using wz_mini_hacks with SSH enabled, you can just copy it via SSH: {{Highlight&lt;br /&gt;
| code = $ for i in  factory_t31_ZMC6tiIDQN  thingino-wyze_cam3_t31al_gc2053_atbm6031.bin  thingino-wyze_cam3_t31x_gc2053_atbm6031.bin  thingino-wyze_cam3_t31x_gc2053_rtl8189ftv.bin ; do cat &amp;quot;$i&amp;quot;  {{!}} ssh root@wyze-camera &amp;quot;cat - &amp;gt; /media/mmc/$i&amp;quot; ; done&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Reboot the camera and wait 2-3 minutes.&lt;br /&gt;
# You should see a wireless access point show up once this is done named &amp;quot;THINGINO-XXXX&amp;quot;. Connect to it and then go to http://172.16.0.1/ to complete the setup. After you enter your password/wifi settings, you will see the MAC address of the device.&lt;br /&gt;
&lt;br /&gt;
== Frigate integration ==&lt;br /&gt;
Getting Thingino and Frigate with nvidia hardware acceleration seems to be a little tempermental when configured with a specific combination of configuration settings that leads to the detection and recording processes to eventually misbehave and crash.  When this misbehavior happens, ffmpeg uses 100% GPU and rapidly eats up as much system memory as it can get before OOM kills it. While this happens, the preview shows corrupt frames like this:&lt;br /&gt;
[[File:Frigate misbehaving.jpg|alt=Frigate + nvidia acceleration + Thingino misbehaving which causes frames to scramble until ffmpeg crashes.|none|thumb|Frigate + nvidia acceleration + Thingino misbehaving which causes frames to scramble until ffmpeg crashes.]]&lt;br /&gt;
A few things that I had to do to make it work:&lt;br /&gt;
&lt;br /&gt;
# If you use the sub stream, you shouldn&#039;t use hwaccel_args : preset-nvidia or else ffmpeg goes off the rails and then crashes&lt;br /&gt;
# If you want to use hwaccel_args: preset-nvidia, then don&#039;t use the sub stream as an input. You can still use it as a live stream.&lt;br /&gt;
&lt;br /&gt;
This seems to work for me:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = garage:&lt;br /&gt;
    ffmpeg:&lt;br /&gt;
      hwaccel_args: preset-nvidia&lt;br /&gt;
      output_args:&lt;br /&gt;
        record: preset-record-generic-audio-aac&lt;br /&gt;
      inputs:&lt;br /&gt;
        - path: rtsp://127.0.0.1:8554/wyze-garage?timeout=30&lt;br /&gt;
          input_args: preset-rtsp-restream&lt;br /&gt;
          roles:&lt;br /&gt;
            - record&lt;br /&gt;
            - detect&lt;br /&gt;
            - audio&lt;br /&gt;
    live:&lt;br /&gt;
      stream_name: wyze-garage_sub&lt;br /&gt;
go2rtc:&lt;br /&gt;
  streams:&lt;br /&gt;
    wyze-garage:&lt;br /&gt;
       - rtsp://thingino:thingino@ip:554/ch0#timeout=30&lt;br /&gt;
    wyze-garage_sub:&lt;br /&gt;
       - rtsp://thingino:thingino@ip:554/ch1#timeout=30&lt;br /&gt;
&lt;br /&gt;
  webrtc:&lt;br /&gt;
    candidates:&lt;br /&gt;
      - 10.1.2.30:8555&lt;br /&gt;
      - stun:8555&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Support ASIX AX88179 ==&lt;br /&gt;
I bought a cheap USB 3.0 gigabit adapter from Amazon &amp;quot;TP-Link USB to Ethernet Adapter (UE306)&amp;quot;. This comes with a AX88179 chip which isn&#039;t supported by Thingino out of the box. In order to make this work, you will need to rebuild the firmware and enable this feature.&lt;br /&gt;
&lt;br /&gt;
The Thingino build is quite straight forward. Follow the instructions outlined at https://github.com/themactep/thingino-firmware/wiki/Building-from-sources. In summary, the steps are:&lt;br /&gt;
&lt;br /&gt;
# On an Ubuntu based system, install the dependencies: apt install bison cmake flex gawk libncurses u-boot-tools git bash dialog&lt;br /&gt;
# Clone the git repo: git clone --depth=1 --recurse-submodules --shallow-submodules &amp;lt;nowiki&amp;gt;https://github.com/themactep/thingino-firmware&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
# Go into the firmware and then run the user-menu.sh script. &lt;br /&gt;
# Next, enable the ASIX AX88179 module. &lt;br /&gt;
## Select &amp;quot;Main Menu&amp;quot;, &lt;br /&gt;
## On the &#039;Thingino Buildroot&#039; menu, select &#039;menuconfig&#039;, then &#039;menuconfig&#039;, then select your device (wyze_cam3_t31x_gc2053_rtl8189ftv), &lt;br /&gt;
## On the &#039;Buildroot xxxxxx Configuration&#039; menu, you can select settings specific to this build. Selet &#039;External options&#039;, Thingino Firmware, USB Networking options, select &#039;ASIX AX88179_178A USB&#039; to enable it. &lt;br /&gt;
## Exit out until you see the main &#039;Thingino Buildroot&#039; menu again and then select &#039;make fast&#039; to compile.&lt;br /&gt;
# Your firmware should be placed in your home directory ~/output directory. You can do a OTA update or copy the .bin file and manually flash it through the web interface. If you somehow chose something bad and the device boot-loops... you&#039;ll have to use the cloner program to recover.&lt;br /&gt;
&lt;br /&gt;
If everything works out, you should see eth0 after the device comes online. Enable eth0 in the web interface and then reboot.&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=File:Ingenic_usb_cloner_device.png&amp;diff=7701</id>
		<title>File:Ingenic usb cloner device.png</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=File:Ingenic_usb_cloner_device.png&amp;diff=7701"/>
		<updated>2025-08-04T02:53:39Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Ingenic usb cloner device&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Thingino&amp;diff=7700</id>
		<title>Thingino</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Thingino&amp;diff=7700"/>
		<updated>2025-08-03T05:13:34Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Thingino is an open-source firmware for Ingenic based IP cameras. This is a good alternative firmware for the Wyze Cam (v2, v3, but not the v4) as it has RTSP support with an up-to-date kernel/OS.&lt;br /&gt;
&lt;br /&gt;
== Installation ==&lt;br /&gt;
Josh at WL Tech Blog has made Thingino installers for all the supported cameras available from his GitHub repo:&lt;br /&gt;
&lt;br /&gt;
* https://github.com/wltechblog/thingino-installers&lt;br /&gt;
&lt;br /&gt;
Simply find the installer for your camera and follow the instructions provided.&lt;br /&gt;
&lt;br /&gt;
=== Installation using Cloner ===&lt;br /&gt;
If you &#039;brick&#039; your T31 based camera through a bad update, you can use the Ingenic USB Cloner tool to reflash the firmware. More information at https://github.com/themactep/thingino-firmware/wiki/Ingenic-USB-Cloner&lt;br /&gt;
&lt;br /&gt;
In short, the cloner tool can re-flash the flash memory without needing the device to boot into an operating system. This works only if you put the T31 SoC into USB-boot mode (think of it like DFU mode for IOS) which happens if the flash memory isn&#039;t bootable. The simplest way to achive this is to open the camera and to short out the flash memory chip on power up to render it unreadable by the SoC.&lt;br /&gt;
&lt;br /&gt;
Steps to use cloner:&lt;br /&gt;
&lt;br /&gt;
# Download cloner for Windows at: https://github.com/gtxaspec/ingenic-cloner-profiles/releases/download/legacy/cloner-2.5.43-windows_thingino.7z&lt;br /&gt;
# Open the camera and locate the flash chip. This is typically an 8 pin chip labelled something like 25Q64 or 25Q128. Read the thingino-firmware wiki linked above for pictures. You need to short out pins 5+6 (pins on opposite corner of where there is an embossed dot on the chip)&lt;br /&gt;
# Plug the camera to your PC while shorting the pins and then release the short.&lt;br /&gt;
# If this is working, you should see a new device in device manager&lt;br /&gt;
# Run the cloner utility&lt;br /&gt;
# To flash a new firmware, select t31x_sfc_nor_writer_full.cfg, under Policy, select full_image, SFC_NOR at offset 0x0 with the full thingino firmware file as the attribute.&lt;br /&gt;
&lt;br /&gt;
=== Wyze cam v3 ===&lt;br /&gt;
Installation on the Wyze Cam v3 is simple: You simply need to copy a few files to your SD card and then reboot the camera. You don&#039;t even need to touch it if you already have wz_mini_hacks installed as you just need to copy the files via SSH and reboot.&lt;br /&gt;
&lt;br /&gt;
# Download the latest installer files from https://github.com/wltechblog/thingino-installers/tree/main/wyze-cam-3&lt;br /&gt;
# Extract the files and then upload it to the SD card. if you&#039;re using wz_mini_hacks with SSH enabled, you can just copy it via SSH: {{Highlight&lt;br /&gt;
| code = $ for i in  factory_t31_ZMC6tiIDQN  thingino-wyze_cam3_t31al_gc2053_atbm6031.bin  thingino-wyze_cam3_t31x_gc2053_atbm6031.bin  thingino-wyze_cam3_t31x_gc2053_rtl8189ftv.bin ; do cat &amp;quot;$i&amp;quot;  {{!}} ssh root@wyze-camera &amp;quot;cat - &amp;gt; /media/mmc/$i&amp;quot; ; done&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Reboot the camera and wait 2-3 minutes.&lt;br /&gt;
# You should see a wireless access point show up once this is done named &amp;quot;THINGINO-XXXX&amp;quot;. Connect to it and then go to http://172.16.0.1/ to complete the setup. After you enter your password/wifi settings, you will see the MAC address of the device.&lt;br /&gt;
&lt;br /&gt;
== Frigate integration ==&lt;br /&gt;
Getting Thingino and Frigate with nvidia hardware acceleration seems to be a little tempermental when configured with a specific combination of configuration settings that leads to the detection and recording processes to eventually misbehave and crash.  When this misbehavior happens, ffmpeg uses 100% GPU and rapidly eats up as much system memory as it can get before OOM kills it. While this happens, the preview shows corrupt frames like this:&lt;br /&gt;
[[File:Frigate misbehaving.jpg|alt=Frigate + nvidia acceleration + Thingino misbehaving which causes frames to scramble until ffmpeg crashes.|none|thumb|Frigate + nvidia acceleration + Thingino misbehaving which causes frames to scramble until ffmpeg crashes.]]&lt;br /&gt;
A few things that I had to do to make it work:&lt;br /&gt;
&lt;br /&gt;
# If you use the sub stream, you shouldn&#039;t use hwaccel_args : preset-nvidia or else ffmpeg goes off the rails and then crashes&lt;br /&gt;
# If you want to use hwaccel_args: preset-nvidia, then don&#039;t use the sub stream as an input. You can still use it as a live stream.&lt;br /&gt;
&lt;br /&gt;
This seems to work for me:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = garage:&lt;br /&gt;
    ffmpeg:&lt;br /&gt;
      hwaccel_args: preset-nvidia&lt;br /&gt;
      output_args:&lt;br /&gt;
        record: preset-record-generic-audio-aac&lt;br /&gt;
      inputs:&lt;br /&gt;
        - path: rtsp://127.0.0.1:8554/wyze-garage?timeout=30&lt;br /&gt;
          input_args: preset-rtsp-restream&lt;br /&gt;
          roles:&lt;br /&gt;
            - record&lt;br /&gt;
            - detect&lt;br /&gt;
            - audio&lt;br /&gt;
    live:&lt;br /&gt;
      stream_name: wyze-garage_sub&lt;br /&gt;
go2rtc:&lt;br /&gt;
  streams:&lt;br /&gt;
    wyze-garage:&lt;br /&gt;
       - rtsp://thingino:thingino@ip:554/ch0#timeout=30&lt;br /&gt;
    wyze-garage_sub:&lt;br /&gt;
       - rtsp://thingino:thingino@ip:554/ch1#timeout=30&lt;br /&gt;
&lt;br /&gt;
  webrtc:&lt;br /&gt;
    candidates:&lt;br /&gt;
      - 10.1.2.30:8555&lt;br /&gt;
      - stun:8555&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Proxmox&amp;diff=7699</id>
		<title>Proxmox</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Proxmox&amp;diff=7699"/>
		<updated>2025-08-03T04:27:00Z</updated>

		<summary type="html">&lt;p&gt;Leo: /* Troubleshooting */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Proxmox Virtual Environment (VE) is an open source virtualization management software that allows deployment and management of KVM-based virtual machines and LXC containers. The platform itself run on a Debian based distribution and supports filesystems such as ZFS and Ceph. &lt;br /&gt;
&lt;br /&gt;
In terms of functionality, Proxmox VE is similar to VMware ESXi with some overlap with vSphere. You can have a cluster of Proxmox servers controlled and managed through a singular web console, command line tools, or via the provided REST API.&lt;br /&gt;
&lt;br /&gt;
==Installation==&lt;br /&gt;
Installation is as simple as installing any other Linux distribution. Download the Proxmox iso image from https://www.proxmox.com/en/downloads. After installation, you should be able to access the web console via HTTPS on port 8006.&lt;br /&gt;
&lt;br /&gt;
==Tasks and How-Tos==&lt;br /&gt;
&lt;br /&gt;
===Remove the subscription nag===&lt;br /&gt;
If your Proxmox server has no active subscription, you will be nagged every time you log in to the web interface. This can be disabled by running the following as root in the server&#039;s console:&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # sed -Ezi.bak &amp;quot;s/(Ext.Msg.show\(\{\s+title: gettext\(&#039;No valid sub)/void\(\{ \/\/\1/g&amp;quot; /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
You will have to do this after updating Proxmox again. &lt;br /&gt;
&lt;br /&gt;
There is also an ansible role which does this at: https://github.com/FuzzyMistborn/ansible-role-proxmox-nag-removal/blob/master/tasks/remove-nag.yml&lt;br /&gt;
&lt;br /&gt;
===Make a VM or LXC start automatically at boot===&lt;br /&gt;
You can enable automatic startup and the startup order for VMs and LXCs under the &#039;Options&#039; panel. [[File:Proxmox start VM at boot.png|alt=Proxmox start VM or container at boot|none|thumb|Proxmox start VM or container at boot]]&lt;br /&gt;
&lt;br /&gt;
===Adding additional LXC templates===&lt;br /&gt;
Proxmox officially supports about a dozen different Linux distributions and provides up to date LXC templates through their repositories. These LXC templates can be downloaded using the &amp;lt;code&amp;gt;pveam&amp;lt;/code&amp;gt; tool or through the Proxmox web interface.&lt;br /&gt;
&lt;br /&gt;
====Adding templates using the web interface====&lt;br /&gt;
[[File:Proxmox CT Templates.png|alt=Download additional Proxmox CT Templates via the web interface|thumb|Download additional Proxmox CT Templates via the web interface]]&lt;br /&gt;
Go to your local storage and click on CT Templates. Click on the Templates button to see available templates.&lt;br /&gt;
&lt;br /&gt;
====Adding templates using the pveam utility====&lt;br /&gt;
The Proxmox VE Appliance Manager (&amp;lt;code&amp;gt;pvadm&amp;lt;/code&amp;gt;) tool is available to manage container templates from Proxmox&#039;s repository. More information at https://pve.proxmox.com/pve-docs/pveam.1.html.&lt;br /&gt;
&lt;br /&gt;
Run &amp;lt;code&amp;gt;pveam available&amp;lt;/code&amp;gt; to list all available templates, then run &amp;lt;code&amp;gt;pveadm download $storage $template&amp;lt;/code&amp;gt; to download a template to a storage pool. For example:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = root@server:~# pveam available&lt;br /&gt;
mail            proxmox-mailgateway-6.4-standard_6.4-1_amd64.tar.gz&lt;br /&gt;
mail            proxmox-mailgateway-7.0-standard_7.0-1_amd64.tar.gz&lt;br /&gt;
system          almalinux-8-default_20210928_amd64.tar.xz&lt;br /&gt;
system          alpine-3.12-default_20200823_amd64.tar.xz&lt;br /&gt;
system          alpine-3.13-default_20210419_amd64.tar.xz&lt;br /&gt;
system          alpine-3.14-default_20210623_amd64.tar.xz&lt;br /&gt;
system          alpine-3.15-default_20211202_amd64.tar.xz&lt;br /&gt;
system          archlinux-base_20210420-1_amd64.tar.gz&lt;br /&gt;
&lt;br /&gt;
root@server:~# pveam download local fedora-35-default_20211111_amd64.tar.xz&lt;br /&gt;
downloading http://download.proxmox.com/images/system/fedora-35-default_20211111_amd64.tar.xz to /var/lib/vz/template/cache/fedora-35-default_20211111_amd64.tar.xz&lt;br /&gt;
--2021-12-29 15:17:28--  http://download.proxmox.com/images/system/fedora-35-default_20211111_amd64.tar.xz&lt;br /&gt;
Resolving download.proxmox.com (download.proxmox.com)... 144.217.225.162, 2607:5300:203:7dc2::162&lt;br /&gt;
Connecting to download.proxmox.com (download.proxmox.com){{!}}144.217.225.162{{!}}:80... connected.&lt;br /&gt;
HTTP request sent, awaiting response... 200 OK&lt;br /&gt;
Length: 89702020 (86M) [application/octet-stream]&lt;br /&gt;
Saving to: &#039;/var/lib/vz/template/cache/fedora-35-default_20211111_amd64.tar.xz.tmp.1053836&#039;&lt;br /&gt;
     0K ........ ........ ........ ........ 37% 2.59M 21s&lt;br /&gt;
 32768K ........ ........ ........ ........ 74% 13.6M 5s&lt;br /&gt;
 65536K ........ ........ .....            100% 20.5M=16s&lt;br /&gt;
2021-12-29 15:17:44 (5.42 MB/s) - &#039;/var/lib/vz/template/cache/fedora-35-default_20211111_amd64.tar.xz.tmp.1053836&#039; saved [89702020/89702020]&lt;br /&gt;
calculating checksum...OK, checksum verified&lt;br /&gt;
download of &#039;http://download.proxmox.com/images/system/fedora-35-default_20211111_amd64.tar.xz&#039; to &#039;/var/lib/vz/template/cache/fedora-35-default_20211111_amd64.tar.xz&#039; finished&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
===Using Terraform with Proxmox===&lt;br /&gt;
You can use Terraform to manage VMs on Proxmox. More information on how to set this up on the [[Terraform]] page. Setup is relatively straight forward and the Terraform plugin for Proxmox is feature rich.&lt;br /&gt;
&lt;br /&gt;
On a related note, you can also use Packer to build VM images. The Packer-Proxmox plugin supports keyboard autotyping which makes automatically building VMs all the more simple.&lt;br /&gt;
&lt;br /&gt;
=== Install SSL certificates ===&lt;br /&gt;
Out of the box, Proxmox will generate a self signed SSL certificate for its management interfaces. This is sufficient from a security standpoint but will result in a SSL certificate warning every time you connect. You may wish to install a signed certificate from a trusted CA to correct this issue.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Note:&#039;&#039;&#039; Changing the SSL certificates prevented my SPICE clients from connecting. I had to revert back to the self generated certificates for them to work again. If you are using SPICE for remote desktkop, you may need to do further testing before rolling it out into production.&lt;br /&gt;
&lt;br /&gt;
To install a new set of SSL certificates, log in to Proxmox via SSH and copy your SSL private key and certificates to &amp;lt;code&amp;gt;/etc/pve/nodes/$node/pve-ssl.{pem,key}&amp;lt;/code&amp;gt;. Replace &#039;$node&#039; with the hostname of your Proxmox machine:&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;pve-ssl.pem&amp;lt;/code&amp;gt; file should contain your full certificate chain. This can be generated by concatenating your primary certificate, followed by any intermediate certificates.&lt;br /&gt;
* The &amp;lt;code&amp;gt;pve-ssl.key&amp;lt;/code&amp;gt; file should contain your private key without a password.&lt;br /&gt;
&lt;br /&gt;
Restart the pveproxy service to apply. If you have any problems, check the service&#039;s status with &amp;lt;code&amp;gt;systemctl status pveproxy&amp;lt;/code&amp;gt;.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # cp fullchain.pem /etc/pve/nodes/&amp;lt;node&amp;gt;/pve-ssl.pem&lt;br /&gt;
# cp private-key.pem /etc/pve/nodes/&amp;lt;node&amp;gt;/pve-ssl.key&lt;br /&gt;
# systemctl restart pveproxy&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
=== Reduce ZFS ARC memory requirement ===&lt;br /&gt;
By default, ZFS&#039;s ARC uses 50% of the host&#039;s memory. If you are running VMs using a ZFS storage pool that exceeds 50% of your system&#039;s available memory, you will need to tweak the amount of memory assigned to ARC to prevent out of memory issues.&lt;br /&gt;
&lt;br /&gt;
The minimum memory assigned to ARC as per Proxmox&#039;s documentation is 2GB + 1GB per TB of storage.&lt;br /&gt;
&lt;br /&gt;
To change the ARC max size to 3 GB: &amp;lt;code&amp;gt;echo &amp;quot;$[3 * 1024*1024*1024]&amp;quot; &amp;gt; /sys/module/zfs/parameters/zfs_arc_max&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To make this permanent, edit &amp;lt;code&amp;gt;/etc/modprobe.d/zfs.conf&amp;lt;/code&amp;gt; and add &amp;lt;code&amp;gt;options zfs zfs_arc_max=3221225472&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Enable QEMU VNC server on a guest VM ===&lt;br /&gt;
There are a few solutions when you want to remote desktop into a VM such as running a RDP/VNC server within the VM or leverage the SPICE display/connection option built into Proxmox. These have their own advantages and disadvantages.&lt;br /&gt;
&lt;br /&gt;
I have recently started using the built-in VNC server that can be enabled on the KVM/QEMU config. The benefit with this approach is that you can control the VM during the boot process as you&#039;re seeing and controlling the VM as you would in the Proxmox web interface.&lt;br /&gt;
&lt;br /&gt;
To enable VNC on a particular VM:&lt;br /&gt;
&lt;br /&gt;
# Edit &amp;lt;code&amp;gt;/etc/pve/local/qemu-server/&amp;lt;vmid&amp;gt;.conf&amp;lt;/code&amp;gt; and the following &amp;lt;code&amp;gt;args&amp;lt;/code&amp;gt; line to enable the VNC server on port 5901 (The port here is the VNC display port and you typically just add 5900 to it. Therefore, port 1 is really TCP port 5901). You may change this port number if you have other VNC servers running. &amp;lt;code&amp;gt;args: -vnc 0.0.0.0:1,password=on&amp;lt;/code&amp;gt;&lt;br /&gt;
# In the same .conf file, add the following line to set the VNC password when the VM starts up. &amp;lt;code&amp;gt;hookscript: local:snippets/set_vnc_password.sh&amp;lt;/code&amp;gt;&lt;br /&gt;
# Create the snippet by creating a file at &amp;lt;code&amp;gt;/var/lib/vz/snippets/set_vnc_password.sh&amp;lt;/code&amp;gt;. Put the following in the script. Note the VNC password is set in plain text here.{{Highlight&lt;br /&gt;
| code = #!/bin/bash&lt;br /&gt;
&lt;br /&gt;
if [[ &amp;quot;$2&amp;quot; != &amp;quot;post-start&amp;quot; ]] ; then&lt;br /&gt;
        echo &amp;quot;Not at post-start stage. Skipping&amp;quot;&lt;br /&gt;
        exit 0&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
VM_ID=$1&lt;br /&gt;
VNC_PASSWORD=myvncpassword&lt;br /&gt;
&lt;br /&gt;
expect -c &amp;quot;&lt;br /&gt;
set timeout 5&lt;br /&gt;
spawn qm monitor $VM_ID&lt;br /&gt;
expect \&amp;quot;qm&amp;gt;\&amp;quot;&lt;br /&gt;
send \&amp;quot;set_password vnc $VNC_PASSWORD -d vnc2\r\&amp;quot;&lt;br /&gt;
expect \&amp;quot;qm&amp;gt;\&amp;quot;&lt;br /&gt;
send \&amp;quot;exit\r\&amp;quot;&lt;br /&gt;
exit&lt;br /&gt;
&amp;quot;&lt;br /&gt;
&lt;br /&gt;
echo&lt;br /&gt;
echo &amp;quot;VNC password set.&amp;quot;&lt;br /&gt;
| lang = bash&lt;br /&gt;
}}&lt;br /&gt;
# Test this by booting the VM. You should be able to connect to the Proxmox server on port 5901 using VNC and authenticate using the password set in the script.&lt;br /&gt;
&lt;br /&gt;
==== VNC server listening on a local socket ====&lt;br /&gt;
Alternatively, you may have the VNC server listen on a local socket. To do so, adjust the args with the following:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ## Listen on a socket (worked for me)&lt;br /&gt;
args: -object secret,id=vncpass,data=password -vnc unix:/var/run/qemu-server/$vmid-secondary.vnc,password-secret=vncpass&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
You may need to use the same snippet script above to set the password since libvirt/Proxmox may set a random passphrase on the VNC server.&lt;br /&gt;
&lt;br /&gt;
With the VNC server running and listening on a socket, you may expose the socket as a TCP port using something like &amp;lt;code&amp;gt;socat&amp;lt;/code&amp;gt;:{{Highlight&lt;br /&gt;
| code = # socat tcp4-listen:5915,fork,reuseaddr unix-connect:/var/run/qemu-server/115-secondary.vnc&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Attach a disk from another VM ===&lt;br /&gt;
To attach a disk to another VM, you must first detach the disk from the source VM. To do so, navigate to the VM&#039;s hardware panel, select the disk to detach, then click on &#039;Disk Actions&#039; and then &#039;Detach&#039;. Then, go to &#039;Disk Action&#039; -&amp;gt; &#039;Reassign Disk&#039; and select the destination VM.&lt;br /&gt;
&lt;br /&gt;
==== The manual process ====&lt;br /&gt;
&#039;&#039;&#039;Note:&#039;&#039;&#039; Not recommended now. This was done before the reassign disk was a feature. These steps are here for legacy reasons.&lt;br /&gt;
&lt;br /&gt;
If you are using ZFS for the VM disks, you can attach a disk from the source VM to another target VM by renaming the disk&#039;s VM ID to the target VM. You can run &amp;lt;code&amp;gt;zfs list&amp;lt;/code&amp;gt; on the Proxmox server to see what disks are available. Once the disk is renamed, the disk can be made visible as an unused disk by running &amp;lt;code&amp;gt;qm rescan&amp;lt;/code&amp;gt; to rescan and update the VM configs. From the Proxmox console, you should then be able to attach the unused disk under the hardware page.&lt;br /&gt;
&lt;br /&gt;
For example, to attach a VM disk which was originally connected to VM 101 to VM 102, I had to rename the disk &amp;lt;code&amp;gt;vm-101-disk-0&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;vm-102-disk-1&amp;lt;/code&amp;gt; by running: &amp;lt;code&amp;gt;zfs rename data/vm-101-disk-0 data/vm-102-disk-1&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Renumber VM IDs ===&lt;br /&gt;
Use the following script to renumber a VM&#039;s ID. This script supports VMs that are stored on both LVM and ZFS. &#039;&#039;&#039;Power off the VM before proceeding&#039;&#039;&#039;. Proxmox should be able to see the VM being renamed automatically.&lt;br /&gt;
&lt;br /&gt;
Be aware that renumbering a VM may cause Cloud-Init to re-run again as it may think it&#039;s on a new instance. This could have unintended side effects such as having your system&#039;s SSH host keys wiped and accounts&#039; passwords reset.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = #!/bin/bash&lt;br /&gt;
Old=&amp;quot;$1&amp;quot;&lt;br /&gt;
New=&amp;quot;$2&amp;quot;&lt;br /&gt;
&lt;br /&gt;
usage(){&lt;br /&gt;
        echo &amp;quot;Usage: $0 old-vmid new-vmid&amp;quot;&lt;br /&gt;
        exit 2&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
if [ -z &amp;quot;$Old&amp;quot; ] {{!}}{{!}} ! expr &amp;quot;$Old&amp;quot; : &#039;^[0-9]\+$&#039; &amp;gt;/dev/null ; then&lt;br /&gt;
        echo &amp;quot;Error: Invalid old-vimd&amp;quot;&lt;br /&gt;
        usage&lt;br /&gt;
&lt;br /&gt;
        if [ ! -f /etc/pve/qemu-server/$Old.conf ] ; then&lt;br /&gt;
                echo &amp;quot;Error: $Old is not a valid VM ID&amp;quot;&lt;br /&gt;
                exit 1&lt;br /&gt;
        fi&lt;br /&gt;
fi&lt;br /&gt;
if [ -z &amp;quot;$New&amp;quot; ] {{!}}{{!}} ! expr &amp;quot;$New&amp;quot; : &#039;^[0-9]\+$&#039; &amp;gt;/dev/null ; then&lt;br /&gt;
        echo &amp;quot;Error: Invalid new-vimd&amp;quot;&lt;br /&gt;
        usage&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Found these disks in the config:&amp;quot;&lt;br /&gt;
cat /etc/pve/qemu-server/$Old.conf {{!}} grep -i -- vm-$Old- {{!}} awk -F, &#039;{print $1}&#039; {{!}} awk &#039;{print $2}&#039;&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Found these disks on LVM:&amp;quot;&lt;br /&gt;
lvs --noheadings -o lv_name,vg_name {{!}} grep &amp;quot;vm-$Old-&amp;quot;&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Found these disks on ZFS:&amp;quot;&lt;br /&gt;
zfs list {{!}} grep &amp;quot;vm-$Old-&amp;quot; {{!}} awk &#039;{print $1}&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Will execute the following:&amp;quot;&lt;br /&gt;
echo&lt;br /&gt;
lvs --noheadings -o lv_name,vg_name {{!}} grep &amp;quot;vm-$Old-&amp;quot; {{!}} while read lv vg ; do&lt;br /&gt;
        echo lvrename $vg/$lv $vg/$(echo $lv {{!}} sed &amp;quot;s/$Old/$New/g&amp;quot;)&lt;br /&gt;
done&lt;br /&gt;
zfs list {{!}} grep &amp;quot;vm-$Old-&amp;quot; {{!}} awk &#039;{print $1}&#039; {{!}} while read i ; do&lt;br /&gt;
        echo zfs rename $i $(echo $i {{!}} sed &amp;quot;s/$Old/$New/g&amp;quot;)&lt;br /&gt;
done&lt;br /&gt;
echo sed -i &amp;quot;s/$Old/$New/g&amp;quot; /etc/pve/qemu-server/$Old.conf&lt;br /&gt;
echo mv /etc/pve/qemu-server/$Old.conf /etc/pve/qemu-server/$New.conf&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
echo&lt;br /&gt;
read -p &amp;quot;Proceed with the rename? (y/n) &amp;quot; answer&lt;br /&gt;
case $answer in&lt;br /&gt;
  [yY]* ) echo &amp;quot;Proceeding...&amp;quot;;;&lt;br /&gt;
  * ) echo &amp;quot;Aborting...&amp;quot;; exit;;&lt;br /&gt;
esac&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Task: Rename LVS data volumes&amp;quot;&lt;br /&gt;
lvs --noheadings -o lv_name,vg_name {{!}} grep &amp;quot;vm-$Old-&amp;quot; {{!}} while read lv vg ; do&lt;br /&gt;
        lvrename $vg/$lv $vg/$(echo $lv {{!}} sed &amp;quot;s/$Old/$New/g&amp;quot;)&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Task: Rename ZFS datasets&amp;quot;&lt;br /&gt;
zfs list {{!}} grep &amp;quot;vm-$Old-&amp;quot; {{!}} awk &#039;{print $1}&#039; {{!}} while read i ; do&lt;br /&gt;
        zfs rename $i $(echo $i {{!}} sed &amp;quot;s/$Old/$New/g&amp;quot;)&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Renumbering IDs&amp;quot;&lt;br /&gt;
sed -i &amp;quot;s/$Old/$New/g&amp;quot; /etc/pve/qemu-server/$Old.conf&lt;br /&gt;
mv /etc/pve/qemu-server/$Old.conf /etc/pve/qemu-server/$New.conf&lt;br /&gt;
| lang = bash&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Remove a single node cluster ===&lt;br /&gt;
If you created a single node cluster and want to undo it, run:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # systemctl stop pve-cluster corosync&lt;br /&gt;
# pmxcfs -l&lt;br /&gt;
# rm /etc/corosync/*&lt;br /&gt;
# rm /etc/pve/corosync.conf&lt;br /&gt;
# killall pmxcfs&lt;br /&gt;
# systemctl start pve-cluster&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
This was copied from: https://forum.proxmox.com/threads/proxmox-ve-6-removing-cluster-configuration.56259/&lt;br /&gt;
&lt;br /&gt;
=== Run Docker in LXC ===&lt;br /&gt;
Docker works in Proxmox LXC but only if the nesting option is enabled. Docker works on either privileged LXC or unprivileged LXC but each has their own caveats:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Feature&lt;br /&gt;
!Privileged&lt;br /&gt;
!Unprivileged&lt;br /&gt;
|-&lt;br /&gt;
|&#039;&#039;&#039;NFS&#039;&#039;&#039;&lt;br /&gt;
|Can mount NFS; requires nfs=1 enabled in LXC configuration&lt;br /&gt;
|Cannot mount NFS unless you use a bind mountpoint&lt;br /&gt;
|-&lt;br /&gt;
|&#039;&#039;&#039;Security&#039;&#039;&#039;&lt;br /&gt;
|UIDs are not mapped; may be dangerous&lt;br /&gt;
|UIDs/GIDs are mapped&lt;br /&gt;
|-&lt;br /&gt;
|&#039;&#039;&#039;OverlayFS&#039;&#039;&#039;&lt;br /&gt;
|ZFS backing filesystem for &amp;lt;code&amp;gt;/var/lib/docker&amp;lt;/code&amp;gt; should have no issues.&lt;br /&gt;
|&amp;lt;code&amp;gt;/var/lib/docker&amp;lt;/code&amp;gt; should use EXT or XFS (or some non-ZFS filesystem, more on this below.)&lt;br /&gt;
|}&lt;br /&gt;
It&#039;s recommended that you do not run Docker in LXC unless you have a good reason such as: a) hardware passthrough on systems that cannot support it through VMs, or b) tight memory constraints where containerized workloads benefit from shared memory.&lt;br /&gt;
&lt;br /&gt;
==== Issues with /var/lib/docker on a ZFS backed storage ====&lt;br /&gt;
Docker uses overlayfs to handle the container layers. On ZFS backed storage, it requires additional privileges to allow it to work properly. For LXC that are unprivileged, certain docker image pulls may fail if the image data is stored on ZFS storage. To work around this issue, you will have to use a different filesystem for &amp;lt;code&amp;gt;/var/lib/docker&amp;lt;/code&amp;gt; by making a zvol and then formatting it with a different filesystem (see https://du.nkel.dev/blog/2021-03-25_proxmox_docker/). Alternatively, put the &amp;lt;code&amp;gt;/var/lib/docker&amp;lt;/code&amp;gt; volume on a non-ZFS backed storage such as a LVM backed storage.  &lt;br /&gt;
&lt;br /&gt;
This issue manifests itself as issues when pulling some (but not all) container images with errors similar to: &lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = [root@nixos:~]# docker-compose pull&lt;br /&gt;
[+] Pulling 8/10&lt;br /&gt;
 ⠋ pihole 9 layers [⣿⣿⣿⣿⣿⣿⣿⣿⣿] 23.91MB/23.91MB Pulling                                                                 &lt;br /&gt;
   ✔ f03b40093957 Pull complete                                                                                        &lt;br /&gt;
   ✔ 8063479210c7 Pull complete                                                                                        &lt;br /&gt;
   ✔ 4f4fb700ef54 Pull complete                                                                                        &lt;br /&gt;
   ✔ 061a6a1d9010 Pull complete                                                                                        &lt;br /&gt;
   ✔ 8b1e64a56394 Pull complete                                                                                        &lt;br /&gt;
   ✔ 8dabcf07e578 Pull complete                                                                                        &lt;br /&gt;
   ⠿ bdec3efaf98a Extracting      [==================================================&amp;gt;]  23.91MB/23.91MB               &lt;br /&gt;
   ✔ 40cba0bade6e Download complete                                                                                    &lt;br /&gt;
   ✔ 9b797b6be3f3 Download complete                                                                                    &lt;br /&gt;
failed to register layer: ApplyLayer exit status 1 stdout:  stderr: unlinkat /var/cache/apt/archives: invalid argument&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Switching &amp;lt;code&amp;gt;/var/lib/docker&amp;lt;/code&amp;gt; to a ext backed filesystem fixed the issue. This can be done with the following steps:&lt;br /&gt;
&lt;br /&gt;
# Create the zvol:  &amp;lt;code&amp;gt;zfs create -V 8G rpool/data/ctvol-111-disk-1&amp;lt;/code&amp;gt;&lt;br /&gt;
# Format the zvol: &amp;lt;code&amp;gt;mkfs.ext4 /dev/zvol/rpool/data/ctvol-111-disk-1&amp;lt;/code&amp;gt;&lt;br /&gt;
# Edit the LXC config under &amp;lt;code&amp;gt;/etc/pve/nodes/pve/lxc/111.conf&amp;lt;/code&amp;gt; and add the following mountpoint config: &lt;br /&gt;
#* &amp;lt;code&amp;gt;mp0: /dev/zvol/rpool/data/ctvol-111-disk-1,mp=/var/lib/docker&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Install NVIDIA driver in Proxmox ===&lt;br /&gt;
The NVIDIA Linux driver requires the following dependencies:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # apt install gcc make pve-headers&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
As part of the installation, the driver will ensure nouveau is disabled. It&#039;ll blacklist the nouveau kernel module for you but you will have to reboot (or &amp;lt;code&amp;gt;rmmod&amp;lt;/code&amp;gt; it) before running the installer again.&lt;br /&gt;
&lt;br /&gt;
Once you have the NVIDIA drivers installed, to hardware accelerate your VM:&lt;br /&gt;
&lt;br /&gt;
* Set the graphics card of the VM to VirGL GPU&lt;br /&gt;
* Install the needed dependencies: &amp;lt;code&amp;gt;apt install libgl1 libegl1&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Setup LXC with NVIDIA GPU ===&lt;br /&gt;
You can share your NVIDIA GPU with one or more LXC in Proxmox. This is possible only if the following conditions are met:&lt;br /&gt;
&lt;br /&gt;
# NVIDIA drivers are installed on Proxmox. {{Highlight&lt;br /&gt;
| code = # wget https://us.download.nvidia.com/XFree86/Linux-x86_64/535.54.03/NVIDIA-Linux-x86_64-535.54.03.run&lt;br /&gt;
# chmod 755 NVIDIA-Linux-x86_64-535.54.03.run&lt;br /&gt;
# ./NVIDIA-Linux-x86_64-535.54.03.run&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# The &amp;lt;u&amp;gt;exact same&amp;lt;/u&amp;gt; NVIDIA drivers are installed in the LXC. (Basically, run the same thing as above. If you&#039;re using NixOS, you might have to do some overrides or match the drivers on Proxmox to what was pulled in when you do nixos-rebuild switch).&lt;br /&gt;
# The NVIDIA devices is passed through to the LXC using the &amp;lt;code&amp;gt;lxc.mount.entry&amp;lt;/code&amp;gt; setting. Do a listing of all the &amp;lt;code&amp;gt;/dev/nvidia*&amp;lt;/code&amp;gt; devices and ensure that you catch all the device numbers.{{Highlight&lt;br /&gt;
| code = lxc.cgroup2.devices.allow: c 226:* rwm&lt;br /&gt;
lxc.cgroup2.devices.allow: c 195:* rwm&lt;br /&gt;
lxc.cgroup2.devices.allow: c 508:* rwm&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
# LXC is allowed to read the NVIDIA devices using the &amp;lt;code&amp;gt;lxc.cgroups2.devices.allow&amp;lt;/code&amp;gt; setting. Ensure that you capture all the &amp;lt;code&amp;gt;/dev/nvidia*&amp;lt;/code&amp;gt; devices that&#039;s appropriate for your card.{{Highlight&lt;br /&gt;
| code = lxc.mount.entry: /dev/dri/card1 dev/dri/card0 none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/dri/renderD129 dev/dri/renderD128 none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia-caps dev/nvidia-caps none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
# Start the LXC. You should be able to run &#039;&amp;lt;code&amp;gt;nvidia-smi&amp;lt;/code&amp;gt;&#039; from inside the container and have it see the status of the card. If it doesn&#039;t, doublecheck everything above: ensure your driver version numbers are matching (it would tell you if it didn&#039;t) and that you have the appropriate device numbers allowed.&lt;br /&gt;
&lt;br /&gt;
==== How to set up Jellyfin on a Ubuntu based LXC ====&lt;br /&gt;
I used to run Jellyfin under a Docker VM with a NVIDIA GPU using PCIe passthrough, but I wanted to try running jellyfin on another machine which doesn&#039;t support iommu. To work around this, I attempted to run Jellyfin within a LXC. &lt;br /&gt;
&lt;br /&gt;
What I did:&lt;br /&gt;
&lt;br /&gt;
# Create a LXC as a privileged container. Two reasons for this: 1) The device passthrough sort of needs it and 2) I need to mount my Jellyfin media files via NFS&lt;br /&gt;
# Set up LXC with a Ubuntu image. Install Jellyfin as per the https://jellyfin.org/docs/general/installation/linux/#ubuntu instructions. Additionally, install the &amp;lt;code&amp;gt;jellyfin-ffmpeg5 libnvidia-decode-525 libnvidia-encode-525 nvidia-utils-525&amp;lt;/code&amp;gt; packages for GPU support. I used version 525 as that&#039;s the most recent stable version offered by NVIDIA. Note that because we&#039;re using these pre-packaged binaries, the driver you install on the PVE must match the exact version.&lt;br /&gt;
# Look at the specific version of the nvidia packages that was installed in the previous step. You can also do a apt search libnvidia-encode to see what versions are available. Download and install the NVIDIA drivers on the Proxmox server itself while ensuring you get the exact version that matches the packages you installed in the LXC environment.&lt;br /&gt;
# Manually edit the LXC config under /etc/pve/lxc/&amp;lt;id&amp;gt;.conf and add: {{Highlight&lt;br /&gt;
| code = lxc.cgroup2.devices.allow: c 226:* rwm&lt;br /&gt;
lxc.cgroup2.devices.allow: c 195:* rwm&lt;br /&gt;
lxc.mount.entry: /dev/dri/card1 dev/dri/card0 none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/dri/renderD129 dev/dri/renderD128 none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia-caps dev/nvidia-caps none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file&lt;br /&gt;
| lang = text&lt;br /&gt;
}} /dev/dri/card1 and /dev/dri/renderD129 are the devices that appeared after installing the NVIDIA drivers on the PVE which made it obvious that they were what I wanted. If you&#039;re unsure, you could try unloading the Nvidia driver and see what disappears.&lt;br /&gt;
# Restart the LXC environment and see if nvidia-smi works. If it doesn&#039;t, ensure your version numbers are matching (it would tell you if it didn&#039;t) and that you have the appropriate device bind mounted into the LXC.&lt;br /&gt;
# Continue with the jellyfin installation as usual. Run: &amp;lt;code&amp;gt;wget -O- https://repo.jellyfin.org/install-debuntu.sh | sudo bash&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== How to run Docker on a NixOS based LXC ====&lt;br /&gt;
I later moved Jellyfin to NixOS running docker in LXC. This was done to simplify the Jellyfin update process and to leverage my existing docker-compose infrastructure. The NixOS LXC is privileged with nesting enabled (as both are required by Docker).&lt;br /&gt;
&lt;br /&gt;
Here is the NixOS config to get Docker installed and with the NVIDIA drivers and runtime set up:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = virtualisation.docker = {&lt;br /&gt;
    enable = true;&lt;br /&gt;
    enableNvidia = true;&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  # Make sure opengl is enabled&lt;br /&gt;
  hardware.opengl = {&lt;br /&gt;
    enable = true;&lt;br /&gt;
    driSupport = true;&lt;br /&gt;
    driSupport32Bit = true;&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  # NVIDIA drivers are unfree.&lt;br /&gt;
  nixpkgs.config.allowUnfreePredicate = pkg:&lt;br /&gt;
    builtins.elem (lib.getName pkg) [&lt;br /&gt;
      &amp;quot;nvidia-x11&amp;quot;&lt;br /&gt;
      &amp;quot;nvidia-settings&amp;quot;&lt;br /&gt;
      &amp;quot;nvidia-persistenced&amp;quot;&lt;br /&gt;
    ];&lt;br /&gt;
&lt;br /&gt;
  # Tell Xorg to use the nvidia driver&lt;br /&gt;
  services.xserver.videoDrivers = [&amp;quot;nvidia&amp;quot;];&lt;br /&gt;
&lt;br /&gt;
  hardware.nvidia = {&lt;br /&gt;
      package = config.boot.kernelPackages.nvidiaPackages.production;&lt;br /&gt;
   };&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
After doing a NixOS rebuild, docker works and docker containers with the nvidia runtime also works.&lt;br /&gt;
&lt;br /&gt;
==== Troubleshooting NVIDIA LXC issues ====&lt;br /&gt;
&lt;br /&gt;
===== Missing nvidia-uvm =====&lt;br /&gt;
You also need the cuda device &amp;lt;code&amp;gt;nvidia-uvm&amp;lt;/code&amp;gt; passed through. &lt;br /&gt;
&lt;br /&gt;
Symptoms of having this device missing are the ffmpeg bundled with Jellyfin with cuda support will fail with this error message:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = [AVHWDeviceContext @ 0x55eea5d88f80] cu-&amp;gt;cuInit(0) failed -&amp;gt; CUDA_ERROR_UNKNOWN: unknown error&lt;br /&gt;
Device creation failed: -542398533.&lt;br /&gt;
Failed to set value &#039;cuda=cu:0&#039; for option &#039;init_hw_device&#039;: Generic error in an external library&lt;br /&gt;
Error parsing global options: Generic error in an external library&lt;br /&gt;
| lang = text&lt;br /&gt;
}}If the device isn&#039;t missing but something is misconfigured, strace would show the process attempting to read but failing:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = openat(AT_FDCWD, &amp;quot;/dev/nvidia-uvm&amp;quot;, O_RDWR{{!}}O_CLOEXEC) = -1 EPERM (Operation not permitted)&lt;br /&gt;
openat(AT_FDCWD, &amp;quot;/dev/nvidia-uvm&amp;quot;, O_RDWR) = -1 EPERM (Operation not permitted)&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
====== The fix ======&lt;br /&gt;
If the device &amp;lt;code&amp;gt;/dev/nvidia-uvm&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;/dev/nvidia-uvm-tools&amp;lt;/code&amp;gt; isn&#039;t available on the PVE (ie. it&#039;s missing), then you will have to run this:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ## Get the device number&lt;br /&gt;
# grep nvidia-uvm /proc/devices {{!}} awk &#039;{print $1}&#039;&lt;br /&gt;
&lt;br /&gt;
## Then create the device node using the device number above (508 for me, but this can change!)&lt;br /&gt;
#  mknod -m 666 /dev/nvidia-uvm c 508 0&lt;br /&gt;
#  mknod -m 666 /dev/nvidia-uvm-tools c 508 0&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
See also: https://old.reddit.com/r/qnap/comments/s7bbv6/fix_for_missing_nvidiauvm_device_devnvidiauvm/&lt;br /&gt;
&lt;br /&gt;
If you&#039;re using LXC, you also have to edit the LXC config file (at &amp;lt;code&amp;gt;/etc/pve/nodes/pve/lxc/###.conf&amp;lt;/code&amp;gt;) and add the following. Adjust the device numbers as needed.&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = lxc.cgroup2.devices.allow: c 226:* rwm&lt;br /&gt;
lxc.cgroup2.devices.allow: c 195:* rwm&lt;br /&gt;
lxc.cgroup2.devices.allow: c 510:* rwm&lt;br /&gt;
lxc.mount.entry: /dev/dri/card1 dev/dri/card0 none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/dri/renderD129 dev/dri/renderD128 none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia-caps dev/nvidia-caps none bind,optional,create=file&lt;br /&gt;
lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Randomly losing NVIDIA device access ====&lt;br /&gt;
Whenever you change a LXC setting (such as memory or CPU change), it will break access to the NVIDIA GPUs from within the LXC. Any existing processes with the NVIDIA GPU opened will continue to work, but any new processes trying to access the GPU will fail. The only fix is to restart the LXC.&lt;br /&gt;
&lt;br /&gt;
=== Replace a failed ZFS mirror boot device ===&lt;br /&gt;
If you are using ZFS mirror as the OS boot device and need to replace one of the disks in the mirror, keep in mind the following:&lt;br /&gt;
&lt;br /&gt;
* Each disk in the ZFS mirror has 3 partitions: 1. BIOS boot, 2. grub, 3. ZFS block device. When you replace a disk, you will have to recreate this partition scheme before you can replace the ZFS device using the 3rd partition.&lt;br /&gt;
* Grub is installed on the 2nd partition on all disks in the ZFS mirror. If you are replacing the disk that your BIOS uses to boot, you should still  be able to boot Proxmox by booting off the other disks from the boot menu. After replacing the failed disk, you can reinstall the grub bootloader.&lt;br /&gt;
&lt;br /&gt;
The steps to replace a failed disk in a ZFS mirror that&#039;s used as the boot device on Proxmox are:&lt;br /&gt;
&lt;br /&gt;
# Remove the failed device and replace it with the new device&lt;br /&gt;
# Once you see the new device on your system (with lsblk), set up the partition table: &amp;lt;code&amp;gt;sgdisk &amp;lt;healthy bootable device&amp;gt; -R &amp;lt;new device&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
# Randomize the GUIDs &amp;lt;code&amp;gt;sgdisk -G &amp;lt;healthy bootable device&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
# Replace the failed device in your zpool. Check the status with &amp;lt;code&amp;gt;zpool status&amp;lt;/code&amp;gt; and then replace the failed device with &amp;lt;code&amp;gt;zpool replace -f &amp;lt;pool&amp;gt; &amp;lt;old zfs partition&amp;gt; &amp;lt;new zfs partition&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
# Reinstall the bootloader on the grub partition on the new device by formatting the partition: &amp;lt;code&amp;gt;proxmox-boot-tool format /dev/sda2&amp;lt;/code&amp;gt;,&lt;br /&gt;
# Then reinstall grub: &amp;lt;code&amp;gt;proxmox-boot-tool init /dev/sda2&amp;lt;/code&amp;gt;&lt;br /&gt;
See Proxmox&#039;s sysadmin documentation on this topic at: https://pve.proxmox.com/pve-docs/chapter-sysadmin.html#sysboot_proxmox_boot_setup&lt;br /&gt;
&lt;br /&gt;
=== Migrating VMs from a Proxmox OS disk ===&lt;br /&gt;
I wanted to reinstall Proxmox on another disk on the same machine while preserving the VMs and containers. This required installing a clean copy of Proxmox on the new drive and then migrating all existing VMs and containers to the new install from the existing disk.&lt;br /&gt;
&lt;br /&gt;
I was able to get this all done by:&lt;br /&gt;
&lt;br /&gt;
* On the old install (while it&#039;s still running), Tar up &amp;lt;code&amp;gt;/etc/pve&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;/var/lib/rrdcached/db&amp;lt;/code&amp;gt; from the old disk. You have to do this while the old version is running since the &amp;lt;code&amp;gt;.conf&amp;lt;/code&amp;gt; files seem to disappear when the system&#039;s off (I didn&#039;t investigate why due to time constraints). Restore it to the new Proxmox install.&lt;br /&gt;
* Add the old disk back as storage on the new Proxmox install. You do not and should not need to format anything. For LVM storage, just add a LVM storage and select the appropriate device. Your VM disks should appear and you should then be able to migrate it / attach it to the VMs which you imported in the previous step.&lt;br /&gt;
&lt;br /&gt;
==Troubleshooting==&lt;br /&gt;
===LXC containers does not start networking===&lt;br /&gt;
I have a Fedora based LXC container which takes a long time to start up. After the tty does start, the only network adapter isn&#039;t up. &amp;lt;code&amp;gt;systemd-networkd&amp;lt;/code&amp;gt; is also in a a failed state with a bad exit code and status &amp;lt;code&amp;gt;226/namespace&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
This was resolved by enabling nesting on this container with the following commands: &lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # pct set $CTID -features nesting=1 &lt;br /&gt;
# pct reboot $CTID&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== PCIe passthrough fails due to platform RMRR ===&lt;br /&gt;
This is covered in more details at [[HP DL380 G6#PCIe passthrough fails due to platform RMRR requirement|HP_DL380_page on PCIe_passthrough_failing]]. Basically, the workaround for this is to install a patched kernel that doesn&#039;t respect the RMRR restrictions.&lt;br /&gt;
&lt;br /&gt;
If you want to build your own patched kernel, you might be interested in this repo: https://github.com/sarrchri/relax-intel-rmrr&lt;br /&gt;
&lt;br /&gt;
=== NVIDIA driver crashes with KVM using VirGL GPU ===&lt;br /&gt;
On Proxmox 7.4 with NVIDIA driver version 535.54.03 installed, starting a virtual machine with a VirGL GPU device results in KVM segfaulting:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = [Wed Jul 26 11:13:38 2023] kvm[559117]: segfault at 0 ip 00007ff13a39ca23 sp 00007ffd4eb76450 error 4 in libnvidia-eglcore.so.535.54.03[7ff1398bd000+164e000]&lt;br /&gt;
[Wed Jul 26 11:13:38 2023] Code: 48 8b 45 10 44 8b a0 04 0d 00 00 e9 44 fe ff ff 0f 1f 80 00 00 00 00 48 8b 77 08 48 89 df e8 e4 4d 78 ff 48 8b 7d 10 48 89 de &amp;lt;48&amp;gt; 8b 07 ff 90 98 01 00 00 48 8b 43 38 e9 f2 fd ff ff 0f 1f 00 48&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
A reboot of the Proxmox server did not help.&lt;br /&gt;
&lt;br /&gt;
Fix: I re-installed the NVIDIA drivers but also answered &#039;yes&#039; for the 32bit libraries to be installed. Perhaps that was the fix, or the act of re-installing the driver recompiled the driver on the current running kernel which fixed the issue.&lt;br /&gt;
&lt;br /&gt;
=== Network interface reset: Detected Hardware Unit Hang ===&lt;br /&gt;
I have a machine with an Intel Corporation 82579V Gigabit network adapter which periodically stutters especially when the server is under heavy load (mainly I/O with some CPU) with the following kernel message being triggered:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = [Fri Aug 18 00:05:03 2023] e1000e 0000:00:19.0 eno1: Detected Hardware Unit Hang:&lt;br /&gt;
                             TDH                  &amp;lt;1d&amp;gt;&lt;br /&gt;
                             TDT                  &amp;lt;77&amp;gt;&lt;br /&gt;
                             next_to_use          &amp;lt;77&amp;gt;&lt;br /&gt;
                             next_to_clean        &amp;lt;1c&amp;gt;&lt;br /&gt;
                           buffer_info[next_to_clean]:&lt;br /&gt;
                             time_stamp           &amp;lt;105642dfd&amp;gt;&lt;br /&gt;
                             next_to_watch        &amp;lt;1d&amp;gt;&lt;br /&gt;
                             jiffies              &amp;lt;1056437b0&amp;gt;&lt;br /&gt;
                             next_to_watch.status &amp;lt;0&amp;gt;&lt;br /&gt;
                           MAC Status             &amp;lt;40080083&amp;gt;&lt;br /&gt;
                           PHY Status             &amp;lt;796d&amp;gt;&lt;br /&gt;
                           PHY 1000BASE-T Status  &amp;lt;3800&amp;gt;&lt;br /&gt;
                           PHY Extended Status    &amp;lt;3000&amp;gt;&lt;br /&gt;
                           PCI Status             &amp;lt;10&amp;gt;&lt;br /&gt;
[Fri Aug 18 00:05:03 2023] e1000e 0000:00:19.0 eno1: Reset adapter unexpectedly&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
When this happens, all networking pauses temporarily and may cause connections to break.&lt;br /&gt;
&lt;br /&gt;
Some online research suggests the issue could be with the e1000e driver having some sort of bug which in conjunction with TCP segmentation offloading (TSO) may cause the interface to hang. The workaround is to disable hardware TSO which you can do with &amp;lt;code&amp;gt;ethool&amp;lt;/code&amp;gt;:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ethtool -K eno1 tso off gso off&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
This change will revert on the next reboot. If it helps with your situation, you can make it persistent by editing &amp;lt;code&amp;gt;/etc/network/interfaces&amp;lt;/code&amp;gt; with a &amp;lt;code&amp;gt;post-up&amp;lt;/code&amp;gt; command:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = auto eno1&lt;br /&gt;
iface eno1 inet static&lt;br /&gt;
  address 10.1.2.2&lt;br /&gt;
  netmask 255.255.252.0&lt;br /&gt;
  gateway 10.1.1.1&lt;br /&gt;
  post-up /sbin/ethtool -K eno1 tso off gso off&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== NVidia PCIe Bus Error ===&lt;br /&gt;
On my X99-E WS + Xeon E5-1660 + NVidia Quadro P400 machine running kernel 6.8.12-8-pve, which is currently my primary Proxmox box, I&#039;m now seeing a never ending stream of correctable errors from the GPU. The GPU&#039;s plugged into the first slot. Kernel messages with these errors look like this:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = root@pve:~# dmesg -T {{!}} tail&lt;br /&gt;
[Sat Aug  2 22:10:12 2025] pcieport 0000:00:03.0: AER: Correctable error message received from 0000:05:00.0&lt;br /&gt;
[Sat Aug  2 22:10:12 2025] nvidia 0000:05:00.0: PCIe Bus Error: severity=Correctable, type=Physical Layer, (Receiver ID)&lt;br /&gt;
[Sat Aug  2 22:10:12 2025] nvidia 0000:05:00.0:   device [10de:1cb3] error status/mask=00000001/0000a000&lt;br /&gt;
[Sat Aug  2 22:10:12 2025] nvidia 0000:05:00.0:    [ 0] RxErr                  (First)&lt;br /&gt;
[Sat Aug  2 22:10:14 2025] pcieport 0000:00:03.0: AER: Correctable error message received from 0000:05:00.0&lt;br /&gt;
[Sat Aug  2 22:10:14 2025] nvidia 0000:05:00.0: PCIe Bus Error: severity=Correctable, type=Physical Layer, (Receiver ID)&lt;br /&gt;
[Sat Aug  2 22:10:14 2025] nvidia 0000:05:00.0:   device [10de:1cb3] error status/mask=00000001/0000a000&lt;br /&gt;
[Sat Aug  2 22:10:14 2025] nvidia 0000:05:00.0:    [ 0] RxErr                  (First)&lt;br /&gt;
&lt;br /&gt;
root@pve:~# lspci -nnn {{!}} grep 05:00&lt;br /&gt;
05:00.0 VGA compatible controller [0300]: NVIDIA Corporation GP107GL [Quadro P400] [10de:1cb3] (rev a1)&lt;br /&gt;
05:00.1 Audio device [0403]: NVIDIA Corporation GP107GL High Definition Audio Controller [10de:0fb9] (rev a1)&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
There are some potential solutions that involve adding kernel arguments to &amp;lt;code&amp;gt;/etc/default/grub&amp;lt;/code&amp;gt;, then run &amp;lt;code&amp;gt;update-grub&amp;lt;/code&amp;gt;, and then reboot.&lt;br /&gt;
&lt;br /&gt;
==== What didn&#039;t help ====&lt;br /&gt;
&lt;br /&gt;
* Adding &amp;lt;code&amp;gt;acpi_enforce_resources=no&amp;lt;/code&amp;gt; to the kernel args and rebooting did not help.&lt;br /&gt;
&lt;br /&gt;
What to try instead:&lt;br /&gt;
&lt;br /&gt;
* Try &amp;lt;code&amp;gt;pcie_aspm=off&amp;lt;/code&amp;gt; which will disable PCIe Active State Power Management.&lt;br /&gt;
* Try pci=noaer which will turn off error reporting&lt;br /&gt;
{{Navbox Linux}}&lt;br /&gt;
[[Category:Linux]]&lt;br /&gt;
[[Category:LinuxUtilities]]&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Thingino&amp;diff=7698</id>
		<title>Thingino</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Thingino&amp;diff=7698"/>
		<updated>2025-08-03T04:16:33Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Thingino is an open-source firmware for Ingenic based IP cameras. This is a good alternative firmware for the Wyze Cam (v2, v3, but not the v4) as it has RTSP support with an up-to-date kernel/OS.&lt;br /&gt;
&lt;br /&gt;
== Installation ==&lt;br /&gt;
Josh at WL Tech Blog has made Thingino installers for all the supported cameras available from his GitHub repo:&lt;br /&gt;
&lt;br /&gt;
* https://github.com/wltechblog/thingino-installers&lt;br /&gt;
&lt;br /&gt;
Simply find the installer for your camera and follow the instructions provided.&lt;br /&gt;
&lt;br /&gt;
=== Wyze cam v3 ===&lt;br /&gt;
Installation on the Wyze Cam v3 is simple: You simply need to copy a few files to your SD card and then reboot the camera. You don&#039;t even need to touch it if you already have wz_mini_hacks installed as you just need to copy the files via SSH and reboot.&lt;br /&gt;
&lt;br /&gt;
# Download the latest installer files from https://github.com/wltechblog/thingino-installers/tree/main/wyze-cam-3&lt;br /&gt;
# Extract the files and then upload it to the SD card. if you&#039;re using wz_mini_hacks with SSH enabled, you can just copy it via SSH: {{Highlight&lt;br /&gt;
| code = $ for i in  factory_t31_ZMC6tiIDQN  thingino-wyze_cam3_t31al_gc2053_atbm6031.bin  thingino-wyze_cam3_t31x_gc2053_atbm6031.bin  thingino-wyze_cam3_t31x_gc2053_rtl8189ftv.bin ; do cat &amp;quot;$i&amp;quot;  {{!}} ssh root@wyze-camera &amp;quot;cat - &amp;gt; /media/mmc/$i&amp;quot; ; done&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Reboot the camera and wait 2-3 minutes.&lt;br /&gt;
# You should see a wireless access point show up once this is done named &amp;quot;THINGINO-XXXX&amp;quot;. Connect to it and then go to http://172.16.0.1/ to complete the setup. After you enter your password/wifi settings, you will see the MAC address of the device.&lt;br /&gt;
&lt;br /&gt;
== Frigate integration ==&lt;br /&gt;
Getting Thingino and Frigate with nvidia hardware acceleration seems to be a little tempermental when configured with a specific combination of configuration settings that leads to the detection and recording processes to eventually misbehave and crash.  When this misbehavior happens, ffmpeg uses 100% GPU and rapidly eats up as much system memory as it can get before OOM kills it. While this happens, the preview shows corrupt frames like this:&lt;br /&gt;
[[File:Frigate misbehaving.jpg|alt=Frigate + nvidia acceleration + Thingino misbehaving which causes frames to scramble until ffmpeg crashes.|none|thumb|Frigate + nvidia acceleration + Thingino misbehaving which causes frames to scramble until ffmpeg crashes.]]&lt;br /&gt;
A few things that I had to do to make it work:&lt;br /&gt;
&lt;br /&gt;
# If you use the sub stream, you shouldn&#039;t use hwaccel_args : preset-nvidia or else ffmpeg goes off the rails and then crashes&lt;br /&gt;
# If you want to use hwaccel_args: preset-nvidia, then don&#039;t use the sub stream as an input. You can still use it as a live stream.&lt;br /&gt;
&lt;br /&gt;
This seems to work for me:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = garage:&lt;br /&gt;
    ffmpeg:&lt;br /&gt;
      hwaccel_args: preset-nvidia&lt;br /&gt;
      output_args:&lt;br /&gt;
        record: preset-record-generic-audio-aac&lt;br /&gt;
      inputs:&lt;br /&gt;
        - path: rtsp://127.0.0.1:8554/wyze-garage?timeout=30&lt;br /&gt;
          input_args: preset-rtsp-restream&lt;br /&gt;
          roles:&lt;br /&gt;
            - record&lt;br /&gt;
            - detect&lt;br /&gt;
            - audio&lt;br /&gt;
    live:&lt;br /&gt;
      stream_name: wyze-garage_sub&lt;br /&gt;
go2rtc:&lt;br /&gt;
  streams:&lt;br /&gt;
    wyze-garage:&lt;br /&gt;
       - rtsp://thingino:thingino@ip:554/ch0#timeout=30&lt;br /&gt;
    wyze-garage_sub:&lt;br /&gt;
       - rtsp://thingino:thingino@ip:554/ch1#timeout=30&lt;br /&gt;
&lt;br /&gt;
  webrtc:&lt;br /&gt;
    candidates:&lt;br /&gt;
      - 10.1.2.30:8555&lt;br /&gt;
      - stun:8555&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=File:Frigate_misbehaving.jpg&amp;diff=7697</id>
		<title>File:Frigate misbehaving.jpg</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=File:Frigate_misbehaving.jpg&amp;diff=7697"/>
		<updated>2025-08-03T04:15:22Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Frigate with ffmpeg misbehaving&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Thingino&amp;diff=7696</id>
		<title>Thingino</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Thingino&amp;diff=7696"/>
		<updated>2025-08-03T03:58:20Z</updated>

		<summary type="html">&lt;p&gt;Leo: Created page with &amp;quot;Thingino is an open-source firmware for Ingenic based IP cameras. This is a good alternative firmware for the Wyze Cam (v2, v3, but not the v4) as it has RTSP support with an up-to-date kernel/OS.  == Installation == Josh at WL Tech Blog has made Thingino installers for all the supported cameras available from his GitHub repo:  * https://github.com/wltechblog/thingino-installers  Simply find the installer for your camera and follow the instructions provided.  === Wyze ca...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Thingino is an open-source firmware for Ingenic based IP cameras. This is a good alternative firmware for the Wyze Cam (v2, v3, but not the v4) as it has RTSP support with an up-to-date kernel/OS.&lt;br /&gt;
&lt;br /&gt;
== Installation ==&lt;br /&gt;
Josh at WL Tech Blog has made Thingino installers for all the supported cameras available from his GitHub repo:&lt;br /&gt;
&lt;br /&gt;
* https://github.com/wltechblog/thingino-installers&lt;br /&gt;
&lt;br /&gt;
Simply find the installer for your camera and follow the instructions provided.&lt;br /&gt;
&lt;br /&gt;
=== Wyze cam v3 ===&lt;br /&gt;
Installation on the Wyze Cam v3 is simple: You simply need to copy a few files to your SD card and then reboot the camera. You don&#039;t even need to touch it if you already have wz_mini_hacks installed as you just need to copy the files via SSH and reboot.&lt;br /&gt;
&lt;br /&gt;
# Download the latest installer files from https://github.com/wltechblog/thingino-installers/tree/main/wyze-cam-3&lt;br /&gt;
# Extract the files and then upload it to the SD card. if you&#039;re using wz_mini_hacks with SSH enabled, you can just copy it via SSH: {{Highlight&lt;br /&gt;
| code = $ for i in  factory_t31_ZMC6tiIDQN  thingino-wyze_cam3_t31al_gc2053_atbm6031.bin  thingino-wyze_cam3_t31x_gc2053_atbm6031.bin  thingino-wyze_cam3_t31x_gc2053_rtl8189ftv.bin ; do cat &amp;quot;$i&amp;quot;  {{!}} ssh root@wyze-camera &amp;quot;cat - &amp;gt; /media/mmc/$i&amp;quot; ; done&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Reboot the camera and wait 2-3 minutes.&lt;br /&gt;
# You should see a wireless access point show up once this is done named &amp;quot;THINGINO-XXXX&amp;quot;. Connect to it and then go to http://172.16.0.1/ to complete the setup. After you enter your password/wifi settings, you will see the MAC address of the device.&lt;br /&gt;
&lt;br /&gt;
== Frigate integration ==&lt;br /&gt;
Getting Thingino and Frigate with nvidia hardware acceleration seems to be a little tempermental with frequent issues that cause the detection and recording threads to crash. A few things that I had to do to make it work:&lt;br /&gt;
&lt;br /&gt;
# If you use the sub stream, you shouldn&#039;t use hwaccel_args : preset-nvidia or else ffmpeg goes off the rails and then crashes&lt;br /&gt;
# If you want to use hwaccel_args: preset-nvidia, then don&#039;t use the sub stream as an input. You can still use it as a live stream.&lt;br /&gt;
&lt;br /&gt;
This seems to work for me:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = garage:&lt;br /&gt;
    ffmpeg:&lt;br /&gt;
      hwaccel_args: preset-nvidia&lt;br /&gt;
      output_args:&lt;br /&gt;
        record: preset-record-generic-audio-aac&lt;br /&gt;
      inputs:&lt;br /&gt;
        - path: rtsp://127.0.0.1:8554/wyze-garage?timeout=30&lt;br /&gt;
          input_args: preset-rtsp-restream&lt;br /&gt;
          roles:&lt;br /&gt;
            - record&lt;br /&gt;
            - detect&lt;br /&gt;
            - audio&lt;br /&gt;
    live:&lt;br /&gt;
      stream_name: wyze-garage_sub&lt;br /&gt;
go2rtc:&lt;br /&gt;
  streams:&lt;br /&gt;
    wyze-garage:&lt;br /&gt;
       - rtsp://thingino:thingino@ip:554/ch0#timeout=30&lt;br /&gt;
    wyze-garage_sub:&lt;br /&gt;
       - rtsp://thingino:thingino@ip:554/ch1#timeout=30&lt;br /&gt;
&lt;br /&gt;
  webrtc:&lt;br /&gt;
    candidates:&lt;br /&gt;
      - 10.1.2.30:8555&lt;br /&gt;
      - stun:8555&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Inkbird_PTH-9C&amp;diff=7695</id>
		<title>Inkbird PTH-9C</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Inkbird_PTH-9C&amp;diff=7695"/>
		<updated>2025-08-02T05:28:21Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The Inkbird PTH-9C is a 3-in-1 CO2/temperature/humidity detector with a LCD display. It has a [[MH-Z19 Carbon Dioxide Sensor|Winsen MH-Z19E]] sensor which is capable of detecting CO2 between 400 and 10,000 ppm, although the firmware in this device only reports up to 5,000 ppm.&lt;br /&gt;
&lt;br /&gt;
=== Wireless support? ===&lt;br /&gt;
[[File:PTH-9C Board.jpg|alt=PTH-9C Board|thumb|PTH-9C Board]]&lt;br /&gt;
The PTH-9C does not come with any wireless capabilities, unlike the PTH-9CW model. However, it appears that the main board is identical to the PTH-9CW with some components, including the crucial tuya chip missing. &lt;br /&gt;
&lt;br /&gt;
Is it possible to solder a ESP8266 chip on the existing pads and take readings? Likely not. I don&#039;t see any serial traffic on the TX/RX pads, so the firmware that&#039;s on the unmarked IC is likely not configured to send traffic out.&lt;br /&gt;
&lt;br /&gt;
If you really do want to get the CO2 sensor data, read it from the MH-Z19E directly from pin 3 (center pin on the 5 pin header) at 9600 baud 8N1. The IC on this unit polls sensor data about every 1 second with the sensor reporting measurements in this format:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = start, command = 86, high + low, 42, 3 bytes of 0&#039;s, checksum&lt;br /&gt;
ff 86 01 f5 42 000000 42&lt;br /&gt;
ff 86 01 f5 42 000000 42&lt;br /&gt;
ff 86 01 f5 42 000000 42&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
The important bits are the high+low values, which when read as &amp;lt;code&amp;gt;0x01F5&amp;lt;/code&amp;gt; gives you 501 (ppm).&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[MH-Z19 Carbon Dioxide Sensor|The MH-Z19 CO2 sensor]]&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=File:PTH-9C_Board.jpg&amp;diff=7694</id>
		<title>File:PTH-9C Board.jpg</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=File:PTH-9C_Board.jpg&amp;diff=7694"/>
		<updated>2025-08-02T05:27:39Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;PTH-9C Board&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Inkbird_PTH-9C&amp;diff=7693</id>
		<title>Inkbird PTH-9C</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Inkbird_PTH-9C&amp;diff=7693"/>
		<updated>2025-08-02T05:11:28Z</updated>

		<summary type="html">&lt;p&gt;Leo: Created page with &amp;quot;The Inkbird PTH-9C is a 3-in-1 CO2/temperature/humidity detector with a LCD display. It has a Winsen MH-Z19E sensor which is capable of detecting CO2 between 400 and 10,000 ppm, although the firmware in this device only reports up to 5,000 ppm.  === Wireless support? === The PTH-9C does not come with any wireless capabilities, unlike the PTH-9CW model. However, it appears that the main board is identical to the PTH-9CW with some component...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The Inkbird PTH-9C is a 3-in-1 CO2/temperature/humidity detector with a LCD display. It has a [[MH-Z19 Carbon Dioxide Sensor|Winsen MH-Z19E]] sensor which is capable of detecting CO2 between 400 and 10,000 ppm, although the firmware in this device only reports up to 5,000 ppm.&lt;br /&gt;
&lt;br /&gt;
=== Wireless support? ===&lt;br /&gt;
The PTH-9C does not come with any wireless capabilities, unlike the PTH-9CW model. However, it appears that the main board is identical to the PTH-9CW with some components, including the crucial tuya chip missing. &lt;br /&gt;
&lt;br /&gt;
Is it possible to solder a ESP8266 chip on the existing pads and take readings? Likely not. I don&#039;t see any serial traffic on the TX/RX pads, so the firmware that&#039;s on the unmarked IC is likely not configured to send traffic out.&lt;br /&gt;
&lt;br /&gt;
If you really do want to get the CO2 sensor data, read it from the MH-Z19E directly from pin 3 (center pin on the 5 pin header) at 9600 baud 8N1. The IC on this unit polls sensor data about every 1 second with the sensor reporting measurements in this format:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = start, command = 86, high + low, 42, 3 bytes of 0&#039;s, checksum&lt;br /&gt;
ff 86 01 f5 42 000000 42&lt;br /&gt;
ff 86 01 f5 42 000000 42&lt;br /&gt;
ff 86 01 f5 42 000000 42&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
The important bits are the high+low values, which when read as &amp;lt;code&amp;gt;0x01F5&amp;lt;/code&amp;gt; gives you 501 (ppm).&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[MH-Z19 Carbon Dioxide Sensor|The MH-Z19 CO2 sensor]]&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=CloudStack&amp;diff=7692</id>
		<title>CloudStack</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=CloudStack&amp;diff=7692"/>
		<updated>2025-07-10T17:49:48Z</updated>

		<summary type="html">&lt;p&gt;Leo: /* Rebuilding UI */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Apache CloudStack is open-source cloud computing software. It is used to deploy a infrastructure as a service (IaaS) platform on virtualization technologies such as KVM, VMware, and Xen. This is similar to OpenStack but is significantly simpler to setup and manage (albeit with less features).&lt;br /&gt;
&lt;br /&gt;
This page contains my notes on setting up and using CloudStack 4.15. I am by no means a CloudStack expert so take my notes here with a huge grain of salt and feel free to make corrections.&lt;br /&gt;
&lt;br /&gt;
==Installation==&lt;br /&gt;
This installation is based on CloudStack 4.15 using CentOS 8. The setup described below uses KVM and Open vSwitch. I&#039;m basing the design decisions and approach from the installation guide at [http://docs.cloudstack.apache.org/en/latest/quickinstallationguide/qig.html#management-server-installation http://docs.cloudstack.apache.org/en/latest/quickinstallationguide/qig.html]&lt;br /&gt;
&lt;br /&gt;
===Overview===&lt;br /&gt;
I will have 1 management node and a few bare metal nodes. All nodes will have the same processor (Intel something) and memory (24GB).&lt;br /&gt;
&lt;br /&gt;
Each node will have the same network configuration based on OpenVSwitch. There will be only 1 ethernet connection per node with various VLANs trunked to each node. The VLANs are:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Network&lt;br /&gt;
!Vlan&lt;br /&gt;
!Network subnet&lt;br /&gt;
|-&lt;br /&gt;
|Management&lt;br /&gt;
|11, untagged&lt;br /&gt;
|172.19.0.0/20&lt;br /&gt;
|-&lt;br /&gt;
|Storage&lt;br /&gt;
|3205&lt;br /&gt;
|172.22.0.0/24&lt;br /&gt;
|-&lt;br /&gt;
|Guest&lt;br /&gt;
|100 - 200&lt;br /&gt;
|n/a&lt;br /&gt;
|-&lt;br /&gt;
|Public&lt;br /&gt;
|2&lt;br /&gt;
|136.159.1.0/24&lt;br /&gt;
|}&lt;br /&gt;
The network configs for the 4 nodes I&#039;ll be using are listed below. There is also a NFS server used for primary storage. The reason for the weird IPs is because this was set up on an existing network.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Node&lt;br /&gt;
!Networks&lt;br /&gt;
|-&lt;br /&gt;
|management&lt;br /&gt;
|Management: 172.19.12.141/20&lt;br /&gt;
Storage: 172.22.0.241/24&lt;br /&gt;
|-&lt;br /&gt;
|baremetal1&lt;br /&gt;
|Management: 172.19.12.142/20&lt;br /&gt;
Storage: 172.22.0.242/24&lt;br /&gt;
|-&lt;br /&gt;
|baremetal2&lt;br /&gt;
|Management: 172.19.12.143/20&lt;br /&gt;
Storage: 172.22.0.243/24&lt;br /&gt;
|-&lt;br /&gt;
|baremetal3&lt;br /&gt;
|Management: 172.19.12.144/20&lt;br /&gt;
Storage: 172.22.0.244/24&lt;br /&gt;
|-&lt;br /&gt;
|netapp1&lt;br /&gt;
|Storage: 172.22.0.19/24&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Switch config===&lt;br /&gt;
For completeness, here&#039;s the configuration of the HP Procurve switch that the nodes are connected to. The switch should have all the guest VLANs defined and tagged.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = config&lt;br /&gt;
&lt;br /&gt;
# Guest VLANs&lt;br /&gt;
vlan 100 name guest100&lt;br /&gt;
vlan 101 name guest101&lt;br /&gt;
...&lt;br /&gt;
vlan 200 name guest 200&lt;br /&gt;
interface 1-8 tagged vlan 100-200&lt;br /&gt;
&lt;br /&gt;
# Public, management, storage VLANs&lt;br /&gt;
vlan 2 name public&lt;br /&gt;
vlan 11 name management&lt;br /&gt;
vlan 3205 name storage&lt;br /&gt;
interface 1-8 untagged vlan 11&lt;br /&gt;
interface 1-8 tagged vlan 2,3205&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
===Node setup===&lt;br /&gt;
Each node will be set up with the following sub-steps.&lt;br /&gt;
&lt;br /&gt;
====CloudStack Repos====&lt;br /&gt;
Install CloudStack repos.{{Highlight&lt;br /&gt;
| code = # cat &amp;gt; /etc/yum.repos.d/cloudstack.repo &amp;lt;&amp;lt;EOF&lt;br /&gt;
[cloudstack]&lt;br /&gt;
name=cloudstack&lt;br /&gt;
baseurl=http://download.cloudstack.org/centos/8/4.15/&lt;br /&gt;
enabled=1&lt;br /&gt;
gpgcheck=0&lt;br /&gt;
EOF&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
====Install base packages====&lt;br /&gt;
Install all other dependencies.{{Highlight&lt;br /&gt;
| code = # yum -y install epel-release&lt;br /&gt;
# yum -y install bridge-utils net-tools&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}Install OpenVSwitch from CentOS Extras:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # yum -y install \&lt;br /&gt;
http://mirror.centos.org/centos/8/extras/x86_64/os/Packages/centos-release-nfv-openvswitch-1-3.el8.noarch.rpm \&lt;br /&gt;
http://mirror.centos.org/centos/8/extras/x86_64/os/Packages/centos-release-nfv-common-1-3.el8.noarch.rpm&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
====Disable SELinux====&lt;br /&gt;
The system should have SELinux disabled. Use &amp;lt;code&amp;gt;setenforce&amp;lt;/code&amp;gt; and edit the selinux config:{{Highlight&lt;br /&gt;
| code = # setenforce 0&lt;br /&gt;
# vi /etc/selinux/config &lt;br /&gt;
## disable selinux&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
====Disable firewalld====&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # systemctl stop firewalld&lt;br /&gt;
# systemctl disable firewalld&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
====Configure Open vSwitch====&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # echo &amp;quot;blacklist bridge&amp;quot; &amp;gt;&amp;gt; /etc/modprobe.d/local-blacklist.conf&lt;br /&gt;
# echo &amp;quot;install bridge /bin/false&amp;quot; &amp;gt;&amp;gt; /etc/modprobe.d/local-dontload.conf&lt;br /&gt;
&lt;br /&gt;
# systemctl enable --now openvswitch&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
We will be using network-scripts to configure the Open vSwitch bridges later. I removed NetworkManager but retained network-scripts to ensure NetworkManager doesn&#039;t interfere with my network setup. The install guide leaves NetworkManager around.&lt;br /&gt;
&lt;br /&gt;
I create a &#039;shared&#039; bridge that&#039;s tied to the network interface called &amp;lt;code&amp;gt;nic0&amp;lt;/code&amp;gt;. This was done to make it easier to change the bridge setup during my testing but this could be simplified. Each of the physical networks I later set up in CloudStack are its own individual bridge to make it obvious how VMs get connected to the network.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ovs-vsctl add-br   nic0&lt;br /&gt;
# ovs-vsctl add-port nic0 enp4s0f0 tag=11 vlan_mode=native-untagged&lt;br /&gt;
# ovs-vsctl set port nic0 trunks=2,11,40-49,3205&lt;br /&gt;
&lt;br /&gt;
# ovs-vsctl add-br management0 nic0 11&lt;br /&gt;
# ovs-vsctl add-br cloudbr0 nic0 2&lt;br /&gt;
# ovs-vsctl add-br cloudbr1 nic0 100&lt;br /&gt;
# ovs-vsctl add-br storage0 nic0 3205&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}The node&#039;s management IP address needs to be removed from the primary network interface and then assigned on the management0 interface. If you&#039;re doing this to a node remotely, this might interrupt your connection.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ip addr del 172.19.12.141/20 dev enp4s0f0&lt;br /&gt;
# ip addr add 172.19.12.141/20 dev management0&lt;br /&gt;
# ip route add default via 172.19.0.3&lt;br /&gt;
# ip addr add 172.22.0.241/24 dev storage0&lt;br /&gt;
&lt;br /&gt;
# ip link set management0 up&lt;br /&gt;
# ip link set storage0 up&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
====Network configuration====&lt;br /&gt;
Once the Open vSwitch bridges are set up, configure the interfaces as follows:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Network Interface&lt;br /&gt;
!Role&lt;br /&gt;
!Configuration&lt;br /&gt;
|-&lt;br /&gt;
|enp4s0f0&lt;br /&gt;
|primary NIC in the host&lt;br /&gt;
|up on boot; no IP&lt;br /&gt;
|-&lt;br /&gt;
|nic0&lt;br /&gt;
|network OVS switch that connects to the other bridges to the NIC&lt;br /&gt;
|up on boot; no IP&lt;br /&gt;
|-&lt;br /&gt;
|cloudbr0&lt;br /&gt;
|public traffic.&lt;br /&gt;
|up on boot; no IP&lt;br /&gt;
|-&lt;br /&gt;
|cloudbr1&lt;br /&gt;
|guest traffic&lt;br /&gt;
|up on boot; no IP&lt;br /&gt;
|-&lt;br /&gt;
|management0&lt;br /&gt;
|management traffic&lt;br /&gt;
|up on boot; assigned with management IP&lt;br /&gt;
|-&lt;br /&gt;
|storage0&lt;br /&gt;
|storage traffic&lt;br /&gt;
|up on boot; assigned with storage network IP&lt;br /&gt;
|-&lt;br /&gt;
|cloud0&lt;br /&gt;
|link local traffic&lt;br /&gt;
|up on boot; assigned 169.254.0.1/16&lt;br /&gt;
|}Network configs are applied using network-scripts. The idea here is to have the network interfaces be configured when the system boots automatically. For interfaces that require a static IP address, I used the following network-scripts file. Adjust the device name and IP address as required.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # cat &amp;lt;&amp;lt;EOF &amp;gt; /etc/sysconfig/network-scripts/ifcfg-cloudbr0&lt;br /&gt;
DEVICE=cloudbr0&lt;br /&gt;
TYPE=Bridge&lt;br /&gt;
ONBOOT=yes&lt;br /&gt;
BOOTPROTO=static&lt;br /&gt;
IPV6INIT=no&lt;br /&gt;
IPV6_AUTOCONF=no&lt;br /&gt;
DELAY=5&lt;br /&gt;
IPADDR=172.16.10.2&lt;br /&gt;
GATEWAY=172.16.10.1&lt;br /&gt;
NETMASK=255.255.255.0&lt;br /&gt;
DNS1=8.8.8.8&lt;br /&gt;
DNS2=8.8.4.4&lt;br /&gt;
USERCTL=no&lt;br /&gt;
NM_CONTROLLED=no&lt;br /&gt;
EOF&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}For devices that don&#039;t require a static IP:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = cat &amp;lt;&amp;lt;EOF &amp;gt; ifcfg-cloudbr0&lt;br /&gt;
DEVICE=cloudbr0&lt;br /&gt;
TYPE=OVSBridge&lt;br /&gt;
DEVICETYPE=ovs&lt;br /&gt;
ONBOOT=yes&lt;br /&gt;
BOOTPROTO=none&lt;br /&gt;
HOTPLUG=no&lt;br /&gt;
NM_CONTROLLED=no&lt;br /&gt;
EOF&lt;br /&gt;
| lang = text&lt;br /&gt;
}}Once configured, verify that your node comes up with the proper network settings on a reboot.&lt;br /&gt;
&lt;br /&gt;
===Management node setup===&lt;br /&gt;
On the management node, set up the network configs and the CloudStack management packages.&lt;br /&gt;
&lt;br /&gt;
====Setup Storage====&lt;br /&gt;
If you intend to use the management server as the primary and secondary storage, you will need to set up a NFS server. If you intend to use an external NFS server as the primary storage, you can skip this step.{{Highlight&lt;br /&gt;
| code = # mkdir -p /export/primary /export/secondary&lt;br /&gt;
# yum -y install nfs-utils&lt;br /&gt;
# cat &amp;gt; /etc/exports &amp;lt;&amp;lt;EOF&lt;br /&gt;
/export/secondary *(rw,async,no_root_squash,no_subtree_check)&lt;br /&gt;
/export/primary *(rw,async,no_root_squash,no_subtree_check)&lt;br /&gt;
EOF&lt;br /&gt;
# systemctl enable --now nfs-server&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
====CloudStack management services====&lt;br /&gt;
Install MySQL. MariaDB isn&#039;t supported and the installation fails with it.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # rpm -ivh http://repo.mysql.com/mysql80-community-release-el8.rpm&lt;br /&gt;
# yum -y install mysql-server&lt;br /&gt;
# yum -y install mysql-connector-python&lt;br /&gt;
&lt;br /&gt;
## edit /etc/my.cnf to have the following lines.&lt;br /&gt;
cat &amp;gt;&amp;gt; /etc/my.cnf &amp;lt;&amp;lt;EOF&lt;br /&gt;
[mysqld]&lt;br /&gt;
innodb_rollback_on_timeout=1&lt;br /&gt;
innodb_lock_wait_timeout=600&lt;br /&gt;
max_connections=350&lt;br /&gt;
log-bin=mysql-bin&lt;br /&gt;
binlog-format = &#039;ROW&#039;&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
# systemctl enable --now mysqld&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Setup CloudStack.{{Highlight&lt;br /&gt;
| code = # yum -y install cloudstack-management&lt;br /&gt;
&lt;br /&gt;
# cloudstack-setup-databases cloud:password@localhost --deploy-as=root&lt;br /&gt;
# cloudstack-setup-management&lt;br /&gt;
# systemctl enable --now cloudstack-management&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}After starting &amp;lt;code&amp;gt;cloudstack-management&amp;lt;/code&amp;gt; for the firs time, it might take from 2-10 minutes for the database to set up completely. During this time, the web interface won&#039;t be responsive. In the mean time, you will need to seed the system VM images to the secondary storage. If you are using an external NFS server for your secondary storage, adjust the mount point in the following command accordingly.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ## Seed the systemvm into secondary storage&lt;br /&gt;
# /usr/share/cloudstack-common/scripts/storage/secondary/cloud-install-sys-tmplt -m /export/secondary -u https://download.cloudstack.org/systemvm/4.15/systemvmtemplate-4.15.1-kvm.qcow2.bz2 -h kvm -F&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
We will continue the setup process via the web interface after setting up a bare metal node.&lt;br /&gt;
&lt;br /&gt;
===Bare metal node setup===&lt;br /&gt;
You should set up at least one bare metal node which will be used to set up your first zone and pod.&lt;br /&gt;
&lt;br /&gt;
On a bare metal node, set up everything outlined in the [[CloudStack#Node setup|Node setup]] section above. The node should have the CloudStack repos, Open vSwitch, SElinux/firewalld, and the networking configured. The agent node must have virtualization enabled on the CPU and KVM should be installed. You should be able to find &amp;lt;code&amp;gt;/dev/kvm&amp;lt;/code&amp;gt; on the system.&lt;br /&gt;
&lt;br /&gt;
====CloudStack Agent====&lt;br /&gt;
To set up the node, install the &amp;lt;code&amp;gt;cloudstack-agent&amp;lt;/code&amp;gt; package.&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # yum -y install cloudstack-agent&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Configure &amp;lt;code&amp;gt;qemu&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;libvirtd&amp;lt;/code&amp;gt;. If you need some starting configs, try the following:{{Highlight&lt;br /&gt;
| code = ## edit /etc/libvirt/qemu.conf &lt;br /&gt;
vnc_listen=0.0.0.0&lt;br /&gt;
&lt;br /&gt;
## edit /etc/libvirt/libvirtd.conf&lt;br /&gt;
listen_tcp = 1&lt;br /&gt;
tcp_port = &amp;quot;16509&amp;quot;&lt;br /&gt;
listen_tls = 0&lt;br /&gt;
tls_port = &amp;quot;16514&amp;quot;&lt;br /&gt;
auth_tcp = &amp;quot;none&amp;quot;&lt;br /&gt;
mdns_adv = 0&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}The CloudStack install guide instructs you to edit the libvirtd arguments to &amp;lt;code&amp;gt;--listen&amp;lt;/code&amp;gt;, but this will prevent libvirtd from starting using systemd. Instead, you should skip this step entirely because the CloudStack agent will configure this for you when you add the node to a zone.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ## The install guide suggests editing /etc/sysconfig/libvirtd to use the listen flag.&lt;br /&gt;
## However, this only works if you&#039;re not using systemd or using the libvirtd-tcp socket.&lt;br /&gt;
## I skipped this step since the agent will configure this later on.&lt;br /&gt;
LIBVIRTD_ARGS=&amp;quot;--listen&amp;quot;&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}Start the CloudStack agent. Verify that the cloudstack-agent is running. At this point libvirtd should also running (it&#039;s a service dependency). &lt;br /&gt;
&lt;br /&gt;
With the agent running on the node, you should now be able to add the node to the CloudStack cluster. This process will rewrite the &amp;lt;code&amp;gt;libvirtd.conf&amp;lt;/code&amp;gt; file and it should set &amp;lt;code&amp;gt;listen_tcp=0&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;listen_tls=1&amp;lt;/code&amp;gt; for you (so that libvirt traffic such as migrations are done via TLS rather than basic TCP). &lt;br /&gt;
&lt;br /&gt;
====Allow sudo access====&lt;br /&gt;
Ensure that &amp;lt;code&amp;gt;/etc/sudoers&amp;lt;/code&amp;gt; does not require TTY. In the older documentation, CloudStack requires that the &#039;cloud&#039; user be able to sudo with the addition of &amp;lt;code&amp;gt;Defaults:cloud !requiretty&amp;lt;/code&amp;gt;. However, looking at the installation on the CentOS 8 box, the agent actually runs as root, so perhaps root needs to be able to sudo? &lt;br /&gt;
&lt;br /&gt;
===Setting up your first zone===&lt;br /&gt;
At this point in the process, you should have at least one bare metal host and your management node should be up and running and it should be serving the CloudStack web UI at http://cloudstack:8080/client. Login using the default &amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;password&amp;lt;/code&amp;gt; credentials.&lt;br /&gt;
&lt;br /&gt;
You will be greeted with a setup wizard. I have had no luck with this and it&#039;s better to ignore it. Instead, navigate to Infrastructure -&amp;gt; zones and manually set up your first zone. &lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Description&lt;br /&gt;
!Screenshot&lt;br /&gt;
|-&lt;br /&gt;
|There are 3 types of zones that you can create:&lt;br /&gt;
&lt;br /&gt;
#&#039;&#039;&#039;Basic zone&#039;&#039;&#039; - All guest VMs are placed on a single shared flat network. There is no isolation or security policies in place to prevent guest VMs from seeing each other.&lt;br /&gt;
#&#039;&#039;&#039;Advanced zone&#039;&#039;&#039; - Guest VMs can be placed in one or more VLAN based networks. Guest networks can either be isolated or L2. Isolated networks (depending on the chosen network offering) comes with a virtual router (VR) which offers NAT/SNAT and firewall services and uses one or more public IP addresses. L2 networks are similar but doesn&#039;t have a virtual router but instead requires these services to be offered externally.  Tenants can also create something called a virtual private cloud (VPC). A VPC is like a regular isolated guest network but with additional features. A VPC allows the user to:&lt;br /&gt;
##Create multiple subnets (called tiers) which can route with each other&lt;br /&gt;
##Network traffic between tiers can be controlled through Network ACLs&lt;br /&gt;
##One or more public IPs can be associated to a VPC.&lt;br /&gt;
##Like an isolated guest network, all subnets can be NATed out through a single public IP&lt;br /&gt;
##You can create a private gateway (and therefore static routes) within a VPC&lt;br /&gt;
##You can create a VPN connection to a VPC&lt;br /&gt;
#&#039;&#039;&#039;Advanced zone with security groups&#039;&#039;&#039; - Guest VMs are placed on a shared network that is publicly routable. There is no concept of a &#039;public&#039; network because the guest network should also be public. As a result, there is no ability to create any other kind of guest networks or VPCs. The only benefit here is the ability to define security groups per-VM (which is implemented via IPTables on the bare metal host). Because enabling security groups in a zone will restrict that zone from being able to create isolated guest networks or VPCs, the security group feature only appears useful in an environment where guests only need to connect to the internet.&lt;br /&gt;
&lt;br /&gt;
Be aware of each type&#039;s limitations before continuing.&lt;br /&gt;
&lt;br /&gt;
We will be creating an advanced network zone.&lt;br /&gt;
|[[File:CloudStack - New Zone 1.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|We will add the DNS resolvers for the zone and specify the hypervisor type (KVM). &lt;br /&gt;
Empty the guest CIDR since we&#039;re going to allow users to specify their own.&lt;br /&gt;
|[[File:CloudStack - New Zone 2.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|When using the advanced zone, you need to specify the physical networks for the management, storage, and public networks. &lt;br /&gt;
These should correspond to the physical network devices on the hypervisor. Recall that in the previous step where we set up the Open vSwitch bridges, we created the following bridges for each role:&lt;br /&gt;
&lt;br /&gt;
*management - management0&lt;br /&gt;
*storage - storage0&lt;br /&gt;
*public - cloudbr0&lt;br /&gt;
*guest - cloudbr1&lt;br /&gt;
|&amp;lt;gallery&amp;gt;&lt;br /&gt;
File:CloudStack - New Zone 3.png&lt;br /&gt;
File:CloudStack - New Zone 3a.png&lt;br /&gt;
&amp;lt;/gallery&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|Specify the public network. The addresses defined here populates the &#039;Public IP&#039; pool.&lt;br /&gt;
&lt;br /&gt;
All isolated guest networks and all VPCs will use one of the addresses defined in this pool for the SNAT/NAT. The addresses specified here should therefore be accessible from the internet.&lt;br /&gt;
|[[File:CloudStack - New Zone 4.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|Create a new pod. &lt;br /&gt;
&lt;br /&gt;
The pod network here should cover your management network subnet. The reserved IP addresses here will be used by system VMs that require access to the management network. &lt;br /&gt;
|[[File:CloudStack - New Zone 5.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|Specify the guest network VLAN range.&lt;br /&gt;
&lt;br /&gt;
Because we&#039;re using VLAN as an isolation method, this range specifies what VLANs the guest networks will use over the guest physical network.&lt;br /&gt;
|[[File:CloudStack - New Zone 6.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|Specify the storage network.&lt;br /&gt;
&lt;br /&gt;
The reserved start/end IPs will be used by system VMs that require access to the primary storage. &lt;br /&gt;
&lt;br /&gt;
If you are assigning static IPs on your bare metal hosts, ensure that the reserved addresses don&#039;t overlap with the IP range specified here (because I had CloudStack assign a VM with the same IP as a bare metal host)&lt;br /&gt;
|[[File:CloudStack - New Zone 7.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|Specify a cluster name.&lt;br /&gt;
|[[File:CloudStack - New Zone 8.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|Add your first bare metal host. &lt;br /&gt;
You must add one host now and can add additional ones later.&lt;br /&gt;
|[[File:CloudStack - New Zone 9.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|Specify your primary storage.&lt;br /&gt;
The server should be accessible from the storage network.&lt;br /&gt;
|[[File:CloudStack - New Zone 10.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|Specify your secondary storage.&lt;br /&gt;
&lt;br /&gt;
You need to have at least one NFS secondary storage that has been seeded with the system VM template. &lt;br /&gt;
&lt;br /&gt;
Secondary storage pools should be accessible from the management network (&#039;&#039;&#039;confirm&#039;&#039;&#039;?)&lt;br /&gt;
|[[File:CloudStack - New Zone 11.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|Launch the zone.&lt;br /&gt;
This step might take a few minutes. If all goes well, you can then enable the zone shortly after. If you run into any problems, check the logs on the management node at /var/log/cloudstack/management.&lt;br /&gt;
|[[File:CloudStack - New Zone 12.png|left|thumb]]&lt;br /&gt;
|}&lt;br /&gt;
Once your zone has been enabled, it should automatically start a Console Proxy VM and secondary storage VM. You can find this under Infrastructure -&amp;gt; System VMs. If for some reason the System VMs are not starting, check that your systemvm template is available in your secondary storage and that the cloud0 bridge on each host is up. You should be able to ping the link local IP address (the 169.254.x.x address) from the hypervisor.&lt;br /&gt;
&lt;br /&gt;
Once the two system VMs are running, verify that you&#039;re able to create new guest networks or VPCs. These networks should create a virtual router. &lt;br /&gt;
&lt;br /&gt;
==Configuration==&lt;br /&gt;
&lt;br /&gt;
===Service offerings===&lt;br /&gt;
&lt;br /&gt;
====Deployment planner====&lt;br /&gt;
There are a few deployment techniques that can be used. These are set within a compute offering and cannot be changed after it&#039;s been created (&#039;&#039;&#039;really?&#039;&#039;&#039; can we change it via API?). The options are:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Deployment planner&lt;br /&gt;
!Description&lt;br /&gt;
|-&lt;br /&gt;
|First fit&lt;br /&gt;
|Placed on the first host that has sufficient capacity&lt;br /&gt;
|-&lt;br /&gt;
|User dispersing&lt;br /&gt;
|Evenly distributes VMs by account across clusters&lt;br /&gt;
|-&lt;br /&gt;
|User concentrated&lt;br /&gt;
|Opposite of the above.&lt;br /&gt;
|-&lt;br /&gt;
|Implicit dedication&lt;br /&gt;
|requires or prefers (depending on planner mode) a dedicated host&lt;br /&gt;
|-&lt;br /&gt;
|Bare metal&lt;br /&gt;
|requires a bare metal host&lt;br /&gt;
|}&lt;br /&gt;
More information from [https://docs.cloudstack.apache.org/en/4.15.2.0/adminguide/service_offerings.html#compute-and-disk-service-offerings CloudStack&#039;s documentation on Compute and Disk Service Offerings].&lt;br /&gt;
&lt;br /&gt;
===Enable SAML2 authentication===&lt;br /&gt;
Enable the SAML2 plugin by setting &amp;lt;code&amp;gt;saml2.enabled=true&amp;lt;/code&amp;gt; under Global Settings. &lt;br /&gt;
&lt;br /&gt;
Set up SAML authentication by specifying the following settings:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Setting&lt;br /&gt;
!Description&lt;br /&gt;
!Example value&lt;br /&gt;
|-&lt;br /&gt;
|saml2.default.idpid&lt;br /&gt;
|The URL of the identity provider. This is likely obtained from the metadata URL and set by the SAML2 plugin every time CloudStack starts.&lt;br /&gt;
|&amp;lt;nowiki&amp;gt;https://sts.windows.net/c609a0ec-xxx-xxx-xxx-xxxxxxxxxxxx/&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|saml2.idp.metadata.url&lt;br /&gt;
|The metadata XML URL&lt;br /&gt;
|&amp;lt;nowiki&amp;gt;https://login.microsoftonline.com/609a0ec-xxx-xxx-xxx-xxxxxxxxxxxx/federationmetadata/2007-06/federationmetadata.xml?appid=c5b8df24-xxx-xxx-xxx-xxxxxxxxxxxx&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|saml2.sp.id&lt;br /&gt;
|The identifier string for this application&lt;br /&gt;
|cloudstack-test.my-organization.tld&lt;br /&gt;
|-&lt;br /&gt;
|saml2.redirect.url&lt;br /&gt;
|The redirect URL using your cloudstack domain.&lt;br /&gt;
|&amp;lt;nowiki&amp;gt;https://cloudstack-test.my-organization.tld/client&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|saml2.user.attribute&lt;br /&gt;
|The attribute to use as the username. &lt;br /&gt;
If you&#039;re not sure what&#039;s available, look at the management logs after a login attempt.&lt;br /&gt;
|For Azure AD, use the email address attribute: &amp;lt;nowiki&amp;gt;http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
Restart the management server. To allow a user access, create the user and enable SSO. The user&#039;s username must match the value that&#039;s obtained from the saml2.user.attribute field.&lt;br /&gt;
&lt;br /&gt;
====Bugs====&lt;br /&gt;
&lt;br /&gt;
=====SAML Request being rejected by Azure AD=====&lt;br /&gt;
If you are using Azure AD, you may have issues authenticating because the SAML request ID that&#039;s generated might begin with a number. When this happens, you will get an error similar to: &amp;lt;code&amp;gt;AADSTS7500529: The value &#039;692rv91k6dgmdas33vr3b2keahr4lqjv&#039; is not a valid SAML ID. The ID must not begin with a number.&amp;lt;/code&amp;gt;. For more information, see: https://github.com/apache/cloudstack/issues/5548&lt;br /&gt;
&lt;br /&gt;
=====Users cannot login via SSO=====&lt;br /&gt;
Users that will be using SAML for authentication will need to have their CloudStack accounts created with SSO enabled. There seems to be a bug with the CloudStack web UI where a user&#039;s SAML IdPID isn&#039;t settable (it gets set to a &#039;0&#039;). A work-around would be to create and authorize users via CloudMonkey.&lt;br /&gt;
&lt;br /&gt;
The steps on adding a new user are:&lt;br /&gt;
&lt;br /&gt;
#Create the user:  &amp;lt;code&amp;gt;create user firstname=First lastname=User email=user1@ucalgary.ca username=user1@ucalgary.ca account=RCS state=enabled password=asdf&amp;lt;/code&amp;gt;&lt;br /&gt;
#Find the user&#039;s ID: &amp;lt;code&amp;gt;list users domainid=&amp;lt;tab&amp;gt; filter=username,id&amp;lt;/code&amp;gt;&lt;br /&gt;
#Authorize the user: &amp;lt;code&amp;gt;authorize samlsso enable=true entityid=&amp;lt;nowiki&amp;gt;https://sts.windows.net/c609a0ec-xxx-xxx-xxx-xxxxxxxxxxxx/&amp;lt;/nowiki&amp;gt; userid=user-id&amp;lt;/code&amp;gt;&lt;br /&gt;
#Verify that the user is enabled for SSO: &amp;lt;code&amp;gt;list samlauthorization filter=userid,idpid,status&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
When authorizing a user, the entityid must be the URL of the identity provider. The end slash is also mandatory.&lt;br /&gt;
&lt;br /&gt;
===Enable SSL===&lt;br /&gt;
A few things to note about enabling SSL:&lt;br /&gt;
&lt;br /&gt;
*If you added hosts via IP address, enabling SSL would likely break the management-to-client connection. You might need to re-add the host so that the certificates all match up.&lt;br /&gt;
* On CloudStack 4.16, the button to upload a new certificate in the SSL dialog box does not work. This is fixed in 4.16.1.&lt;br /&gt;
&lt;br /&gt;
==== Preparing your SSL certificates ====&lt;br /&gt;
First, generate a private key and certificate signing request and then obtain your SSL certificate from a certificate authority. For a typical CloudStack installation, you should obtain SSL certificates for both your management server as well as your console proxy.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # openssl genrsa -out server.key 4096&lt;br /&gt;
# openssl req -new -sha256 \&lt;br /&gt;
         -key server.key \&lt;br /&gt;
         -subj &amp;quot;/C=CA/ST=Alberta/O=Steamr/CN=cloudstack-test.example.com&amp;quot; \&lt;br /&gt;
         -reqexts SAN \&lt;br /&gt;
         -extensions SAN \&lt;br /&gt;
         -config &amp;lt;(cat /etc/pki/tls/openssl.cnf &amp;lt;(printf &amp;quot;[SAN]\nsubjectAltName=DNS:cloudstack-test-console.example.com&amp;quot;)) \&lt;br /&gt;
         -out server.csr&lt;br /&gt;
## With the server.csr file, upload it to your Certificate Authority to obtained a signed certificate.&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Your certificate authority should have given you your signed certificate as well as the root and any other intermediate certificates in a X.509 (.crt) format. If you need to self sign this certificate signing request, do the following:{{Highlight&lt;br /&gt;
| code = ## Run the following only if you want to self sign your certificate&lt;br /&gt;
## Make your root CA&lt;br /&gt;
# openssl genrsa -des3 -out rootCA.key 4096&lt;br /&gt;
# openssl req -x509 -new -subj &amp;quot;/C=CA/ST=Alberta/O=Steamr/CN=example.com&amp;quot; -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.crt&lt;br /&gt;
&lt;br /&gt;
## Sign the certificate&lt;br /&gt;
# openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out server.crt -days 500 -sha256&lt;br /&gt;
&lt;br /&gt;
## Check the certificate&lt;br /&gt;
# openssl x509 -in server.crt -text -noout&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}Next, you need to convert your certificate into a PKCS12 format and your private key into a PKCS8 format. This is the only format that works with the CloudStack management server. We place the PKCS12 keystore file at &amp;lt;code&amp;gt;/etc/cloudstack/management/ssl_keystore.pkcs12&amp;lt;/code&amp;gt;.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ## Combine Files&lt;br /&gt;
# cat server.key server.crt intermediate.crt root.crt &amp;gt; combined.crt&lt;br /&gt;
&lt;br /&gt;
## Create keystore&lt;br /&gt;
## You may use &#039;password&#039; as the password&lt;br /&gt;
# openssl pkcs12 -in combined.crt -export -out combined.pkcs12&lt;br /&gt;
&lt;br /&gt;
## Import keystore&lt;br /&gt;
## Provide the same password above. Eg. &#039;password&#039;&lt;br /&gt;
# keytool -importkeystore -srckeystore combined.pkcs12 -srcstoretype PKCS12 -destkeystore /etc/cloudstack/management/ssl_keystore.pkcs12 -deststoretype pkcs12&lt;br /&gt;
&lt;br /&gt;
## Convert the private key into PKCS8 format&lt;br /&gt;
## Provide the same password above. Eg. &#039;password&#039;&lt;br /&gt;
# openssl pkcs8 -topk8 -in server.key -out server.pkcs8.encrypted.key&lt;br /&gt;
# openssl pkcs8 -in server.pkcs8.encrypted.key -out server.pkcs8.key&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Upload SSL certificates ====&lt;br /&gt;
You can upload SSL certificates to CloudStack under Infrastructure -&amp;gt; Summary and then clicking on the &#039;SSL Certificates&amp;quot; button. Provide the root certificate authority, the certificate, the private key (in PKCS8 format), and the domain that the certificate applies to. Wildcard domains should be specified as &amp;lt;code&amp;gt;*.example.com&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
Alternatively, you may use the CloudMonkey tool to upload certificates using the file parameter passing feature like so:&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # cmk upload customcertificate domainsuffix=cloudstack.steamr.com id=1 name=root certificate=@root.crt&lt;br /&gt;
# cmk upload customcertificate domainsuffix=cloudstack.steamr.com id=2 name=intermediate1 certificate=@intermediate.crt&lt;br /&gt;
# cmk upload customcertificate domainsuffix=cloudstack.steamr.com id=3 privatekey=@server.pkcs8.key certificate=@domain.crt&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Enabling HTTPS ====&lt;br /&gt;
Next, you will need to enable HTTPS on both the management console and console proxy.&lt;br /&gt;
&lt;br /&gt;
Note that you may enable the HTTPS setting only after at least one certificate has been uploaded. If the server has no certificates, the option is ignored.&lt;br /&gt;
&lt;br /&gt;
===== Enable HTTPS on the management console =====&lt;br /&gt;
The management console can be configured by editing &amp;lt;code&amp;gt;/etc/cloudstack/management/server.properties&amp;lt;/code&amp;gt; with the following lines. Set the keystore password to the same password you used above to import it.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = https.enable=true&lt;br /&gt;
https.port=8443&lt;br /&gt;
https.keystore=/etc/cloudstack/management/ssl_keystore.pkcs12&lt;br /&gt;
https.keystore.password=password&lt;br /&gt;
| lang = text&lt;br /&gt;
}}Restart the management server for this to apply.&lt;br /&gt;
{{Info&lt;br /&gt;
| title = Why port 8443?&lt;br /&gt;
| message = Because CloudStack runs under a non-root account, it can only bind to high port (&amp;gt; 1024) numbers. &lt;br /&gt;
&lt;br /&gt;
You can still have CloudStack visible on port 443 if you use a [[IPTables#Mapping incoming traffic to a different internal port|IPTables rule]].&lt;br /&gt;
}}&lt;br /&gt;
Confirm that you are able to reach your management console via HTTPS.&lt;br /&gt;
&lt;br /&gt;
==== Enable HTTPS on the console proxy ====&lt;br /&gt;
If you enable SSL on the management console, you will also need to enable SSL for the console proxies for the VNC web sockets to work properly. If your management console certificate (from the previous sections) contain a Subject Alternative Name (SAN) or is a wildcard certificate that includes your console proxy&#039;s DNS name, SSL for the console proxy should be working. If your certificates do not include the console proxy&#039;s DNS name, you will need to obtain another SSL certificate and add it to the SSL keystore and upload it to CloudStack using the same instructions above.&lt;br /&gt;
&lt;br /&gt;
==== Renewing SSL certificate ====&lt;br /&gt;
To renew a SSL certificate, you&#039;ll have to ensure that the keystore is updated and also upload the certificate via CloudMonkey or the management console (under Summary -&amp;gt; Certificates).&lt;br /&gt;
&lt;br /&gt;
In a folder containing your certificate (&amp;lt;code&amp;gt;server.crt&amp;lt;/code&amp;gt;), intermediate and root certificates (&amp;lt;code&amp;gt;intermediate.crt&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;root.crt&amp;lt;/code&amp;gt;), and also your private key (&amp;lt;code&amp;gt;server.key&amp;lt;/code&amp;gt;), run the following to update your SSL keystore and upload the certificates via cmk:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # cat server.key server.crt intermediate.crt root.crt &amp;gt; combined.crt&lt;br /&gt;
&lt;br /&gt;
## Note the keystore location that&#039;s defined in your configs&lt;br /&gt;
# grep https.keystore /etc/cloudstack/management/server.properties&lt;br /&gt;
https.keystore=/etc/cloudstack/management/ssl_keystore.pkcs12&lt;br /&gt;
https.keystore.password=xxxxxxx&lt;br /&gt;
&lt;br /&gt;
## Create keystore and import your certificate into it.&lt;br /&gt;
# openssl pkcs12 -in combined.crt -export -out combined.pkcs12&lt;br /&gt;
# keytool -importkeystore -srckeystore combined.pkcs12 -srcstoretype PKCS12 -destkeystore ssl_keystore.pkcs12 -deststoretype pkcs12&lt;br /&gt;
&lt;br /&gt;
## Move the keystore over the existing one as defined in the server config. You may want to backup the old one just in case.&lt;br /&gt;
# mv /etc/cloudstack/management/ssl_keystore.pkcs12 /etc/cloudstack/management/ssl_keystore.pkcs12-org&lt;br /&gt;
# mv ssl_keystore.pkcs12 /etc/cloudstack/management/ssl_keystore.pkcs12&lt;br /&gt;
&lt;br /&gt;
## Convert your key to pkcs8 if you haven&#039;t already done so. Use the same password for both commands.&lt;br /&gt;
# openssl pkcs8 -topk8 -in server.key -out server.pkcs8.key-encrypted&lt;br /&gt;
# openssl pkcs8 -in server.pkcs8.key-encrypted -out server.pkcs8.key&lt;br /&gt;
&lt;br /&gt;
## Upload your certificate&lt;br /&gt;
# for domain in $(openssl x509 -in server.crt -text -noout {{!}} grep DNS: {{!}} tr -d , {{!}} sed &#039;s/DNS://g&#039;) ; do&lt;br /&gt;
        echo &amp;quot;Uploading domain for $domain&amp;quot;&lt;br /&gt;
&lt;br /&gt;
        cmk upload customcertificate domainsuffix=$domain id=1 name=root certificate=@root.crt&lt;br /&gt;
        cmk upload customcertificate domainsuffix=$domain id=2 name=intermediate1 certificate=@intermediate.crt&lt;br /&gt;
        cmk upload customcertificate domainsuffix=$domain id=3 privatekey=@server.pkcs8.key certificate=@server.crt&lt;br /&gt;
done&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Tasks ==&lt;br /&gt;
===Re-add existing KVM bare metal host===&lt;br /&gt;
Once a host has been added to CloudStack, the CloudStack agent will have generated some public/private keys and configured itself to talk to the management node.  If you need to remove and re-add a host, you will need to clean up the agent before re-adding it back to CloudStack again. Based on my experience, I had to do the following:&lt;br /&gt;
&lt;br /&gt;
#Before removing the host from CloudStack, drain it of all VMs. &amp;lt;code&amp;gt;virsh list&amp;lt;/code&amp;gt; should be empty. If not and you&#039;ve removed the host from the management server already, manually kill each VM with &amp;lt;code&amp;gt;virsh destroy&amp;lt;/code&amp;gt;.&lt;br /&gt;
#&amp;lt;code&amp;gt;systemctl stop cloudstack-agent&amp;lt;/code&amp;gt;&lt;br /&gt;
#&amp;lt;code&amp;gt;rm -rf /etc/cloudstack/agent/cloud*&amp;lt;/code&amp;gt;&lt;br /&gt;
#unmount any primary storages with &amp;lt;code&amp;gt;umount /mnt/*&amp;lt;/code&amp;gt; and clean up with &amp;lt;code&amp;gt;rmdir /mnt/*&amp;lt;/code&amp;gt;&lt;br /&gt;
#&amp;lt;code&amp;gt;systemctl stop libvirtd&amp;lt;/code&amp;gt;&lt;br /&gt;
#&amp;lt;code&amp;gt;rm -rf /var/lib/libvirt/qemu&amp;lt;/code&amp;gt;&lt;br /&gt;
#You may need to edit &amp;lt;code&amp;gt;/etc/sysconfig/libvirtd&amp;lt;/code&amp;gt; to not use the listen flag. This might prevent libvirtd (and subsequently cloudstack-agent) from starting.&lt;br /&gt;
#Edit &amp;lt;code&amp;gt;/etc/cloudstack/agent/agent.properties&amp;lt;/code&amp;gt; and remove the keystore passphrase, any UUIDs, cluster/pod/zone, and the host. You should keep the guid or regenerate it with uuidgen. You should also keep the public/private/guest network devices set.&lt;br /&gt;
#Restart with &amp;lt;code&amp;gt;systemctl start cloudstack-agent&amp;lt;/code&amp;gt; (libvirt should come up automatically as it&#039;s a dependency). Ensure that it comes up OK.&lt;br /&gt;
&lt;br /&gt;
You may then re-add the host back to CloudStack.&lt;br /&gt;
&lt;br /&gt;
===Building RPMs===&lt;br /&gt;
To build the RPM packages from scratch, you&#039;ll need to install a bunch of dependencies and then run the build script. For more information, see:&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;nowiki&amp;gt;https://docs.cloudstack.apache.org/en/4.15.2.0/installguide/building_from_source.html#building-rpms-from-source&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # yum groupinstall &amp;quot;Development Tools&amp;quot;&lt;br /&gt;
# yum install java-11-openjdk-devel genisoimage mysql mysql-server createrepo&lt;br /&gt;
# yum install epel-release&lt;br /&gt;
&lt;br /&gt;
# curl -sL https://rpm.nodesource.com/setup_12.x {{!}} sudo bash -&lt;br /&gt;
# yum install nodejs&lt;br /&gt;
&lt;br /&gt;
# cat &amp;lt;&amp;lt;EOF &amp;gt; /etc/yum.repos.d/mysql.repo&lt;br /&gt;
[mysql-community]&lt;br /&gt;
name=MySQL Community connectors&lt;br /&gt;
baseurl=http://repo.mysql.com/yum/mysql-connectors-community/el/$releasever/$basearch/&lt;br /&gt;
gpgkey=http://repo.mysql.com/RPM-GPG-KEY-mysql&lt;br /&gt;
enabled=1&lt;br /&gt;
gpgcheck=1&lt;br /&gt;
EOF&lt;br /&gt;
# yum -y install mysql-connector-python&lt;br /&gt;
&lt;br /&gt;
enable powertools&lt;br /&gt;
&lt;br /&gt;
# yum install jpackage-utils maven&lt;br /&gt;
&lt;br /&gt;
# git clone https://github.com/apache/cloudstack.git&lt;br /&gt;
# cd cloudstack&lt;br /&gt;
# git checkout 4.15&lt;br /&gt;
&lt;br /&gt;
# cd packaging&lt;br /&gt;
# sh package.sh --distribution centos8&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Rebuilding UI ===&lt;br /&gt;
CloudStack&#039;s web interface is bundled with the pre-built cloudstack-ui package. If you need to make any custom changes to the UI, you can follow the build instructions from the [https://github.com/apache/cloudstack/tree/main/ui README file under the ui directory.]&lt;br /&gt;
&lt;br /&gt;
Building the UI is a straight forward process once you have all the necessary software dependencies in place. Once the UI is built, you can then install it on the management server and have it served instead of the &#039;stock&#039; bundled UI.&lt;br /&gt;
&lt;br /&gt;
==== Building the UI ====&lt;br /&gt;
You will need a server with npm installed along with a copy of the CloudStack repo. The simplest way I&#039;ve found to accomplish this is to create the following Docker image and then run the build process within the container.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = FROM rockylinux/rockylinux:8.10&lt;br /&gt;
&lt;br /&gt;
RUN curl -sL https://rpm.nodesource.com/setup_16.x {{!}} bash -&lt;br /&gt;
RUN yum -y install nodejs&lt;br /&gt;
| lang = dockerfile&lt;br /&gt;
}}&lt;br /&gt;
Clone the CloudStack repo (I placed it in /tmp in this example) and then run the following:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # docker build -t cs-ui-builder --progress=plain --no-cache .&lt;br /&gt;
&lt;br /&gt;
# docker run --rm -ti \&lt;br /&gt;
        -v /tmp/cloudstack:/cloudstack \&lt;br /&gt;
        -v /tmp/npm:/root/.npm cs-ui-builder \&lt;br /&gt;
        bash -c &amp;quot;cd /cloudstack/ui; npm install; npm run build&amp;quot;&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
We make a volume for the .npm directory to help speed up subsequent builds as the npm dependencies are cached, but this is entirely optional.&lt;br /&gt;
&lt;br /&gt;
==== Installing the UI ====&lt;br /&gt;
# Copy the &amp;lt;code&amp;gt;dist/&amp;lt;/code&amp;gt; directory to &amp;lt;code&amp;gt;/usr/share/cloudstack-management/webapp/&amp;lt;/code&amp;gt;&lt;br /&gt;
# Edit &amp;lt;code&amp;gt;/etc/cloudstack/management/server.properties&amp;lt;/code&amp;gt; and make sure that &amp;lt;code&amp;gt;webapp.dir&amp;lt;/code&amp;gt; is set to: &amp;lt;code&amp;gt;webapp.dir=/usr/share/cloudstack-management/webapp&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Restart the CloudStack management service and then reload the console page. Ensure that the vue app isn&#039;t cached.&lt;br /&gt;
&lt;br /&gt;
==== Installing and using CloudStack&#039;s prebuilt UI ====&lt;br /&gt;
The cloudstack-ui package contains the prebuilt CloudStack UI. The location of the UI files are placed under &amp;lt;code&amp;gt;/usr/share/cloudstack-ui&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
As of CloudStack 4.18, when I tried using this prebuilt package, I had to do the following things:&lt;br /&gt;
&lt;br /&gt;
* Edit &amp;lt;code&amp;gt;/etc/cloudstack/management/server.properties&amp;lt;/code&amp;gt; and set &amp;lt;code&amp;gt;webapp.dir=/usr/share/cloudstack-ui&amp;lt;/code&amp;gt;&lt;br /&gt;
* In &amp;lt;code&amp;gt;/usr/share/cloudstack-ui&amp;lt;/code&amp;gt;, run: &amp;lt;code&amp;gt;find . -type d -exec chmod -v o+x {} \;&amp;lt;/code&amp;gt; because the directories aren&#039;t executable by the &#039;cloud&#039; user.&lt;br /&gt;
* Create /usr/share/cloudstack-ui/WEB-INF and place this [https://github.com/apache/cloudstack/blob/main/client/src/main/webapp/WEB-INF/web.xml web.xml file] within. Otherwise, requests to the API break.&lt;br /&gt;
&lt;br /&gt;
===Usage server===&lt;br /&gt;
Install &amp;lt;code&amp;gt;cloudstack-usage&amp;lt;/code&amp;gt;. Start it and restart the management server. Set &amp;lt;code&amp;gt;enable.usage.server=true&amp;lt;/code&amp;gt; in global settings.&lt;br /&gt;
&lt;br /&gt;
The usage data will be stored in the usage database on your management server. Metrics are gathered daily and can be viewed through Cloud Monkey. There is no option to view this data in the management console.&lt;br /&gt;
&lt;br /&gt;
The collected data is coarse in nature, but it should be sufficient enough for you to determine an account or VM&#039;s resource utilization over a time period of a day or more and should be good enough to implement a rough billing / showback amount.&lt;br /&gt;
&lt;br /&gt;
===Adding some Linux templates===&lt;br /&gt;
You can add the &amp;quot;Generic Cloud&amp;quot; qcow2 disk images as a system template to CloudStack. &lt;br /&gt;
&lt;br /&gt;
Because these cloud images uses cloud-init, you will need to provide some custom userdata when deploying these images. Userdata will only work when the VM is deployed on a network that offers the &amp;quot;User Data&amp;quot; service offering. If you can&#039;t use userdata or if you want the VMs to come up with a specific root password, you can use [[virt-customize]] to set the root password on the qcow2 file.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Distro&lt;br /&gt;
!Type&lt;br /&gt;
!URL&lt;br /&gt;
|-&lt;br /&gt;
|Rocky Linux 8.4&lt;br /&gt;
|CentOS 8&lt;br /&gt;
|https://download.rockylinux.org/pub/rocky/8.4/images/Rocky-8-GenericCloud-8.4-20210620.0.x86_64.qcow2&lt;br /&gt;
|-&lt;br /&gt;
|CentOS 8.4&lt;br /&gt;
|CentOS 8&lt;br /&gt;
|https://cloud.centos.org/centos/8/x86_64/images/CentOS-8-GenericCloud-8.4.2105-20210603.0.x86_64.qcow2&lt;br /&gt;
|-&lt;br /&gt;
|Fedora 34&lt;br /&gt;
|Fedora Linux (64 bit)&lt;br /&gt;
|https://download.fedoraproject.org/pub/fedora/linux/releases/34/Cloud/x86_64/images/Fedora-Cloud-Base-34-1.2.x86_64.qcow2&lt;br /&gt;
|-&lt;br /&gt;
|Ubuntu Server 21.04&lt;br /&gt;
|&lt;br /&gt;
|http://cloud-images.ubuntu.com/hirsute/current/hirsute-server-cloudimg-amd64.img&lt;br /&gt;
You need to convert img to qcow with qemu-img:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;qemu-img create -F qcow2 -b cloudimg-amd64.img -f qcow2 cloudimg-adm64.qcow2 10G&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
Here&#039;s an example of a cloud-init configuration which you would put in the userdata field when deploying a VM:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = #cloud-config&lt;br /&gt;
hostname: vm01&lt;br /&gt;
manage_etc_hosts: true&lt;br /&gt;
users:&lt;br /&gt;
  - name: vmadm&lt;br /&gt;
    sudo: ALL=(ALL) NOPASSWD:ALL&lt;br /&gt;
    groups: users, admin&lt;br /&gt;
    home: /home/vmadm&lt;br /&gt;
    shell: /bin/bash&lt;br /&gt;
    lock_passwd: false&lt;br /&gt;
ssh_pwauth: true&lt;br /&gt;
disable_root: false&lt;br /&gt;
chpasswd:&lt;br /&gt;
  list: {{!}}&lt;br /&gt;
    vmadm:vmadm&lt;br /&gt;
  expire: false&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Importing a VMware Virtual Machine ===&lt;br /&gt;
To import a VMware virtual machine:&lt;br /&gt;
&lt;br /&gt;
# Copy the virtual machine&#039;s .vmdk disk file to a CloudStack node&lt;br /&gt;
# Convert the .vmdk into a .qcow format using the qemu-img convert command. Eg. &amp;lt;code&amp;gt;qemu-img convert -f linux.vmdk -O linux.qcow2&amp;lt;/code&amp;gt;&lt;br /&gt;
# Run the file command on the qcow disk and make a note of its size (in bytes).&lt;br /&gt;
# Create a new virtual machine in CloudStack. Use an ISO and not a template. Set the size of the VM&#039;s ROOT disk to match the disk size noted from the previous step.&lt;br /&gt;
# Start and stop the VM to ensure the virtual disk is created. Make a note of the virtual disk&#039;s ID.&lt;br /&gt;
# Copy the converted qcow disk over the existing virtual disk image in the primary storage.&lt;br /&gt;
# Restart the VM in CloudStack.&lt;br /&gt;
&lt;br /&gt;
Some things to note with this process:&lt;br /&gt;
&lt;br /&gt;
* The disk subsystem might differ between KVM and VMware. As a result, you may need to [[Rebuilding the initial ramdisk|rebuild the initrd file]] so that it has the necessary drivers to boot properly.&lt;br /&gt;
&lt;br /&gt;
=== Increasing the management console&#039;s timeout ===&lt;br /&gt;
The default timeout is 30 minutes. You may adjust the number of minutes in the &amp;lt;code&amp;gt;session.timeout&amp;lt;/code&amp;gt; value stored in &amp;lt;code&amp;gt;/etc/cloudstack/management/server.properties&amp;lt;/code&amp;gt;.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = session.timeout=60&lt;br /&gt;
| lang = xml&lt;br /&gt;
}}&lt;br /&gt;
Restart the cloudstack-management service to apply.&lt;br /&gt;
&lt;br /&gt;
=== Upgrade CloudStack ===&lt;br /&gt;
Before upgrading CloudStack, review the upgrade instructions from CloudStack&#039;s documentation. For 4.17 to 4.18, see:  https://docs.cloudstack.apache.org/en/4.18.0.0/upgrading/upgrade/upgrade-4.17.html&lt;br /&gt;
&lt;br /&gt;
In a nutshell, upgrading CloudStack for KVM hosts requires the following steps:&lt;br /&gt;
&lt;br /&gt;
# Before upgrading, load the next systemvm template image. System templates are available from: http://download.cloudstack.org/systemvm/. The systemvm template for KVM should be named something like: &amp;lt;code&amp;gt;systemvm-kvm-4.18.0&amp;lt;/code&amp;gt;. When adding the template, specify qcow2 as its format.&lt;br /&gt;
# Backup your CloudStack and usage database. {{Highlight&lt;br /&gt;
| code = $ mysqldump -u root -p -R cloud &amp;gt; cloud-backup_$(date +%Y-%m-%d-%H%M%S)&lt;br /&gt;
$ mysqldump -u root -p cloud_usage &amp;gt; cloud_usage-backup_$(date +%Y-%m-%d-%H%M%S)&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# If you have outstanding system packages to upgrade, do so now (excluding CloudStack packages) and reboot.&lt;br /&gt;
# Stop the CloudStack Management server. Manually unmount any CloudStack mounts. I had to do when I upgraded from 4.16 to 4.17 since it prevented CloudStack from starting. Upgrade the cloudstack-management and cloudstack-common packages. Restart CloudStack. {{Highlight&lt;br /&gt;
| code = # systemctl stop cloudstack-management&lt;br /&gt;
# umount /var/cloudstack/mnt/*&lt;br /&gt;
# yum -y update cloudstack\*&lt;br /&gt;
# systemctl start cloudstack-management&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Ensure things are running. Watch the logs in &amp;lt;code&amp;gt;tail -f /var/log/cloud/management/*log&amp;lt;/code&amp;gt;. Verify that the management server can still communicate with the hosts.&lt;br /&gt;
# For each CloudStack host, drain it of hosts, stop the cloudstack-agent service, do a full upgrade, reboot. {{Highlight&lt;br /&gt;
| code = # systemctl stop cloudstack-agent&lt;br /&gt;
# yum -y update cloudstack-agent&lt;br /&gt;
&lt;br /&gt;
## Restart the service or reboot just to make sure the host can come up by itself&lt;br /&gt;
## reboot&lt;br /&gt;
# systemctl restart cloudstack-agent&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Updating the system VMs ====&lt;br /&gt;
Check your list of virtual routers under Infrastructure -&amp;gt; Virtual routers. Update any VMs that are marked with &#039;requires upgrade&#039;. Do so by selecting the VM and clicking on the &#039;upgrade router to use newer template&#039; button.&lt;br /&gt;
&lt;br /&gt;
If the virtual router doesn&#039;t start up properly after performing an upgrade, make sure that the VM is running on a node with an appropriate CloudStack agent version. Virtual routers that land on a node with an older version of the agent won&#039;t start properly.&lt;br /&gt;
&lt;br /&gt;
==== Other upgrade notes ====&lt;br /&gt;
Things to watch out for:&lt;br /&gt;
&lt;br /&gt;
* Don&#039;t upgrade CloudStack packages on a server until you stop the CloudStack services. For some reason, I&#039;ve had issues in the past where something with Java gets corrupted if you try to do an upgrade while the java processes are still running. This then results in some odd class loader issue which results in the service being unable to start after the upgrade.&lt;br /&gt;
* System VM template: I upgraded the CloudStack management server to 4.16.1 using a custom compiled RPM package. However, the management server didn&#039;t start and inspecting the logs show that it was expecting a system VM template at &amp;lt;code&amp;gt;/usr/share/cloudstack-management/templates/systemvm/systemvmtemplate-4.16.1-kvm.qcow2.bz2&amp;lt;/code&amp;gt;. This is easily fixed by downloading the template and restarting the management server.  &amp;lt;code&amp;gt;wget &amp;lt;nowiki&amp;gt;http://download.cloudstack.org/systemvm/4.16/systemvmtemplate-4.16.1-kvm.qcow2.bz2&amp;lt;/nowiki&amp;gt; -O /usr/share/cloudstack-management/templates/systemvm/systemvmtemplate-4.16.1-kvm.qcow2.bz2&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Traefik ===&lt;br /&gt;
&lt;br /&gt;
==== Using Traefik for SSL termination ====&lt;br /&gt;
With the console proxy served using SSL, we could put a reverse proxy in front of both the management UI and the console proxy service VMs with a valid certificate. This allows us to &#039;mask&#039; the self-signed certificate with Traefik&#039;s ability to request for a proper certificate from Let&#039;s Encrypt.&lt;br /&gt;
&lt;br /&gt;
In my test version of CloudStack, I&#039;ve set up Traefik with the following configs. I updated the console proxy to use a dynamic URL by setting &amp;lt;code&amp;gt;consoleproxy.url.domain&amp;lt;/code&amp;gt; to something like &amp;lt;code&amp;gt;*.cloudstack-test.example.com&amp;lt;/code&amp;gt;. CloudStack&#039;s console proxy service will translate the &amp;lt;code&amp;gt;*&amp;lt;/code&amp;gt; by the system VM&#039;s IP address (Eg. 10.1.1.1 becomes 10-1-1-1). We&#039;ll tell Traefik to reverse proxy these domains for both HTTPS and WSS on ports 443 and 8080 respectively. My dynamic traefik configs to make this happen looks like the following:{{Highlight&lt;br /&gt;
| code = http:&lt;br /&gt;
  serversTransports:&lt;br /&gt;
    ignorecert:&lt;br /&gt;
      insecureSkipVerify: true&lt;br /&gt;
&lt;br /&gt;
  routers:&lt;br /&gt;
    cloudstack:&lt;br /&gt;
      rule: Host(`cloudstack-test.example.com`)&lt;br /&gt;
      service: cloudstack-poc&lt;br /&gt;
      entrypoints:&lt;br /&gt;
        - http&lt;br /&gt;
      middlewares:&lt;br /&gt;
        - https-redirect&lt;br /&gt;
&lt;br /&gt;
    cloudstack-https:&lt;br /&gt;
      rule: Host(`cloudstack-test.example.com`)&lt;br /&gt;
      service: cloudstack-poc&lt;br /&gt;
      entrypoints:&lt;br /&gt;
        - https&lt;br /&gt;
      tls:&lt;br /&gt;
        certresolver: letsencrypt&lt;br /&gt;
&lt;br /&gt;
    cloudstack-pub-ip-136-159-1-100:&lt;br /&gt;
      rule: Host(`136-159-1-1.cloudstack-test.example.com`)&lt;br /&gt;
      service: 136-159-1-100&lt;br /&gt;
      entrypoints:&lt;br /&gt;
        - https&lt;br /&gt;
      tls:&lt;br /&gt;
        certresolver: letsencrypt&lt;br /&gt;
&lt;br /&gt;
    cloudstack-pub-ip-136-159-1-100-ws:&lt;br /&gt;
      rule: Host(`136-159-1-1.cloudstack-test.example.com`)&lt;br /&gt;
      service: 136-159-1-100-ws&lt;br /&gt;
      entrypoints:&lt;br /&gt;
        - httpws&lt;br /&gt;
      tls:&lt;br /&gt;
        certresolver: letsencrypt&lt;br /&gt;
		&lt;br /&gt;
  services:&lt;br /&gt;
    cloudstack-poc:&lt;br /&gt;
      loadBalancer:&lt;br /&gt;
        servers:&lt;br /&gt;
          - url: &amp;quot;http://172.19.12.141:8080&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    136-159-1-100:&lt;br /&gt;
      loadBalancer:&lt;br /&gt;
        servers:&lt;br /&gt;
          - url: &amp;quot;https://136.159.1.100&amp;quot;&lt;br /&gt;
        serversTransport: ignorecert&lt;br /&gt;
&lt;br /&gt;
    136-159-1-100-ws:&lt;br /&gt;
      loadBalancer:&lt;br /&gt;
        servers:&lt;br /&gt;
          - url: &amp;quot;https://136.159.1.100:8080&amp;quot;&lt;br /&gt;
        serversTransport: ignorecert&lt;br /&gt;
&lt;br /&gt;
  middlewares:&lt;br /&gt;
    https-redirect:&lt;br /&gt;
      redirectscheme:&lt;br /&gt;
        scheme: https&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}And the following traefik configs:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = entryPoints:&lt;br /&gt;
  http:&lt;br /&gt;
    address: &amp;quot;:80&amp;quot;&lt;br /&gt;
  https:&lt;br /&gt;
    address: &amp;quot;:443&amp;quot;&lt;br /&gt;
  httpws:&lt;br /&gt;
    address: &amp;quot;:8080&amp;quot;&lt;br /&gt;
&lt;br /&gt;
certificatesResolvers:&lt;br /&gt;
  letsencrypt:&lt;br /&gt;
    acme:&lt;br /&gt;
      email: user@example.com&lt;br /&gt;
      storage: &amp;quot;/config/acme.json&amp;quot;&lt;br /&gt;
      httpChallenge:&lt;br /&gt;
        entryPoint: http&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Change guest VM CPU flags ===&lt;br /&gt;
The default CPU flags that guest VMs sees are set to qemu64 compatible features. The qemu64 feature set covers a very small subset of the available features that modern CPUs have which makes the guest VM be compatible to nearly all available CPUs at the cost of reduced features. The feature flags in qemu64 are: &amp;lt;code&amp;gt;fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pse36 clflush mmx fxsr sse sse2 ht syscall nx lm rep_good nopl xtopology cpuid tsc_known_freq pni cx16 x2apic hypervisor lahf_lm cpuid_fault pti&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For virtualized workloads that require additional feature sets, you can edit the CloudStack agent to use a different guest CPU mode. Select one of:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;custom&#039;&#039;&#039;: This is the default and it defaults to the x86_qemu64 feature set defined in &amp;lt;code&amp;gt;/usr/share/libvirt/cpu_map/x86_qemu64.xml&amp;lt;/code&amp;gt;. You may select a different CPU map by specifying &amp;lt;code&amp;gt;guest.cpu.model&amp;lt;/code&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;host-model&#039;&#039;&#039;: Uses a CPU model compatible with your host. Most feature flags are available. Guest CPUs will identify itself as a generic CPU of that family such as &amp;lt;code&amp;gt;Intel Xeon Processor (Icelake)&amp;lt;/code&amp;gt; (note the lack of &#039;(R)&#039; after Intel and Xeon brands and no specific CPU model number).&lt;br /&gt;
* &#039;&#039;&#039;host-passthrough&#039;&#039;&#039;: Use CPU passthrough; feature flags match exactly. Migrations only work with matching CPUs and may still fail when using this mode. Guest CPUs will identify itself as the underlying CPU in that hypervisor (such as &amp;lt;code&amp;gt;Intel(R) Xeon(R) Gold 5320 CPU @ 2.20GHz&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
For CloudStack clusters with identical CPUs, it&#039;s recommended to use host-model. I&#039;ve tried using host-passthrough on matching hosts using a Intel Xeon Silver 4316 and migrations sometimes fail and the VM requires a reset to be brought back up.&lt;br /&gt;
&lt;br /&gt;
For more information, see: http://docs.cloudstack.apache.org/en/4.15.0.0/installguide/hypervisor/kvm.html#install-and-configure-the-agent&lt;br /&gt;
&lt;br /&gt;
To change the CPU mode, you simply need to add the appropriate line into the agent properties file and restart the agent:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ## Matching host model&lt;br /&gt;
# echo &amp;quot;guest.cpu.mode=host-model&amp;quot; &amp;gt;&amp;gt; /etc/cloudstack/agent/agent.properties&lt;br /&gt;
# systemctl restart cloudstack-agent.service&lt;br /&gt;
&lt;br /&gt;
## Passthrough&lt;br /&gt;
# echo &amp;quot;guest.cpu.mode=host-passthrough&amp;quot; &amp;gt;&amp;gt; /etc/cloudstack/agent/agent.properties&lt;br /&gt;
# systemctl restart cloudstack-agent.service&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Using Open vSwitch and DPDK ===&lt;br /&gt;
Getting DPDK working with Open vSwitch is relatively straight forward. You need to install the DPDK packages, configure the kernel to use hugepages and IO passthrough, enable the vfio driver on your network interfaces for DPDK support, reconfigure Open vSwitch to use the DPDK device, and enable DPDK on the CloudStack agent.&lt;br /&gt;
&lt;br /&gt;
There are some existing resources that might help.&lt;br /&gt;
&lt;br /&gt;
*https://www.shapeblue.com/openvswitch-with-dpdk-support-on-cloudstack/&lt;br /&gt;
* https://access.redhat.com/documentation/en-us/red_hat_openstack_platform/10/html/ovs-dpdk_end_to_end_troubleshooting_guide/configure_and_test_lacp_bonding_with_open_vswitch_dpdk&lt;br /&gt;
&lt;br /&gt;
Install DPDK tools:&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # yum -y install dpdk dpdk-tools&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Reconfigure your kernel by editing &amp;lt;code&amp;gt;/etc/default/grub&amp;lt;/code&amp;gt;. Add the following. Adjust the &amp;lt;code&amp;gt;isolcpus&amp;lt;/code&amp;gt; depending on your CPUs available. I assigned 4 cores out of 80 vCPUs. I am also using 16 1GB huge pages. Adjust this according to how much memory your system has (and probably what performance you&#039;re seeing){{Highlight&lt;br /&gt;
| code = # vi /etc/default/grub&lt;br /&gt;
## default_hugepagesz=1GB hugepagesz=1G hugepages=16 iommu=pt intel_iommu=on isolcpus=1-19,21-39,41-59,61-79 intel_pstate=disable nosoftlockup&lt;br /&gt;
&lt;br /&gt;
# grub2-mkconfig -o /boot/grub2/grub.cfg&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}You can also configure huge pages by sysctl (optional if you set it in the kernel cmdline){{Highlight&lt;br /&gt;
| code = # echo &#039;vm.nr_hugepages=16&#039; &amp;gt; /etc/sysctl.d/hugepages.conf&lt;br /&gt;
# sysctl -w vm.nr_hugepages=16&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}Load the &amp;lt;code&amp;gt;vfio-pci&amp;lt;/code&amp;gt; kernel module on boot&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # echo vfio-pci &amp;gt; /etc/modules-load.d/vfio-pci.conf&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Reboot the machine. When it comes back, verify that you have hugepages and vfio-pci loaded, and that IOMMU is working.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # cat /proc/cmdline {{!}} grep iommu=pt&lt;br /&gt;
# cat /proc/cmdline {{!}} grep intel_iommu=on&lt;br /&gt;
# dmesg {{!}} grep -e DMAR -e IOMMU&lt;br /&gt;
# grep HugePages_ /proc/meminfo&lt;br /&gt;
# lsmod {{!}} grep vfio-pci&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Set the network interfaces you wish to use DPDK on to the vfio-pci driver. This is done using the &amp;lt;code&amp;gt;dpdk-devbind.py&amp;lt;/code&amp;gt; script that&#039;s provided by the DPDK tools package.{{Highlight&lt;br /&gt;
| code = # modprobe vfio-pci&lt;br /&gt;
# dpdk-devbind.py --bind=vfio-pci ens2f0&lt;br /&gt;
# dpdk-devbind.py --bind=vfio-pci ens2f1&lt;br /&gt;
## Verify&lt;br /&gt;
# dpdk-devbind.py --status&lt;br /&gt;
&lt;br /&gt;
Network devices using DPDK-compatible driver&lt;br /&gt;
============================================&lt;br /&gt;
0000:31:00.0 &#039;Ethernet Controller X710 for 10GBASE-T 15ff&#039; drv=vfio-pci unused=i40e&lt;br /&gt;
0000:31:00.1 &#039;Ethernet Controller X710 for 10GBASE-T 15ff&#039; drv=vfio-pci unused=i40e&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}Enable DPDK on Open vSwitch. pmd-cpu-mask defines what cores are used for data path packet processing. The dpdk-lcore-mask defines cores that non-datapath OVS-DPDK threads such as handler and revalidator threads run. These two masks should not overlap. For more information on these parameters, see: https://developers.redhat.com/blog/2017/06/28/ovs-dpdk-parameters-dealing-with-multi-numa&amp;lt;nowiki/&amp;gt;.{{Highlight&lt;br /&gt;
| code = &lt;br /&gt;
# ovs-vsctl --no-wait set Open_vSwitch . other_config:dpdk-init=true&lt;br /&gt;
# ovs-vsctl --no-wait set Open_vSwitch . other_config:dpdk-lcore-mask=0x00000001  &lt;br /&gt;
# ovs-vsctl --no-wait set Open_vSwitch . other_config:pmd-cpu-mask=0x17c0017c                   &lt;br /&gt;
# ovs-vsctl --no-wait set Open_vSwitch . other_config:dpdk-socket-mem=&amp;quot;1024&amp;quot;&lt;br /&gt;
&lt;br /&gt;
## Verify&lt;br /&gt;
# ovs-vsctl get Open_vSwitch . dpdk_initialized&lt;br /&gt;
# ovs-vsctl get Open_vSwitch . dpdk_version&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}If Open vSwitch is already configured to use these interfaces by name, you will just need to change the interface type to dpdk and set its PCI address.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ovs-vsctl set interface ens2f0 type=dpdk&lt;br /&gt;
# ovs-vsctl set interface ens2f0 options:dpdk-devargs=0000:31:00.0&lt;br /&gt;
# ovs-vsctl set interface ens2f1 type=dpdk&lt;br /&gt;
# ovs-vsctl set interface ens2f1 options:dpdk-devargs=0000:31:00.1&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
The bridge that these interfaces are connected to must also have its datapath_type updated:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ovs-vsctl set bridge nic0 datapath_type=netdev&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Restart Open vSwitch for these to apply properly and confirm that it&#039;s working&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # systemctl restart openvswitch&lt;br /&gt;
# ovs-vsctl show&lt;br /&gt;
...&lt;br /&gt;
        Port bond0&lt;br /&gt;
            Interface ens2f1&lt;br /&gt;
                type: dpdk&lt;br /&gt;
                options: {dpdk-devargs=&amp;quot;0000:31:00.1&amp;quot;}&lt;br /&gt;
            Interface ens2f0&lt;br /&gt;
                type: dpdk&lt;br /&gt;
                options: {dpdk-devargs=&amp;quot;0000:31:00.0&amp;quot;}&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Update the CloudStack agent so that this host has the DPDK capability. Edit &amp;lt;code&amp;gt;/etc/cloudstack/agent/agent.properties&amp;lt;/code&amp;gt;. Note that the keyword is &amp;lt;code&amp;gt;openvswitch.dpdk.enabled&amp;lt;/code&amp;gt; (enabled ending with -ed). The example from ShapeBlue&#039;s blog post is wrong.{{Highlight&lt;br /&gt;
| code = network.bridge.type=openvswitch&lt;br /&gt;
libvirt.vif.driver=com.cloud.hypervisor.kvm.resource.OvsVifDriver&lt;br /&gt;
openvswitch.dpdk.enabled=true&lt;br /&gt;
openvswitch.dpdk.ovs.path=/var/run/openvswitch/&lt;br /&gt;
| lang = text&lt;br /&gt;
}}Restart the CloudStack agent for this capability to be visible by the management server. You should be able to call &amp;lt;code&amp;gt;list hosts filter=capabilities,name&amp;lt;/code&amp;gt; and have the host list dpdk as a capability. Eg:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = (localcloud) 🐱 &amp;gt; list hosts filter=capabilities,name&lt;br /&gt;
count = 22&lt;br /&gt;
host:&lt;br /&gt;
+-------------------+----------+&lt;br /&gt;
{{!}}   CAPABILITIES    {{!}}   NAME   {{!}}&lt;br /&gt;
+-------------------+----------+&lt;br /&gt;
{{!}} hvm,snapshot,dpdk {{!}} cs9      {{!}}&lt;br /&gt;
{{!}} hvm,snapshot,dpdk {{!}} cs10     {{!}}&lt;br /&gt;
{{!}} hvm,snapshot,dpdk {{!}} cs11     {{!}}&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
If you don&#039;t see this, double check your agent configs and restart it again.&lt;br /&gt;
&lt;br /&gt;
For VMs to take advantage of DPDK, you must either set extraconfig on the virtual machine or create a new compute service offering. Extraconfig might get overwritten whenever the VM is updated, so it&#039;s not a reliable solution. Extraconfig is a URL encoded config and you cannot use single quotes in it or else you will break the VM deployment. Eg:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = (localcloud) 🐱 &amp;gt; update virtualmachine extraconfig=dpdk-hugepages:%0A%3CmemoryBacking%3E%0A%20%20%20%3Chugepages%3E%0A%20%20%20%20%3C/hugepages%3E%0A%3C/memoryBacking%3E%0A%0Adpdk-numa:%0A%3Ccpu%20mode=%22host-passthrough%22%3E%0A%20%20%20%3Cnuma%3E%0A%20%20%20%20%20%20%20%3Ccell%20id=%220%22%20cpus=%220%22%20memory=%229437184%22%20unit=%22KiB%22%20memAccess=%22shared%22/%3E%0A%20%20%20%3C/numa%3E%0A%3C/cpu%3E%0A%0Adpdk-interface-queue:%0A%3Cdriver%20name=%22vhost%22%20queues=%22128%22/%3E id=af64cc80-a4e4-4c17-9c7d-c34ed234dc6a&lt;br /&gt;
virtualmachine = map[account:RCS affinitygroup:[] cpunumber:2 cpuspeed:1000 cpuused:5.88% created:2022-05-03T13:16:02-0600 details:map[Message.ReservedCapacityFreed.Flag:false dpdk-hugepages:a extraconfig-dpdk-hugepages:&amp;lt;memoryBacking&amp;gt;&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Troubleshooting ====&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = 2022-05-05T22:35:28.312Z{{!}}281704{{!}}netdev_dpdk{{!}}INFO{{!}}vHost Device &#039;/var/run/openvswitch/csdpdk-1&#039; connection has been destroyed&lt;br /&gt;
2022-05-05T22:35:28.312Z{{!}}281705{{!}}netdev_dpdk{{!}}INFO{{!}}vHost Device &#039;/var/run/openvswitch/csdpdk-1&#039; connection has been destroyed&lt;br /&gt;
2022-05-05T22:35:28.313Z{{!}}281706{{!}}netdev_dpdk{{!}}INFO{{!}}vHost Device &#039;/var/run/openvswitch/csdpdk-1&#039; connection has been destroyed&lt;br /&gt;
2022-05-05T22:35:28.313Z{{!}}281707{{!}}netdev_dpdk{{!}}INFO{{!}}vHost Device &#039;/var/run/openvswitch/csdpdk-1&#039; connection has been destroyed&lt;br /&gt;
2022-05-05T22:35:28.313Z{{!}}281708{{!}}netdev_dpdk{{!}}INFO{{!}}vHost Device &#039;/var/run/openvswitch/csdpdk-1&#039; connection has been destroyed&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
Check the agent logs for issues from qemu. I had defined an invalid property which prevented the VM from starting.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = [root@cs10 agent]# grep qemu agent.log&lt;br /&gt;
org.libvirt.LibvirtException: internal error: process exited while connecting to monitor: 2022-05-05T22:35:52.060450Z qemu-kvm: -netdev vhost-user,chardev=charnet0,queues=256,id=hostnet0: you are asking more queues than supported: 128&lt;br /&gt;
2022-05-05T22:35:52.060633Z qemu-kvm: -netdev vhost-user,chardev=charnet0,queues=256,id=hostnet0: you are asking more queues than supported: 128&lt;br /&gt;
2022-05-05T22:35:52.060817Z qemu-kvm: -netdev vhost-user,chardev=charnet0,queues=256,id=hostnet0: you are asking more queues than supported: 128&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==Tools==&lt;br /&gt;
&lt;br /&gt;
===CloudMonkey===&lt;br /&gt;
Get started:&lt;br /&gt;
&lt;br /&gt;
*Download from: https://github.com/apache/cloudstack-cloudmonkey/releases/tag/6.1.0&lt;br /&gt;
*Documentation at: https://cwiki.apache.org/confluence/display/CLOUDSTACK/CloudStack+cloudmonkey+CLI&lt;br /&gt;
&lt;br /&gt;
When you first run CloudMonkey, you will need to set the CloudStack instance URL and credentials and then run sync.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = $ cmk&lt;br /&gt;
&amp;gt; set url http://172.19.12.141:8080/client/api&lt;br /&gt;
&amp;gt; set username admin&lt;br /&gt;
&amp;gt; set password password&lt;br /&gt;
&amp;gt; sync&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
The settings are then saved to &amp;lt;code&amp;gt;~/.cmk/config&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The sync command fetches all the available API calls that your account can use. Once that is done, you can then use tab completion while in the CloudMonkey CLI.&lt;br /&gt;
&lt;br /&gt;
====Cheat sheet====&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!What&lt;br /&gt;
!Command&lt;br /&gt;
|-&lt;br /&gt;
|Change output format&lt;br /&gt;
|&amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;set display table|json&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|Create compute offering&lt;br /&gt;
|&amp;lt;code&amp;gt;create serviceoffering name=rcs.c2 displaytext=Medium cpunumber=2 cpuspeed=750 memory=2048 storagetype=shared provisioningtype=thin offerha=false limitcpuuse=false isvolatile=false issystem=false deploymentplanner=UserDispersingPlanner cachemode=none customized=false&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|Add a new host&lt;br /&gt;
|&amp;lt;code&amp;gt;add host clusterid=XX podid=XX zoneid=XX hypervisor=KVM password=**** username=root url=&amp;lt;nowiki&amp;gt;http://bm01&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
====Automate zone deployments====&lt;br /&gt;
There is an example script on how to automate a basic zone deployment at: https://github.com/apache/cloudstack-cloudmonkey/wiki/Usage&lt;br /&gt;
&lt;br /&gt;
=== Terraform ===&lt;br /&gt;
The Terraform CloudStack provide works for the most part. However, for CloudStack 4.16, you&#039;ll need to recompile it from scratch because the distributed binaries don&#039;t work properly (resulting in deployments hanging indefinitely). To build the Terraform provider, I will use Docker:&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # git clone https://github.com/apache/cloudstack-terraform-provider.git&lt;br /&gt;
# cd cloudstack-terraform-provide&lt;br /&gt;
# git clone https://github.com/tetra12/cloudstack-go.git&lt;br /&gt;
# cat &amp;lt;&amp;lt;EOF &amp;gt;&amp;gt; go.mod&lt;br /&gt;
replace github.com/apache/cloudstack-go/v2 =&amp;gt; ./cloudstack-go&lt;br /&gt;
exclude github.com/apache/cloudstack-go/v2 v2.11.0&lt;br /&gt;
EOF&lt;br /&gt;
# docker run --rm -ti -v /home/me/cloudstack-terraform-provider/:/build golang bash &lt;br /&gt;
&amp;gt; cd /build&lt;br /&gt;
&amp;gt; go build&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Copy the resulting binary to your terraform plugins path. Because I ran terraform init, it placed it in my terraform directory under &amp;lt;code&amp;gt;.terraform/providers/registry.terraform.io/cloudstack/cloudstack/0.4.0/linux_amd64/terraform-provider-cloudstack_v0.4.0&amp;lt;/code&amp;gt;. Edit the metadata file in the same directory as the provider executable and remove the file hash so that terraform runs the provider.&lt;br /&gt;
&lt;br /&gt;
See also: [[Terraform#CloudStack]]&lt;br /&gt;
&lt;br /&gt;
=== Packer ===&lt;br /&gt;
The Packer CloudStack provider also works for the most part, but is limited in that it cannot enter keyboard inputs. Any OS deployments will require some sort of manual inputs or require that the ISO media you use is completely automated. I also had to compile the provider manually since the default plugin that&#039;s fetched by packer doesn&#039;t quite work due to API changes.&lt;br /&gt;
&lt;br /&gt;
See also: [[Packer#CloudStack]]&lt;br /&gt;
&lt;br /&gt;
==Troubleshooting==&lt;br /&gt;
When you run into issues, check the logs in &amp;lt;code&amp;gt;/var/log/cloudstack/&amp;lt;/code&amp;gt;. There&#039;s typically a stacktrace which gets generated whenever you encounter an error.&lt;br /&gt;
&lt;br /&gt;
===Can&#039;t create shared network in a advanced zone using Open vSwitch===&lt;br /&gt;
Whenever I try creating a shared network in an advanced zone that is using OVS, the step fails with: &amp;quot;Unable to convert network offering with specified id to network profile&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Stack trace shows that the [https://github.com/apache/cloudstack/blob/bf6266188c89a5487383f216333ae10e878d2c10/plugins/network-elements/ovs/src/main/java/com/cloud/network/guru/OvsGuestNetworkGuru.java#L99 OVS guest network guru] isn&#039;t able at designing the network because the zone isn&#039;t capable of handling this network offering.&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = 2021-09-28 16:36:26,416 DEBUG [c.c.a.ApiServer] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) CIDRs from which account &#039;Acct[76a1585d-1bf6-11ec-a3c5-8f3e88f01ab1-admin]&#039; is allowed to perform API calls: 0.0.0.0/0,::/0&lt;br /&gt;
2021-09-28 16:36:26,439 DEBUG [c.c.u.AccountManagerImpl] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Access granted to Acct[76a1585d-1bf6-11ec-a3c5-8f3e88f01ab1-admin] to [Network Offering [7-Guest-DefaultSharedNetworkOffering] by AffinityGroupAccessChecker&lt;br /&gt;
2021-09-28 16:36:26,517 DEBUG [c.c.n.g.BigSwitchBcfGuestNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Refusing to design this network, the physical isolation type is not BCF_SEGMENT&lt;br /&gt;
2021-09-28 16:36:26,521 DEBUG [o.a.c.n.c.m.ContrailGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Refusing to design this network&lt;br /&gt;
2021-09-28 16:36:26,524 DEBUG [c.c.n.g.NiciraNvpGuestNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Refusing to design this network&lt;br /&gt;
2021-09-28 16:36:26,527 DEBUG [o.a.c.n.o.OpendaylightGuestNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Refusing to design this network&lt;br /&gt;
2021-09-28 16:36:26,530 DEBUG [c.c.n.g.OvsGuestNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Refusing to design this network&lt;br /&gt;
2021-09-28 16:36:26,536 DEBUG [c.c.n.g.DirectNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) GRE: VLAN&lt;br /&gt;
2021-09-28 16:36:26,536 DEBUG [c.c.n.g.DirectNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) GRE: VXLAN&lt;br /&gt;
2021-09-28 16:36:26,536 INFO  [c.c.n.g.DirectNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Refusing to design this network&lt;br /&gt;
2021-09-28 16:36:26,539 INFO  [c.c.n.g.DirectNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Refusing to design this network&lt;br /&gt;
2021-09-28 16:36:26,543 DEBUG [o.a.c.n.g.SspGuestNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) SSP not configured to be active&lt;br /&gt;
2021-09-28 16:36:26,546 DEBUG [c.c.n.g.BrocadeVcsGuestNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Refusing to design this network&lt;br /&gt;
2021-09-28 16:36:26,549 DEBUG [o.a.c.e.o.NetworkOrchestrator] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Releasing lock for Acct[76a0531f-1bf6-11ec-a3c5-8f3e88f01ab1-system]&lt;br /&gt;
2021-09-28 16:36:26,624 DEBUG [c.c.u.d.T.Transaction] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Rolling back the transaction: Time = 172 Name =  qtp1816147548-400; called by -TransactionLegacy.rollback:888-TransactionLegacy.removeUpTo:831-TransactionLegacy.close:655-Transaction.execute:38-Transaction.execute:47-NetworkOrches&lt;br /&gt;
trator.createGuestNetwork:2572-NetworkOrchestrator.createGuestNetwork:2327-NetworkServiceImpl$4.doInTransaction:1502-NetworkServiceImpl$4.doInTransaction:1450-Transaction.execute:40-NetworkServiceImpl.commitNetwork:1450-NetworkServiceImpl.createGuestNetwork:1366&lt;br /&gt;
2021-09-28 16:36:26,667 ERROR [c.c.a.ApiServer] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) unhandled exception executing api command: [Ljava.lang.String;@69a8823d&lt;br /&gt;
com.cloud.utils.exception.CloudRuntimeException: Unable to convert network offering with specified id to network profile&lt;br /&gt;
        at org.apache.cloudstack.engine.orchestration.NetworkOrchestrator.setupNetwork(NetworkOrchestrator.java:739)&lt;br /&gt;
        at org.apache.cloudstack.engine.orchestration.NetworkOrchestrator$10.doInTransaction(NetworkOrchestrator.java:2634)&lt;br /&gt;
        at org.apache.cloudstack.engine.orchestration.NetworkOrchestrator$10.doInTransaction(NetworkOrchestrator.java:2572)&lt;br /&gt;
        at com.cloud.utils.db.Transaction$2.doInTransaction(Transaction.java:50)&lt;br /&gt;
        at com.cloud.utils.db.Transaction.execute(Transaction.java:40)&lt;br /&gt;
        at com.cloud.utils.db.Transaction.execute(Transaction.java:47)&lt;br /&gt;
...&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
====Possible answer====&lt;br /&gt;
The guest network was set up with GRE isolation. This however isn&#039;t supported with KVM as the hypervisor (see [https://events.static.linuxfound.org/sites/events/files/slides/CloudStack%20Collab%20Hypervisor.pdf this presentation]). After re-creating the zone with the guest physical network set up with just VLAN isolation, I was able to create a regular shared guest network that all tenants within the zone can see and use.&lt;br /&gt;
&lt;br /&gt;
To make the shared network SNAT out, I created another shared network offering that also has SourceNat and StaticNat.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = $ cmk list serviceofferings issystem=true name=&#039;System Offering For Software Router&#039;&lt;br /&gt;
$ cmk create networkoffering \&lt;br /&gt;
name=SharedNetworkOfferingWithSourceNatService displaytext=&amp;quot;Shared Network Offering with Source NAT Service&amp;quot; traffictype=GUEST guestiptype=shared conservemode=true specifyvlan=true specifyipranges=true \&lt;br /&gt;
serviceofferingid=307b14d8-afd1-43ea-948c-ffe882cd5926 \&lt;br /&gt;
supportedservices=Dhcp,Dns,Firewall,SourceNat,StaticNat,PortForwarding \&lt;br /&gt;
serviceProviderList[0].service=Dhcp serviceProviderList[0].provider=VirtualRouter \&lt;br /&gt;
serviceProviderList[1].service=Dns serviceProviderList[1].provider=VirtualRouter \&lt;br /&gt;
serviceProviderList[2].service=Firewall serviceProviderList[2].provider=VirtualRouter \&lt;br /&gt;
serviceProviderList[3].service=SourceNat serviceProviderList[3].provider=VirtualRouter \&lt;br /&gt;
serviceProviderList[4].service=StaticNat serviceProviderList[4].provider=VirtualRouter \&lt;br /&gt;
serviceProviderList[5].service=PortForwarding serviceProviderList[5].provider=VirtualRouter \&lt;br /&gt;
servicecapabilitylist[0].service=SourceNat servicecapabilitylist[0].capabilitytype=SupportedSourceNatTypes servicecapabilitylist[0].capabilityvalue=peraccount&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Using this network offering, I was able to create a shared network in the advanced networking zone that has a NAT service which is visible to all accounts. The only issue with this approach is that there isn&#039;t a way to create a port forwarding for a specific VM because the account that owns this network is &#039;system&#039;.&lt;br /&gt;
&lt;br /&gt;
=== Libvirtd can&#039;t start due to expired certificate ===&lt;br /&gt;
For some reason, the host stopped renewing agent certs with the management server. As a result, libvirtd will not restart. I only noticed this after rebooting an affected node after migrating all the VMs off the system.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # libvirtd -l&lt;br /&gt;
2023-11-30 18:16:25.116+0000: 39448: info : libvirt version: 8.0.0, package: 22.module+el8.9.0+1405+b6048078 (infrastructure@rockylinux.org, 2023-07-31-18:01:38, )&lt;br /&gt;
2023-11-30 18:16:25.116+0000: 39448: info : hostname: cs1&lt;br /&gt;
2023-11-30 18:16:25.116+0000: 39448: error : virNetTLSContextCheckCertTimes:142 : The server certificate /etc/pki/libvirt/servercert.pem has expired&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Note that the certficate file &amp;lt;code&amp;gt;/etc/pki/libvirt/servercert.pem&amp;lt;/code&amp;gt; symlinks to &amp;lt;code&amp;gt;/etc/cloudstack/agent/cloud.crt&amp;lt;/code&amp;gt;. The certificates and private keys under &amp;lt;code&amp;gt;/etc/cloudstack/agent/cloud*&amp;lt;/code&amp;gt; are generated by the CloudStack management server and then sent to and saved by the agent&amp;lt;ref&amp;gt;Certificate saved by the agent: https://github.com/apache/cloudstack/blob/cb62ce67671699fa01564b3b4b0d3d83eb3d5acb/agent/src/main/java/com/cloud/agent/Agent.java#L671&amp;lt;/ref&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Since the node is already out of service, the easiest fix here is to [[CloudStack#Re-add existing KVM bare metal host|re-add this KVM bare metal host]] back into CloudStack again.&lt;br /&gt;
&lt;br /&gt;
==Open-ended questions==&lt;br /&gt;
===Compute offerings with &#039;unlimited&#039; CPU cycles?===&lt;br /&gt;
Compute offerings require a MHz value assigned. Why is this? Can we just assign a VM entire cores?&lt;br /&gt;
&lt;br /&gt;
- If you read the docs, CPU (in MHz) only has an effect if CPU cap is selected. In all other cases, the value here is something akin to &#039;cpu shares&#039;. &lt;br /&gt;
&lt;br /&gt;
- if you put in a huge number like 9999, deployment would fail though.&lt;br /&gt;
&lt;br /&gt;
===How to implement showback?===&lt;br /&gt;
Is there a way to implement showback based on resources consumed by account?&lt;br /&gt;
&lt;br /&gt;
===Monitoring resources?===&lt;br /&gt;
Is there a way to monitor resource usage by account, node? Any good way to push VMs into a CMDB like ServiceNow?&lt;br /&gt;
&lt;br /&gt;
===NetApp integration?===&lt;br /&gt;
Is it possible to do guest VM snapshots by leveraging NetApp?&lt;br /&gt;
&lt;br /&gt;
===Backups?===&lt;br /&gt;
The only backup plugins that are available are &#039;dummy&#039; which does nothing and &#039;veeam&#039; which only supports VMware + Veeam.  If you&#039;re using KVM, there doesn&#039;t seem to be any way to easily backup/restore VMs.&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br /&amp;gt;&lt;br /&gt;
{{Navbox Linux}}&lt;br /&gt;
[[Category:Linux]]&lt;br /&gt;
[[Category:LinuxUtilities]]&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Traefik&amp;diff=7691</id>
		<title>Traefik</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Traefik&amp;diff=7691"/>
		<updated>2025-07-02T16:44:28Z</updated>

		<summary type="html">&lt;p&gt;Leo: /* Certificates with intermediate signing certificates */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Traefik is an open source HTTP reverse proxy and load balancer written in golang. It can be integrated into various infrastructures such as Docker and Kubernetes or used as a standalone service. Traefik comes with lots of automation features such as dynamic configuration through container labels and automatic SSL certificate renewals using the ACME protocol.&lt;br /&gt;
&lt;br /&gt;
==Docker integration==&lt;br /&gt;
Traefik can be used as the reverse proxy for all web-related services on a Docker host. The added benefit with this approach is that new web services that are added can be automatically configured through docker labels and SSL certificates are automatically obtained provided that the DNS names already resolve. &lt;br /&gt;
&lt;br /&gt;
Extra care is required when using Traefik and Docker since Traefik needs to have access to the Docker socket in order to see container events for its automatic configuration. Since having access to the Docker socket is pretty much equivalent to owning the Docker host, from a security stand-point, it&#039;s best to have an intermediate Docker container proxy the Docker socket so that it can only be read from but not written to. More on this set up below. &lt;br /&gt;
&lt;br /&gt;
===Setting up Traefik with Docker Compose===&lt;br /&gt;
Create a new &#039;traefik&#039; Docker network. This network will be used by any other containers on the host that needs to be reverse proxied by Treafik.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # docker network create traefik&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Next, we will set up the Traefik docker-compose stack. Traefik should be configured to work with Docker. We&#039;ll also restrict Traefik from exposing containers unless they are explicitly labeled.  Review the [https://doc.traefik.io/traefik/providers/docker/ Traefik Docker documentation] for all the available Docker related options.&lt;br /&gt;
&lt;br /&gt;
Included here is the socket-proxy which we&#039;ll use to restrict what Traefik can do to Docker in case it gets compromised.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = version: &#039;3.3&#039;&lt;br /&gt;
&lt;br /&gt;
services:&lt;br /&gt;
  traefik:&lt;br /&gt;
    image: traefik:latest&lt;br /&gt;
    restart: always&lt;br /&gt;
    command: --web --docker --docker.watch --docker.exposedbydefault=false --providers.docker.endpoint=tcp://socket-proxy:2375&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ./config/traefik.yml:/traefik.yml&lt;br /&gt;
      - ./config:/config&lt;br /&gt;
      - /var/log/traefik:/var/log/traefik&lt;br /&gt;
    networks:&lt;br /&gt;
      - traefik&lt;br /&gt;
      - internal&lt;br /&gt;
    environment:&lt;br /&gt;
      - &amp;quot;TZ=MST7MDT,M3.2.0,M11.1.0&amp;quot;&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;80:80&amp;quot;&lt;br /&gt;
      - &amp;quot;443:443&amp;quot;&lt;br /&gt;
&lt;br /&gt;
  # https://github.com/Tecnativa/docker-socket-proxy&lt;br /&gt;
  socket-proxy:&lt;br /&gt;
    build:&lt;br /&gt;
      context: ./socket-proxy&lt;br /&gt;
      dockerfile: Dockerfile&lt;br /&gt;
    restart: always&lt;br /&gt;
    volumes:&lt;br /&gt;
      - /var/run/docker.sock:/var/run/docker.sock&lt;br /&gt;
    environment:&lt;br /&gt;
      CONTAINERS: 1&lt;br /&gt;
    networks:&lt;br /&gt;
      - internal&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;2375&amp;quot;&lt;br /&gt;
&lt;br /&gt;
networks:&lt;br /&gt;
  internal:&lt;br /&gt;
  traefik:&lt;br /&gt;
    name: traefik&lt;br /&gt;
    external: true&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;br /&gt;
Bring the entire stack up and when everything comes up, you&#039;re done. &lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # docker-compose up -d&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Using Traefik with your Docker containers ===&lt;br /&gt;
With the the Traefik container set up, we can now create containers to take advantage of Traefik as the ingress controller for the Docker host. This can be done through just container labels.  &lt;br /&gt;
Containers should be labeled with the following labels. &lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Description&lt;br /&gt;
!Label (example)&lt;br /&gt;
|-&lt;br /&gt;
|Enable Traefik for this container&lt;br /&gt;
|&amp;lt;code&amp;gt;traefik.enable=true&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|Define the Docker network that Traefik needs to use to connect to this container&lt;br /&gt;
|&amp;lt;code&amp;gt;traefik.docker.network=traefik&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|Define a new service to this container. For example, if your container is listening on port 8888, define a new Traefik service that listens on port 8888.&lt;br /&gt;
|&amp;lt;code&amp;gt;traefik.http.services.my-new-service.loadbalancer.server.port=8888&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|Expose your container using the service created previously via the HTTPS entrypoint.&lt;br /&gt;
This will make Traefik forward HTTPS data matching your virtual host to this container.&lt;br /&gt;
&lt;br /&gt;
SSL certificates are handled with Let&#039;s Encrypt.&lt;br /&gt;
|&amp;lt;code&amp;gt;traefik.http.routers.new-service-router-https.entrypoints=https&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;traefik.http.routers.new-service-router-https.rule=Host(`my-new-service.example.com`)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;traefik.http.routers.new-service-router-https.service=gitlab-service&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;traefik.http.routers.new-service-router-https.tls=true&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;traefik.http.routers.new-service-router-https.tls.certresolver=letsencrypt&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|You can also expose your container via HTTP.&lt;br /&gt;
Be sure that your router name is distinct.&lt;br /&gt;
|&amp;lt;code&amp;gt;traefik.http.routers.new-service-router.entrypoints=http&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;traefik.http.routers.new-service-router.rule=Host(`my-new-service.example.com`)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;traefik.http.routers.new-service-router.service=gitlab-service&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|If you want your HTTP traffic redirected to the HTTPS version, create a new middleware that redirects to the HTTPS scheme.&lt;br /&gt;
|&amp;lt;code&amp;gt;traefik.http.routers.new-service-router.entrypoints=http&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;traefik.http.routers.new-service-router.rule=Host(`my-new-service.example.com`)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;traefik.http.routers.new-service-router.middlewares=new-service-redirect&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;traefik.http.middlewares.new-service-redirect.redirectscheme.scheme=https&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
If your container exposes multiple services, you can define multiple services and routers in your labels. Just make sure things are named uniquely.&lt;br /&gt;
&lt;br /&gt;
Here&#039;s the configuration for one of my test Wiki containers using Traefik.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = wiki:&lt;br /&gt;
    image: mediawiki:1.37&lt;br /&gt;
    labels:&lt;br /&gt;
      - traefik.enable=true&lt;br /&gt;
      - traefik.docker.network=traefik&lt;br /&gt;
      - traefik.http.middlewares.wiki-https-redirect.redirectscheme.scheme=https&lt;br /&gt;
      # http&lt;br /&gt;
      - traefik.http.routers.wiki.entrypoints=http&lt;br /&gt;
      - traefik.http.routers.wiki.rule=Host(`wiki-test.example.com`)&lt;br /&gt;
      - traefik.http.routers.wiki.middlewares=wiki-https-redirect&lt;br /&gt;
      # https&lt;br /&gt;
      - traefik.http.routers.wiki-https.entrypoints=https&lt;br /&gt;
      - traefik.http.routers.wiki-https.rule=Host(`wiki-test.example.com`)&lt;br /&gt;
      - traefik.http.routers.wiki-https.tls=true&lt;br /&gt;
      - traefik.http.routers.wiki-https.tls.certresolver=letsencrypt&lt;br /&gt;
      - traefik.http.routers.wiki-https.service=wiki-https&lt;br /&gt;
      - traefik.http.services.wiki-https.loadbalancer.server.port=8080&lt;br /&gt;
    networks:&lt;br /&gt;
      - traefik&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;8080&amp;quot;&lt;br /&gt;
    restart: always&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Standalone setup ==&lt;br /&gt;
Traefik can be used as a standalone service to reverse proxy and do SSL termination. The example below will set up a simple reverse proxy for a local web service listening on port 8000. The SSL certificate will automatically be obtained using Let&#039;s Encrypt.&lt;br /&gt;
&lt;br /&gt;
Create the following &amp;lt;code&amp;gt;traefik.yml&amp;lt;/code&amp;gt; configuration file at &amp;lt;code&amp;gt;/etc/traefik/traefik.yml&amp;lt;/code&amp;gt;:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = log:&lt;br /&gt;
  level: INFO&lt;br /&gt;
  filepath: &amp;quot;/var/log/traefik.log&amp;quot;&lt;br /&gt;
&lt;br /&gt;
accessLog:&lt;br /&gt;
  filepath: &amp;quot;/var/log/access.log&amp;quot;&lt;br /&gt;
  fields:&lt;br /&gt;
    defaultMode: keep&lt;br /&gt;
    headers:&lt;br /&gt;
      defaultMode: keep&lt;br /&gt;
&lt;br /&gt;
api:&lt;br /&gt;
  dashboard: false&lt;br /&gt;
  insecure: false&lt;br /&gt;
  debug: true&lt;br /&gt;
&lt;br /&gt;
entryPoints:&lt;br /&gt;
  http:&lt;br /&gt;
    address: &amp;quot;:80&amp;quot;&lt;br /&gt;
  https:&lt;br /&gt;
    address: &amp;quot;:443&amp;quot;&lt;br /&gt;
&lt;br /&gt;
providers:&lt;br /&gt;
  file:&lt;br /&gt;
    filename: &amp;quot;/etc/traefik/dynamic_config.yml&amp;quot;&lt;br /&gt;
    watch: true&lt;br /&gt;
&lt;br /&gt;
certificatesResolvers:&lt;br /&gt;
  letsencrypt:&lt;br /&gt;
    acme:&lt;br /&gt;
      email: my-email-address@example.com&lt;br /&gt;
      storage: &amp;quot;/etc/traefik/acme.json&amp;quot;&lt;br /&gt;
      httpChallenge:&lt;br /&gt;
        entryPoint: http&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;br /&gt;
Create a &amp;lt;code&amp;gt;dynamic_config.yml&amp;lt;/code&amp;gt; file at &amp;lt;code&amp;gt;/etc/traefik/dynamic_config.yml&amp;lt;/code&amp;gt;. This file will define our routes and backend services. You must have a separate router for each of the insecure http and secure https routes to your backend service.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = http:&lt;br /&gt;
  serversTransports:&lt;br /&gt;
    ignorecert:&lt;br /&gt;
      insecureSkipVerify: true&lt;br /&gt;
&lt;br /&gt;
  routers:&lt;br /&gt;
    testing-http:&lt;br /&gt;
      rule: Host(`testing.example.com`)&lt;br /&gt;
      service: my-testing-service&lt;br /&gt;
      entrypoints:&lt;br /&gt;
        - http&lt;br /&gt;
		&lt;br /&gt;
    testing-https:&lt;br /&gt;
      rule: Host(`testing.example.com`)&lt;br /&gt;
      service: my-testing-service&lt;br /&gt;
      entrypoints:&lt;br /&gt;
        - https&lt;br /&gt;
      tls:&lt;br /&gt;
        certresolver: letsencrypt&lt;br /&gt;
&lt;br /&gt;
  services:&lt;br /&gt;
    my-testing-service:&lt;br /&gt;
      loadBalancer:&lt;br /&gt;
        servers:&lt;br /&gt;
          - url: &amp;quot;http://127.0.0.1:8000&amp;quot;&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;br /&gt;
Start traefik:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # traefik --web --config /etc/traefik/traefik.yml&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==Tasks==&lt;br /&gt;
&lt;br /&gt;
===Add host header on requests to the backend===&lt;br /&gt;
You can specify custom request headers by injecting a middleware. For example, when reverse proxying HTTP traffic for a website, you may wish to have the reverse proxy provide the &#039;Host&#039; header. To do this, we can add the following dynamic config.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = http:&lt;br /&gt;
  routers:&lt;br /&gt;
    public:&lt;br /&gt;
      rule: Host(`public.example.com`)&lt;br /&gt;
      service: public&lt;br /&gt;
      entrypoints:&lt;br /&gt;
        - http&lt;br /&gt;
      middlewares:&lt;br /&gt;
        - public&lt;br /&gt;
&lt;br /&gt;
  services:&lt;br /&gt;
    public:&lt;br /&gt;
      loadBalancer:&lt;br /&gt;
        servers:&lt;br /&gt;
          - url: &amp;quot;http://10.1.2.200&amp;quot;&lt;br /&gt;
&lt;br /&gt;
  middlewares:&lt;br /&gt;
    public:&lt;br /&gt;
      headers:&lt;br /&gt;
        customRequestHeaders:&lt;br /&gt;
          Host: &amp;quot;public.example.com&amp;quot;&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Add a path prefix ===&lt;br /&gt;
To serve content under a specific path (eg. /downloads) using another container, create the service as you would normally but specify the router rule to include a &amp;lt;code&amp;gt;PathPrefix&amp;lt;/code&amp;gt;. Traefik can also strip out this path prefix so that the underlying server sees only the root path (eg. have /downloads/file become /file) using the stripprefix middleware. &lt;br /&gt;
&lt;br /&gt;
Here is an example &amp;lt;code&amp;gt;docker-compose.yml&amp;lt;/code&amp;gt; definition:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = services:&lt;br /&gt;
  downloads:&lt;br /&gt;
    build:&lt;br /&gt;
      context: ./downloads&lt;br /&gt;
    labels:&lt;br /&gt;
      - traefik.enable=true&lt;br /&gt;
      - traefik.docker.network=traefik&lt;br /&gt;
      # http&lt;br /&gt;
      - traefik.http.routers.downloads.entrypoints=http&lt;br /&gt;
      - &#039;traefik.http.routers.downloads.rule=(Host(`example.com`) &amp;amp;&amp;amp; PathPrefix(`/downloads/`))&#039;&lt;br /&gt;
      - traefik.http.routers.downloads.middlewares=downloads-https-redirect&lt;br /&gt;
      - traefik.http.middlewares.downloads-https-redirect.redirectscheme.scheme=https&lt;br /&gt;
      # https&lt;br /&gt;
      - traefik.http.routers.downloads-https.entrypoints=https&lt;br /&gt;
      - &#039;traefik.http.routers.downloads-https.rule=(Host(`example.com`) &amp;amp;&amp;amp; PathPrefix(`/downloads/`))&#039;&lt;br /&gt;
      - traefik.http.routers.downloads-https.tls=true&lt;br /&gt;
      - traefik.http.routers.downloads-https.tls.certresolver=letsencrypt&lt;br /&gt;
      - traefik.http.services.downloads-https.loadbalancer.server.port=8080&lt;br /&gt;
      - traefik.http.routers.downloads-https.service=downloads-https&lt;br /&gt;
      - traefik.http.routers.downloads-https.middlewares=downloads-strip-prefix&lt;br /&gt;
      - traefik.http.middlewares.downloads-strip-prefix.stripprefix.prefixes=/downloads&lt;br /&gt;
    networks:&lt;br /&gt;
      - traefik&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;8080&amp;quot;&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Install a custom certificate ===&lt;br /&gt;
Installing a custom certificate and private key in Traefik is simple:&lt;br /&gt;
&lt;br /&gt;
# Edit the traefik.conf and ensure that it has a dynamic configuration set. {{Highlight&lt;br /&gt;
| code = providers:&lt;br /&gt;
  file:&lt;br /&gt;
    filename: &amp;quot;/config/dynamic_config.yml&amp;quot;&lt;br /&gt;
    watch: true&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;br /&gt;
# In the dynamic configuration file, specify the certificate and key files. For example, here is what I would place for a *.example.com wildcard certificate: {{Highlight&lt;br /&gt;
| code = tls:&lt;br /&gt;
  certificates:&lt;br /&gt;
    - certFile: /config/ssl/example.com.crt&lt;br /&gt;
      keyFile: /config/ssl/example.com.key&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Place the certificates in the /config/ssl directory.&lt;br /&gt;
&lt;br /&gt;
Restart Traefik if you didn&#039;t previously have the dynamic config file defined in the traefik.conf previously. Traefik will automatically determine what domains it has certificates for and use it as required.&lt;br /&gt;
&lt;br /&gt;
Your certificates must be set to mode 0600 (that is, it cannot be readable to anyone else) for them to be loaded properly.&lt;br /&gt;
&lt;br /&gt;
==== Certificates with intermediate signing certificates ====&lt;br /&gt;
If your certificate is signed by one or more intermediate certificates, you will likely want to include them within the certificate file you pass to Traefik. The certificate file containing the full set of intermediate and root certificates is called a fullchain certificate. This fullchain cert file can be generated by concatenating certificates furthest from the root certificate first. That is: your certificate, followed by the intermediates furthest from root first, then the root certificate.&lt;br /&gt;
&lt;br /&gt;
For example, if I have an Entrust certificate, concatenate the files together:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # cat ServerCertificate.crt Intermediate1.crt Intermediate2.crt Root.crt &amp;gt; fullchain.crt&lt;br /&gt;
&lt;br /&gt;
## If your certs don&#039;t end on a new line...&lt;br /&gt;
# for i in ServerCertificate.crt Intermediate1.crt Intermediate2.crt Root.crt ; do cat $i ; echo ; done &amp;gt; fullchain.crt&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}Remember to set the mode to 0600 when using the fullchain certificate.{{Navbox Linux}}&lt;br /&gt;
{{Navbox Web}}&lt;br /&gt;
[[Category:WebApp]]&lt;br /&gt;
[[Category:Linux]]&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Traefik&amp;diff=7690</id>
		<title>Traefik</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Traefik&amp;diff=7690"/>
		<updated>2025-07-02T16:36:17Z</updated>

		<summary type="html">&lt;p&gt;Leo: Certificates with intermediate certs&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Traefik is an open source HTTP reverse proxy and load balancer written in golang. It can be integrated into various infrastructures such as Docker and Kubernetes or used as a standalone service. Traefik comes with lots of automation features such as dynamic configuration through container labels and automatic SSL certificate renewals using the ACME protocol.&lt;br /&gt;
&lt;br /&gt;
==Docker integration==&lt;br /&gt;
Traefik can be used as the reverse proxy for all web-related services on a Docker host. The added benefit with this approach is that new web services that are added can be automatically configured through docker labels and SSL certificates are automatically obtained provided that the DNS names already resolve. &lt;br /&gt;
&lt;br /&gt;
Extra care is required when using Traefik and Docker since Traefik needs to have access to the Docker socket in order to see container events for its automatic configuration. Since having access to the Docker socket is pretty much equivalent to owning the Docker host, from a security stand-point, it&#039;s best to have an intermediate Docker container proxy the Docker socket so that it can only be read from but not written to. More on this set up below. &lt;br /&gt;
&lt;br /&gt;
===Setting up Traefik with Docker Compose===&lt;br /&gt;
Create a new &#039;traefik&#039; Docker network. This network will be used by any other containers on the host that needs to be reverse proxied by Treafik.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # docker network create traefik&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Next, we will set up the Traefik docker-compose stack. Traefik should be configured to work with Docker. We&#039;ll also restrict Traefik from exposing containers unless they are explicitly labeled.  Review the [https://doc.traefik.io/traefik/providers/docker/ Traefik Docker documentation] for all the available Docker related options.&lt;br /&gt;
&lt;br /&gt;
Included here is the socket-proxy which we&#039;ll use to restrict what Traefik can do to Docker in case it gets compromised.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = version: &#039;3.3&#039;&lt;br /&gt;
&lt;br /&gt;
services:&lt;br /&gt;
  traefik:&lt;br /&gt;
    image: traefik:latest&lt;br /&gt;
    restart: always&lt;br /&gt;
    command: --web --docker --docker.watch --docker.exposedbydefault=false --providers.docker.endpoint=tcp://socket-proxy:2375&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ./config/traefik.yml:/traefik.yml&lt;br /&gt;
      - ./config:/config&lt;br /&gt;
      - /var/log/traefik:/var/log/traefik&lt;br /&gt;
    networks:&lt;br /&gt;
      - traefik&lt;br /&gt;
      - internal&lt;br /&gt;
    environment:&lt;br /&gt;
      - &amp;quot;TZ=MST7MDT,M3.2.0,M11.1.0&amp;quot;&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;80:80&amp;quot;&lt;br /&gt;
      - &amp;quot;443:443&amp;quot;&lt;br /&gt;
&lt;br /&gt;
  # https://github.com/Tecnativa/docker-socket-proxy&lt;br /&gt;
  socket-proxy:&lt;br /&gt;
    build:&lt;br /&gt;
      context: ./socket-proxy&lt;br /&gt;
      dockerfile: Dockerfile&lt;br /&gt;
    restart: always&lt;br /&gt;
    volumes:&lt;br /&gt;
      - /var/run/docker.sock:/var/run/docker.sock&lt;br /&gt;
    environment:&lt;br /&gt;
      CONTAINERS: 1&lt;br /&gt;
    networks:&lt;br /&gt;
      - internal&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;2375&amp;quot;&lt;br /&gt;
&lt;br /&gt;
networks:&lt;br /&gt;
  internal:&lt;br /&gt;
  traefik:&lt;br /&gt;
    name: traefik&lt;br /&gt;
    external: true&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;br /&gt;
Bring the entire stack up and when everything comes up, you&#039;re done. &lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # docker-compose up -d&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Using Traefik with your Docker containers ===&lt;br /&gt;
With the the Traefik container set up, we can now create containers to take advantage of Traefik as the ingress controller for the Docker host. This can be done through just container labels.  &lt;br /&gt;
Containers should be labeled with the following labels. &lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Description&lt;br /&gt;
!Label (example)&lt;br /&gt;
|-&lt;br /&gt;
|Enable Traefik for this container&lt;br /&gt;
|&amp;lt;code&amp;gt;traefik.enable=true&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|Define the Docker network that Traefik needs to use to connect to this container&lt;br /&gt;
|&amp;lt;code&amp;gt;traefik.docker.network=traefik&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|Define a new service to this container. For example, if your container is listening on port 8888, define a new Traefik service that listens on port 8888.&lt;br /&gt;
|&amp;lt;code&amp;gt;traefik.http.services.my-new-service.loadbalancer.server.port=8888&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|Expose your container using the service created previously via the HTTPS entrypoint.&lt;br /&gt;
This will make Traefik forward HTTPS data matching your virtual host to this container.&lt;br /&gt;
&lt;br /&gt;
SSL certificates are handled with Let&#039;s Encrypt.&lt;br /&gt;
|&amp;lt;code&amp;gt;traefik.http.routers.new-service-router-https.entrypoints=https&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;traefik.http.routers.new-service-router-https.rule=Host(`my-new-service.example.com`)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;traefik.http.routers.new-service-router-https.service=gitlab-service&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;traefik.http.routers.new-service-router-https.tls=true&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;traefik.http.routers.new-service-router-https.tls.certresolver=letsencrypt&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|You can also expose your container via HTTP.&lt;br /&gt;
Be sure that your router name is distinct.&lt;br /&gt;
|&amp;lt;code&amp;gt;traefik.http.routers.new-service-router.entrypoints=http&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;traefik.http.routers.new-service-router.rule=Host(`my-new-service.example.com`)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;traefik.http.routers.new-service-router.service=gitlab-service&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|If you want your HTTP traffic redirected to the HTTPS version, create a new middleware that redirects to the HTTPS scheme.&lt;br /&gt;
|&amp;lt;code&amp;gt;traefik.http.routers.new-service-router.entrypoints=http&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;traefik.http.routers.new-service-router.rule=Host(`my-new-service.example.com`)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;traefik.http.routers.new-service-router.middlewares=new-service-redirect&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;traefik.http.middlewares.new-service-redirect.redirectscheme.scheme=https&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
If your container exposes multiple services, you can define multiple services and routers in your labels. Just make sure things are named uniquely.&lt;br /&gt;
&lt;br /&gt;
Here&#039;s the configuration for one of my test Wiki containers using Traefik.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = wiki:&lt;br /&gt;
    image: mediawiki:1.37&lt;br /&gt;
    labels:&lt;br /&gt;
      - traefik.enable=true&lt;br /&gt;
      - traefik.docker.network=traefik&lt;br /&gt;
      - traefik.http.middlewares.wiki-https-redirect.redirectscheme.scheme=https&lt;br /&gt;
      # http&lt;br /&gt;
      - traefik.http.routers.wiki.entrypoints=http&lt;br /&gt;
      - traefik.http.routers.wiki.rule=Host(`wiki-test.example.com`)&lt;br /&gt;
      - traefik.http.routers.wiki.middlewares=wiki-https-redirect&lt;br /&gt;
      # https&lt;br /&gt;
      - traefik.http.routers.wiki-https.entrypoints=https&lt;br /&gt;
      - traefik.http.routers.wiki-https.rule=Host(`wiki-test.example.com`)&lt;br /&gt;
      - traefik.http.routers.wiki-https.tls=true&lt;br /&gt;
      - traefik.http.routers.wiki-https.tls.certresolver=letsencrypt&lt;br /&gt;
      - traefik.http.routers.wiki-https.service=wiki-https&lt;br /&gt;
      - traefik.http.services.wiki-https.loadbalancer.server.port=8080&lt;br /&gt;
    networks:&lt;br /&gt;
      - traefik&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;8080&amp;quot;&lt;br /&gt;
    restart: always&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Standalone setup ==&lt;br /&gt;
Traefik can be used as a standalone service to reverse proxy and do SSL termination. The example below will set up a simple reverse proxy for a local web service listening on port 8000. The SSL certificate will automatically be obtained using Let&#039;s Encrypt.&lt;br /&gt;
&lt;br /&gt;
Create the following &amp;lt;code&amp;gt;traefik.yml&amp;lt;/code&amp;gt; configuration file at &amp;lt;code&amp;gt;/etc/traefik/traefik.yml&amp;lt;/code&amp;gt;:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = log:&lt;br /&gt;
  level: INFO&lt;br /&gt;
  filepath: &amp;quot;/var/log/traefik.log&amp;quot;&lt;br /&gt;
&lt;br /&gt;
accessLog:&lt;br /&gt;
  filepath: &amp;quot;/var/log/access.log&amp;quot;&lt;br /&gt;
  fields:&lt;br /&gt;
    defaultMode: keep&lt;br /&gt;
    headers:&lt;br /&gt;
      defaultMode: keep&lt;br /&gt;
&lt;br /&gt;
api:&lt;br /&gt;
  dashboard: false&lt;br /&gt;
  insecure: false&lt;br /&gt;
  debug: true&lt;br /&gt;
&lt;br /&gt;
entryPoints:&lt;br /&gt;
  http:&lt;br /&gt;
    address: &amp;quot;:80&amp;quot;&lt;br /&gt;
  https:&lt;br /&gt;
    address: &amp;quot;:443&amp;quot;&lt;br /&gt;
&lt;br /&gt;
providers:&lt;br /&gt;
  file:&lt;br /&gt;
    filename: &amp;quot;/etc/traefik/dynamic_config.yml&amp;quot;&lt;br /&gt;
    watch: true&lt;br /&gt;
&lt;br /&gt;
certificatesResolvers:&lt;br /&gt;
  letsencrypt:&lt;br /&gt;
    acme:&lt;br /&gt;
      email: my-email-address@example.com&lt;br /&gt;
      storage: &amp;quot;/etc/traefik/acme.json&amp;quot;&lt;br /&gt;
      httpChallenge:&lt;br /&gt;
        entryPoint: http&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;br /&gt;
Create a &amp;lt;code&amp;gt;dynamic_config.yml&amp;lt;/code&amp;gt; file at &amp;lt;code&amp;gt;/etc/traefik/dynamic_config.yml&amp;lt;/code&amp;gt;. This file will define our routes and backend services. You must have a separate router for each of the insecure http and secure https routes to your backend service.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = http:&lt;br /&gt;
  serversTransports:&lt;br /&gt;
    ignorecert:&lt;br /&gt;
      insecureSkipVerify: true&lt;br /&gt;
&lt;br /&gt;
  routers:&lt;br /&gt;
    testing-http:&lt;br /&gt;
      rule: Host(`testing.example.com`)&lt;br /&gt;
      service: my-testing-service&lt;br /&gt;
      entrypoints:&lt;br /&gt;
        - http&lt;br /&gt;
		&lt;br /&gt;
    testing-https:&lt;br /&gt;
      rule: Host(`testing.example.com`)&lt;br /&gt;
      service: my-testing-service&lt;br /&gt;
      entrypoints:&lt;br /&gt;
        - https&lt;br /&gt;
      tls:&lt;br /&gt;
        certresolver: letsencrypt&lt;br /&gt;
&lt;br /&gt;
  services:&lt;br /&gt;
    my-testing-service:&lt;br /&gt;
      loadBalancer:&lt;br /&gt;
        servers:&lt;br /&gt;
          - url: &amp;quot;http://127.0.0.1:8000&amp;quot;&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;br /&gt;
Start traefik:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # traefik --web --config /etc/traefik/traefik.yml&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==Tasks==&lt;br /&gt;
&lt;br /&gt;
===Add host header on requests to the backend===&lt;br /&gt;
You can specify custom request headers by injecting a middleware. For example, when reverse proxying HTTP traffic for a website, you may wish to have the reverse proxy provide the &#039;Host&#039; header. To do this, we can add the following dynamic config.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = http:&lt;br /&gt;
  routers:&lt;br /&gt;
    public:&lt;br /&gt;
      rule: Host(`public.example.com`)&lt;br /&gt;
      service: public&lt;br /&gt;
      entrypoints:&lt;br /&gt;
        - http&lt;br /&gt;
      middlewares:&lt;br /&gt;
        - public&lt;br /&gt;
&lt;br /&gt;
  services:&lt;br /&gt;
    public:&lt;br /&gt;
      loadBalancer:&lt;br /&gt;
        servers:&lt;br /&gt;
          - url: &amp;quot;http://10.1.2.200&amp;quot;&lt;br /&gt;
&lt;br /&gt;
  middlewares:&lt;br /&gt;
    public:&lt;br /&gt;
      headers:&lt;br /&gt;
        customRequestHeaders:&lt;br /&gt;
          Host: &amp;quot;public.example.com&amp;quot;&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Add a path prefix ===&lt;br /&gt;
To serve content under a specific path (eg. /downloads) using another container, create the service as you would normally but specify the router rule to include a &amp;lt;code&amp;gt;PathPrefix&amp;lt;/code&amp;gt;. Traefik can also strip out this path prefix so that the underlying server sees only the root path (eg. have /downloads/file become /file) using the stripprefix middleware. &lt;br /&gt;
&lt;br /&gt;
Here is an example &amp;lt;code&amp;gt;docker-compose.yml&amp;lt;/code&amp;gt; definition:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = services:&lt;br /&gt;
  downloads:&lt;br /&gt;
    build:&lt;br /&gt;
      context: ./downloads&lt;br /&gt;
    labels:&lt;br /&gt;
      - traefik.enable=true&lt;br /&gt;
      - traefik.docker.network=traefik&lt;br /&gt;
      # http&lt;br /&gt;
      - traefik.http.routers.downloads.entrypoints=http&lt;br /&gt;
      - &#039;traefik.http.routers.downloads.rule=(Host(`example.com`) &amp;amp;&amp;amp; PathPrefix(`/downloads/`))&#039;&lt;br /&gt;
      - traefik.http.routers.downloads.middlewares=downloads-https-redirect&lt;br /&gt;
      - traefik.http.middlewares.downloads-https-redirect.redirectscheme.scheme=https&lt;br /&gt;
      # https&lt;br /&gt;
      - traefik.http.routers.downloads-https.entrypoints=https&lt;br /&gt;
      - &#039;traefik.http.routers.downloads-https.rule=(Host(`example.com`) &amp;amp;&amp;amp; PathPrefix(`/downloads/`))&#039;&lt;br /&gt;
      - traefik.http.routers.downloads-https.tls=true&lt;br /&gt;
      - traefik.http.routers.downloads-https.tls.certresolver=letsencrypt&lt;br /&gt;
      - traefik.http.services.downloads-https.loadbalancer.server.port=8080&lt;br /&gt;
      - traefik.http.routers.downloads-https.service=downloads-https&lt;br /&gt;
      - traefik.http.routers.downloads-https.middlewares=downloads-strip-prefix&lt;br /&gt;
      - traefik.http.middlewares.downloads-strip-prefix.stripprefix.prefixes=/downloads&lt;br /&gt;
    networks:&lt;br /&gt;
      - traefik&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;8080&amp;quot;&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Install a custom certificate ===&lt;br /&gt;
Installing a custom certificate and private key in Traefik is simple:&lt;br /&gt;
&lt;br /&gt;
# Edit the traefik.conf and ensure that it has a dynamic configuration set. {{Highlight&lt;br /&gt;
| code = providers:&lt;br /&gt;
  file:&lt;br /&gt;
    filename: &amp;quot;/config/dynamic_config.yml&amp;quot;&lt;br /&gt;
    watch: true&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;br /&gt;
# In the dynamic configuration file, specify the certificate and key files. For example, here is what I would place for a *.example.com wildcard certificate: {{Highlight&lt;br /&gt;
| code = tls:&lt;br /&gt;
  certificates:&lt;br /&gt;
    - certFile: /config/ssl/example.com.crt&lt;br /&gt;
      keyFile: /config/ssl/example.com.key&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Place the certificates in the /config/ssl directory.&lt;br /&gt;
&lt;br /&gt;
Restart Traefik if you didn&#039;t previously have the dynamic config file defined in the traefik.conf previously. Traefik will automatically determine what domains it has certificates for and use it as required.&lt;br /&gt;
&lt;br /&gt;
==== Certificates with intermediate signing certificates ====&lt;br /&gt;
If your certificate is signed by one or more intermediate certificates, you will likely want to include them within the certificate file you pass to Traefik. Do this by concatenating certificates furthest from the root certificate first. That is: your certificate, followed by the intermediates furthest from root first, then the root certificate.&lt;br /&gt;
&lt;br /&gt;
For example, if I have an Entrust certificate, concatenate the files together:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # cat ServerCertificate.crt Intermediate1.crt Intermediate2.crt Root.crt &amp;gt; certificate.crt&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
{{Navbox Linux}}&lt;br /&gt;
{{Navbox Web}}&lt;br /&gt;
[[Category:WebApp]]&lt;br /&gt;
[[Category:Linux]]&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Entrust&amp;diff=7689</id>
		<title>Entrust</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Entrust&amp;diff=7689"/>
		<updated>2025-07-02T16:25:23Z</updated>

		<summary type="html">&lt;p&gt;Leo: Sectigo signing certificate info&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Entrust is a certificate authority.&lt;br /&gt;
&lt;br /&gt;
== Sectigo ==&lt;br /&gt;
Google stopped trusting Entrust signed certificates since around June 2024. Entrust has since started signing their certificates through Sectigo. The certificate chain for a EV certificate now looks like this:&lt;br /&gt;
&lt;br /&gt;
USERTrust RSA Certification Authority&lt;br /&gt;
&lt;br /&gt;
* [https://paste.steamr.com/view/raw/97e71c82 Sectigo Public Server Authentication Root R46]&lt;br /&gt;
** [https://paste.steamr.com/view/raw/8a4028f7 Entrust EV TLS Issuing RSA CA 2]&lt;br /&gt;
&lt;br /&gt;
Like Entrust&#039;s crap certs, you&#039;ll have to install the Sectigo and Entrust intermediate certs to make most things work. For some reason, I don&#039;t see the intermediate certs listed on their website, so the links above go to a copy on my pastebin.&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
The Entrust Root Certification Authority (G2) doesn&#039;t appear to be part of the system root CA. As a result, if you try to &amp;lt;code&amp;gt;wget&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;curl&amp;lt;/code&amp;gt; to a resource using certificates signed by Entrust, you&#039;ll get an error like the one below.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # wget -O - https://somewhere.ucalgary.ca/&lt;br /&gt;
--2022-03-29 16:58:39--  https://somewhere.ucalgary.ca/&lt;br /&gt;
Resolving somewhere.ucalgary.ca (somewhere.ucalgary.ca)... 10.43.144.134&lt;br /&gt;
Connecting to somewhere.ucalgary.ca (somewhere.ucalgary.ca){{!}}10.43.144.134{{!}}:443... connected.&lt;br /&gt;
ERROR: cannot verify somewhere.ucalgary.ca&#039;s certificate, issued by ‘CN=Entrust Certification Authority - L1K,OU=(c) 2012 Entrust\\, Inc. - for authorized use only,OU=See www.entrust.net/legal-terms,O=Entrust\\, Inc.,C=US’:&lt;br /&gt;
  Unable to locally verify the issuer&#039;s authority.&lt;br /&gt;
To connect to somewhere.ucalgary.ca insecurely, use `--no-check-certificate&#039;.&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
To fix this, you&#039;ll need to install the root and chain certificates that are provided by Entrust at https://www.entrust.com/resources/certificate-solutions/tools/root-certificate-downloads.&lt;br /&gt;
&lt;br /&gt;
=== Installing certificates ===&lt;br /&gt;
On RHEL based systems:{{Highlight&lt;br /&gt;
| code = # wget https://web.entrust.com/root-certificates/entrust_l1k.cer -O /usr/share/pki/ca-trust-source/anchors/entrust_l1k.cer&lt;br /&gt;
# wget https://web.entrust.com/root-certificates/entrust_g2_ca.cer -O /usr/share/pki/ca-trust-source/anchors/entrust_g2_ca.cer&lt;br /&gt;
# wget https://web.entrust.com/root-certificates/entrust_l1m_sha2.cer -O  /usr/share/pki/ca-trust-source/anchors/entrust_l1m_sha2.cer&lt;br /&gt;
# update-ca-trust extract&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}On Ubuntu:{{Highlight&lt;br /&gt;
| code = # wget https://web.entrust.com/root-certificates/entrust_l1k.cer   -O /usr/share/ca-certificates/entrust_l1k.cer&lt;br /&gt;
# wget https://web.entrust.com/root-certificates/entrust_g2_ca.cer -O /usr/share/ca-certificates/entrust_g2_ca.cer&lt;br /&gt;
# wget https://web.entrust.com/root-certificates/entrust_l1m_sha2.cer -O  /usr/share/ca-certificates/entrust_l1m_sha2.cer&lt;br /&gt;
# update-ca-certificates&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}On Debian:{{Highlight&lt;br /&gt;
| code = # mkdir -p /usr/local/share/ca-certificates&lt;br /&gt;
# cd /usr/local/share/ca-certificates&lt;br /&gt;
# curl https://web.entrust.com/root-certificates/entrust_l1k.cer  &amp;gt; entrust_l1k.cer&lt;br /&gt;
# curl https://web.entrust.com/root-certificates/entrust_g2_ca.cer &amp;gt; entrust_g2_ca.cer&lt;br /&gt;
# curl https://web.entrust.com/root-certificates/entrust_l1m_sha2.cer &amp;gt;  entrust_l1m_sha2.cer&lt;br /&gt;
# for i in *cer; do openssl x509 -inform PEM  -in $i -outform PEM -out ${i%.cer}.crt ; done&lt;br /&gt;
# update-ca-certificates&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=CloudStack&amp;diff=7688</id>
		<title>CloudStack</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=CloudStack&amp;diff=7688"/>
		<updated>2025-06-18T15:56:54Z</updated>

		<summary type="html">&lt;p&gt;Leo: /* Re-add existing KVM bare metal host */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Apache CloudStack is open-source cloud computing software. It is used to deploy a infrastructure as a service (IaaS) platform on virtualization technologies such as KVM, VMware, and Xen. This is similar to OpenStack but is significantly simpler to setup and manage (albeit with less features).&lt;br /&gt;
&lt;br /&gt;
This page contains my notes on setting up and using CloudStack 4.15. I am by no means a CloudStack expert so take my notes here with a huge grain of salt and feel free to make corrections.&lt;br /&gt;
&lt;br /&gt;
==Installation==&lt;br /&gt;
This installation is based on CloudStack 4.15 using CentOS 8. The setup described below uses KVM and Open vSwitch. I&#039;m basing the design decisions and approach from the installation guide at [http://docs.cloudstack.apache.org/en/latest/quickinstallationguide/qig.html#management-server-installation http://docs.cloudstack.apache.org/en/latest/quickinstallationguide/qig.html]&lt;br /&gt;
&lt;br /&gt;
===Overview===&lt;br /&gt;
I will have 1 management node and a few bare metal nodes. All nodes will have the same processor (Intel something) and memory (24GB).&lt;br /&gt;
&lt;br /&gt;
Each node will have the same network configuration based on OpenVSwitch. There will be only 1 ethernet connection per node with various VLANs trunked to each node. The VLANs are:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Network&lt;br /&gt;
!Vlan&lt;br /&gt;
!Network subnet&lt;br /&gt;
|-&lt;br /&gt;
|Management&lt;br /&gt;
|11, untagged&lt;br /&gt;
|172.19.0.0/20&lt;br /&gt;
|-&lt;br /&gt;
|Storage&lt;br /&gt;
|3205&lt;br /&gt;
|172.22.0.0/24&lt;br /&gt;
|-&lt;br /&gt;
|Guest&lt;br /&gt;
|100 - 200&lt;br /&gt;
|n/a&lt;br /&gt;
|-&lt;br /&gt;
|Public&lt;br /&gt;
|2&lt;br /&gt;
|136.159.1.0/24&lt;br /&gt;
|}&lt;br /&gt;
The network configs for the 4 nodes I&#039;ll be using are listed below. There is also a NFS server used for primary storage. The reason for the weird IPs is because this was set up on an existing network.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Node&lt;br /&gt;
!Networks&lt;br /&gt;
|-&lt;br /&gt;
|management&lt;br /&gt;
|Management: 172.19.12.141/20&lt;br /&gt;
Storage: 172.22.0.241/24&lt;br /&gt;
|-&lt;br /&gt;
|baremetal1&lt;br /&gt;
|Management: 172.19.12.142/20&lt;br /&gt;
Storage: 172.22.0.242/24&lt;br /&gt;
|-&lt;br /&gt;
|baremetal2&lt;br /&gt;
|Management: 172.19.12.143/20&lt;br /&gt;
Storage: 172.22.0.243/24&lt;br /&gt;
|-&lt;br /&gt;
|baremetal3&lt;br /&gt;
|Management: 172.19.12.144/20&lt;br /&gt;
Storage: 172.22.0.244/24&lt;br /&gt;
|-&lt;br /&gt;
|netapp1&lt;br /&gt;
|Storage: 172.22.0.19/24&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Switch config===&lt;br /&gt;
For completeness, here&#039;s the configuration of the HP Procurve switch that the nodes are connected to. The switch should have all the guest VLANs defined and tagged.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = config&lt;br /&gt;
&lt;br /&gt;
# Guest VLANs&lt;br /&gt;
vlan 100 name guest100&lt;br /&gt;
vlan 101 name guest101&lt;br /&gt;
...&lt;br /&gt;
vlan 200 name guest 200&lt;br /&gt;
interface 1-8 tagged vlan 100-200&lt;br /&gt;
&lt;br /&gt;
# Public, management, storage VLANs&lt;br /&gt;
vlan 2 name public&lt;br /&gt;
vlan 11 name management&lt;br /&gt;
vlan 3205 name storage&lt;br /&gt;
interface 1-8 untagged vlan 11&lt;br /&gt;
interface 1-8 tagged vlan 2,3205&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
===Node setup===&lt;br /&gt;
Each node will be set up with the following sub-steps.&lt;br /&gt;
&lt;br /&gt;
====CloudStack Repos====&lt;br /&gt;
Install CloudStack repos.{{Highlight&lt;br /&gt;
| code = # cat &amp;gt; /etc/yum.repos.d/cloudstack.repo &amp;lt;&amp;lt;EOF&lt;br /&gt;
[cloudstack]&lt;br /&gt;
name=cloudstack&lt;br /&gt;
baseurl=http://download.cloudstack.org/centos/8/4.15/&lt;br /&gt;
enabled=1&lt;br /&gt;
gpgcheck=0&lt;br /&gt;
EOF&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
====Install base packages====&lt;br /&gt;
Install all other dependencies.{{Highlight&lt;br /&gt;
| code = # yum -y install epel-release&lt;br /&gt;
# yum -y install bridge-utils net-tools&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}Install OpenVSwitch from CentOS Extras:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # yum -y install \&lt;br /&gt;
http://mirror.centos.org/centos/8/extras/x86_64/os/Packages/centos-release-nfv-openvswitch-1-3.el8.noarch.rpm \&lt;br /&gt;
http://mirror.centos.org/centos/8/extras/x86_64/os/Packages/centos-release-nfv-common-1-3.el8.noarch.rpm&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
====Disable SELinux====&lt;br /&gt;
The system should have SELinux disabled. Use &amp;lt;code&amp;gt;setenforce&amp;lt;/code&amp;gt; and edit the selinux config:{{Highlight&lt;br /&gt;
| code = # setenforce 0&lt;br /&gt;
# vi /etc/selinux/config &lt;br /&gt;
## disable selinux&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
====Disable firewalld====&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # systemctl stop firewalld&lt;br /&gt;
# systemctl disable firewalld&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
====Configure Open vSwitch====&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # echo &amp;quot;blacklist bridge&amp;quot; &amp;gt;&amp;gt; /etc/modprobe.d/local-blacklist.conf&lt;br /&gt;
# echo &amp;quot;install bridge /bin/false&amp;quot; &amp;gt;&amp;gt; /etc/modprobe.d/local-dontload.conf&lt;br /&gt;
&lt;br /&gt;
# systemctl enable --now openvswitch&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
We will be using network-scripts to configure the Open vSwitch bridges later. I removed NetworkManager but retained network-scripts to ensure NetworkManager doesn&#039;t interfere with my network setup. The install guide leaves NetworkManager around.&lt;br /&gt;
&lt;br /&gt;
I create a &#039;shared&#039; bridge that&#039;s tied to the network interface called &amp;lt;code&amp;gt;nic0&amp;lt;/code&amp;gt;. This was done to make it easier to change the bridge setup during my testing but this could be simplified. Each of the physical networks I later set up in CloudStack are its own individual bridge to make it obvious how VMs get connected to the network.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ovs-vsctl add-br   nic0&lt;br /&gt;
# ovs-vsctl add-port nic0 enp4s0f0 tag=11 vlan_mode=native-untagged&lt;br /&gt;
# ovs-vsctl set port nic0 trunks=2,11,40-49,3205&lt;br /&gt;
&lt;br /&gt;
# ovs-vsctl add-br management0 nic0 11&lt;br /&gt;
# ovs-vsctl add-br cloudbr0 nic0 2&lt;br /&gt;
# ovs-vsctl add-br cloudbr1 nic0 100&lt;br /&gt;
# ovs-vsctl add-br storage0 nic0 3205&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}The node&#039;s management IP address needs to be removed from the primary network interface and then assigned on the management0 interface. If you&#039;re doing this to a node remotely, this might interrupt your connection.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ip addr del 172.19.12.141/20 dev enp4s0f0&lt;br /&gt;
# ip addr add 172.19.12.141/20 dev management0&lt;br /&gt;
# ip route add default via 172.19.0.3&lt;br /&gt;
# ip addr add 172.22.0.241/24 dev storage0&lt;br /&gt;
&lt;br /&gt;
# ip link set management0 up&lt;br /&gt;
# ip link set storage0 up&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
====Network configuration====&lt;br /&gt;
Once the Open vSwitch bridges are set up, configure the interfaces as follows:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Network Interface&lt;br /&gt;
!Role&lt;br /&gt;
!Configuration&lt;br /&gt;
|-&lt;br /&gt;
|enp4s0f0&lt;br /&gt;
|primary NIC in the host&lt;br /&gt;
|up on boot; no IP&lt;br /&gt;
|-&lt;br /&gt;
|nic0&lt;br /&gt;
|network OVS switch that connects to the other bridges to the NIC&lt;br /&gt;
|up on boot; no IP&lt;br /&gt;
|-&lt;br /&gt;
|cloudbr0&lt;br /&gt;
|public traffic.&lt;br /&gt;
|up on boot; no IP&lt;br /&gt;
|-&lt;br /&gt;
|cloudbr1&lt;br /&gt;
|guest traffic&lt;br /&gt;
|up on boot; no IP&lt;br /&gt;
|-&lt;br /&gt;
|management0&lt;br /&gt;
|management traffic&lt;br /&gt;
|up on boot; assigned with management IP&lt;br /&gt;
|-&lt;br /&gt;
|storage0&lt;br /&gt;
|storage traffic&lt;br /&gt;
|up on boot; assigned with storage network IP&lt;br /&gt;
|-&lt;br /&gt;
|cloud0&lt;br /&gt;
|link local traffic&lt;br /&gt;
|up on boot; assigned 169.254.0.1/16&lt;br /&gt;
|}Network configs are applied using network-scripts. The idea here is to have the network interfaces be configured when the system boots automatically. For interfaces that require a static IP address, I used the following network-scripts file. Adjust the device name and IP address as required.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # cat &amp;lt;&amp;lt;EOF &amp;gt; /etc/sysconfig/network-scripts/ifcfg-cloudbr0&lt;br /&gt;
DEVICE=cloudbr0&lt;br /&gt;
TYPE=Bridge&lt;br /&gt;
ONBOOT=yes&lt;br /&gt;
BOOTPROTO=static&lt;br /&gt;
IPV6INIT=no&lt;br /&gt;
IPV6_AUTOCONF=no&lt;br /&gt;
DELAY=5&lt;br /&gt;
IPADDR=172.16.10.2&lt;br /&gt;
GATEWAY=172.16.10.1&lt;br /&gt;
NETMASK=255.255.255.0&lt;br /&gt;
DNS1=8.8.8.8&lt;br /&gt;
DNS2=8.8.4.4&lt;br /&gt;
USERCTL=no&lt;br /&gt;
NM_CONTROLLED=no&lt;br /&gt;
EOF&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}For devices that don&#039;t require a static IP:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = cat &amp;lt;&amp;lt;EOF &amp;gt; ifcfg-cloudbr0&lt;br /&gt;
DEVICE=cloudbr0&lt;br /&gt;
TYPE=OVSBridge&lt;br /&gt;
DEVICETYPE=ovs&lt;br /&gt;
ONBOOT=yes&lt;br /&gt;
BOOTPROTO=none&lt;br /&gt;
HOTPLUG=no&lt;br /&gt;
NM_CONTROLLED=no&lt;br /&gt;
EOF&lt;br /&gt;
| lang = text&lt;br /&gt;
}}Once configured, verify that your node comes up with the proper network settings on a reboot.&lt;br /&gt;
&lt;br /&gt;
===Management node setup===&lt;br /&gt;
On the management node, set up the network configs and the CloudStack management packages.&lt;br /&gt;
&lt;br /&gt;
====Setup Storage====&lt;br /&gt;
If you intend to use the management server as the primary and secondary storage, you will need to set up a NFS server. If you intend to use an external NFS server as the primary storage, you can skip this step.{{Highlight&lt;br /&gt;
| code = # mkdir -p /export/primary /export/secondary&lt;br /&gt;
# yum -y install nfs-utils&lt;br /&gt;
# cat &amp;gt; /etc/exports &amp;lt;&amp;lt;EOF&lt;br /&gt;
/export/secondary *(rw,async,no_root_squash,no_subtree_check)&lt;br /&gt;
/export/primary *(rw,async,no_root_squash,no_subtree_check)&lt;br /&gt;
EOF&lt;br /&gt;
# systemctl enable --now nfs-server&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
====CloudStack management services====&lt;br /&gt;
Install MySQL. MariaDB isn&#039;t supported and the installation fails with it.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # rpm -ivh http://repo.mysql.com/mysql80-community-release-el8.rpm&lt;br /&gt;
# yum -y install mysql-server&lt;br /&gt;
# yum -y install mysql-connector-python&lt;br /&gt;
&lt;br /&gt;
## edit /etc/my.cnf to have the following lines.&lt;br /&gt;
cat &amp;gt;&amp;gt; /etc/my.cnf &amp;lt;&amp;lt;EOF&lt;br /&gt;
[mysqld]&lt;br /&gt;
innodb_rollback_on_timeout=1&lt;br /&gt;
innodb_lock_wait_timeout=600&lt;br /&gt;
max_connections=350&lt;br /&gt;
log-bin=mysql-bin&lt;br /&gt;
binlog-format = &#039;ROW&#039;&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
# systemctl enable --now mysqld&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Setup CloudStack.{{Highlight&lt;br /&gt;
| code = # yum -y install cloudstack-management&lt;br /&gt;
&lt;br /&gt;
# cloudstack-setup-databases cloud:password@localhost --deploy-as=root&lt;br /&gt;
# cloudstack-setup-management&lt;br /&gt;
# systemctl enable --now cloudstack-management&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}After starting &amp;lt;code&amp;gt;cloudstack-management&amp;lt;/code&amp;gt; for the firs time, it might take from 2-10 minutes for the database to set up completely. During this time, the web interface won&#039;t be responsive. In the mean time, you will need to seed the system VM images to the secondary storage. If you are using an external NFS server for your secondary storage, adjust the mount point in the following command accordingly.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ## Seed the systemvm into secondary storage&lt;br /&gt;
# /usr/share/cloudstack-common/scripts/storage/secondary/cloud-install-sys-tmplt -m /export/secondary -u https://download.cloudstack.org/systemvm/4.15/systemvmtemplate-4.15.1-kvm.qcow2.bz2 -h kvm -F&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
We will continue the setup process via the web interface after setting up a bare metal node.&lt;br /&gt;
&lt;br /&gt;
===Bare metal node setup===&lt;br /&gt;
You should set up at least one bare metal node which will be used to set up your first zone and pod.&lt;br /&gt;
&lt;br /&gt;
On a bare metal node, set up everything outlined in the [[CloudStack#Node setup|Node setup]] section above. The node should have the CloudStack repos, Open vSwitch, SElinux/firewalld, and the networking configured. The agent node must have virtualization enabled on the CPU and KVM should be installed. You should be able to find &amp;lt;code&amp;gt;/dev/kvm&amp;lt;/code&amp;gt; on the system.&lt;br /&gt;
&lt;br /&gt;
====CloudStack Agent====&lt;br /&gt;
To set up the node, install the &amp;lt;code&amp;gt;cloudstack-agent&amp;lt;/code&amp;gt; package.&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # yum -y install cloudstack-agent&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Configure &amp;lt;code&amp;gt;qemu&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;libvirtd&amp;lt;/code&amp;gt;. If you need some starting configs, try the following:{{Highlight&lt;br /&gt;
| code = ## edit /etc/libvirt/qemu.conf &lt;br /&gt;
vnc_listen=0.0.0.0&lt;br /&gt;
&lt;br /&gt;
## edit /etc/libvirt/libvirtd.conf&lt;br /&gt;
listen_tcp = 1&lt;br /&gt;
tcp_port = &amp;quot;16509&amp;quot;&lt;br /&gt;
listen_tls = 0&lt;br /&gt;
tls_port = &amp;quot;16514&amp;quot;&lt;br /&gt;
auth_tcp = &amp;quot;none&amp;quot;&lt;br /&gt;
mdns_adv = 0&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}The CloudStack install guide instructs you to edit the libvirtd arguments to &amp;lt;code&amp;gt;--listen&amp;lt;/code&amp;gt;, but this will prevent libvirtd from starting using systemd. Instead, you should skip this step entirely because the CloudStack agent will configure this for you when you add the node to a zone.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ## The install guide suggests editing /etc/sysconfig/libvirtd to use the listen flag.&lt;br /&gt;
## However, this only works if you&#039;re not using systemd or using the libvirtd-tcp socket.&lt;br /&gt;
## I skipped this step since the agent will configure this later on.&lt;br /&gt;
LIBVIRTD_ARGS=&amp;quot;--listen&amp;quot;&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}Start the CloudStack agent. Verify that the cloudstack-agent is running. At this point libvirtd should also running (it&#039;s a service dependency). &lt;br /&gt;
&lt;br /&gt;
With the agent running on the node, you should now be able to add the node to the CloudStack cluster. This process will rewrite the &amp;lt;code&amp;gt;libvirtd.conf&amp;lt;/code&amp;gt; file and it should set &amp;lt;code&amp;gt;listen_tcp=0&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;listen_tls=1&amp;lt;/code&amp;gt; for you (so that libvirt traffic such as migrations are done via TLS rather than basic TCP). &lt;br /&gt;
&lt;br /&gt;
====Allow sudo access====&lt;br /&gt;
Ensure that &amp;lt;code&amp;gt;/etc/sudoers&amp;lt;/code&amp;gt; does not require TTY. In the older documentation, CloudStack requires that the &#039;cloud&#039; user be able to sudo with the addition of &amp;lt;code&amp;gt;Defaults:cloud !requiretty&amp;lt;/code&amp;gt;. However, looking at the installation on the CentOS 8 box, the agent actually runs as root, so perhaps root needs to be able to sudo? &lt;br /&gt;
&lt;br /&gt;
===Setting up your first zone===&lt;br /&gt;
At this point in the process, you should have at least one bare metal host and your management node should be up and running and it should be serving the CloudStack web UI at http://cloudstack:8080/client. Login using the default &amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;password&amp;lt;/code&amp;gt; credentials.&lt;br /&gt;
&lt;br /&gt;
You will be greeted with a setup wizard. I have had no luck with this and it&#039;s better to ignore it. Instead, navigate to Infrastructure -&amp;gt; zones and manually set up your first zone. &lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Description&lt;br /&gt;
!Screenshot&lt;br /&gt;
|-&lt;br /&gt;
|There are 3 types of zones that you can create:&lt;br /&gt;
&lt;br /&gt;
#&#039;&#039;&#039;Basic zone&#039;&#039;&#039; - All guest VMs are placed on a single shared flat network. There is no isolation or security policies in place to prevent guest VMs from seeing each other.&lt;br /&gt;
#&#039;&#039;&#039;Advanced zone&#039;&#039;&#039; - Guest VMs can be placed in one or more VLAN based networks. Guest networks can either be isolated or L2. Isolated networks (depending on the chosen network offering) comes with a virtual router (VR) which offers NAT/SNAT and firewall services and uses one or more public IP addresses. L2 networks are similar but doesn&#039;t have a virtual router but instead requires these services to be offered externally.  Tenants can also create something called a virtual private cloud (VPC). A VPC is like a regular isolated guest network but with additional features. A VPC allows the user to:&lt;br /&gt;
##Create multiple subnets (called tiers) which can route with each other&lt;br /&gt;
##Network traffic between tiers can be controlled through Network ACLs&lt;br /&gt;
##One or more public IPs can be associated to a VPC.&lt;br /&gt;
##Like an isolated guest network, all subnets can be NATed out through a single public IP&lt;br /&gt;
##You can create a private gateway (and therefore static routes) within a VPC&lt;br /&gt;
##You can create a VPN connection to a VPC&lt;br /&gt;
#&#039;&#039;&#039;Advanced zone with security groups&#039;&#039;&#039; - Guest VMs are placed on a shared network that is publicly routable. There is no concept of a &#039;public&#039; network because the guest network should also be public. As a result, there is no ability to create any other kind of guest networks or VPCs. The only benefit here is the ability to define security groups per-VM (which is implemented via IPTables on the bare metal host). Because enabling security groups in a zone will restrict that zone from being able to create isolated guest networks or VPCs, the security group feature only appears useful in an environment where guests only need to connect to the internet.&lt;br /&gt;
&lt;br /&gt;
Be aware of each type&#039;s limitations before continuing.&lt;br /&gt;
&lt;br /&gt;
We will be creating an advanced network zone.&lt;br /&gt;
|[[File:CloudStack - New Zone 1.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|We will add the DNS resolvers for the zone and specify the hypervisor type (KVM). &lt;br /&gt;
Empty the guest CIDR since we&#039;re going to allow users to specify their own.&lt;br /&gt;
|[[File:CloudStack - New Zone 2.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|When using the advanced zone, you need to specify the physical networks for the management, storage, and public networks. &lt;br /&gt;
These should correspond to the physical network devices on the hypervisor. Recall that in the previous step where we set up the Open vSwitch bridges, we created the following bridges for each role:&lt;br /&gt;
&lt;br /&gt;
*management - management0&lt;br /&gt;
*storage - storage0&lt;br /&gt;
*public - cloudbr0&lt;br /&gt;
*guest - cloudbr1&lt;br /&gt;
|&amp;lt;gallery&amp;gt;&lt;br /&gt;
File:CloudStack - New Zone 3.png&lt;br /&gt;
File:CloudStack - New Zone 3a.png&lt;br /&gt;
&amp;lt;/gallery&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|Specify the public network. The addresses defined here populates the &#039;Public IP&#039; pool.&lt;br /&gt;
&lt;br /&gt;
All isolated guest networks and all VPCs will use one of the addresses defined in this pool for the SNAT/NAT. The addresses specified here should therefore be accessible from the internet.&lt;br /&gt;
|[[File:CloudStack - New Zone 4.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|Create a new pod. &lt;br /&gt;
&lt;br /&gt;
The pod network here should cover your management network subnet. The reserved IP addresses here will be used by system VMs that require access to the management network. &lt;br /&gt;
|[[File:CloudStack - New Zone 5.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|Specify the guest network VLAN range.&lt;br /&gt;
&lt;br /&gt;
Because we&#039;re using VLAN as an isolation method, this range specifies what VLANs the guest networks will use over the guest physical network.&lt;br /&gt;
|[[File:CloudStack - New Zone 6.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|Specify the storage network.&lt;br /&gt;
&lt;br /&gt;
The reserved start/end IPs will be used by system VMs that require access to the primary storage. &lt;br /&gt;
&lt;br /&gt;
If you are assigning static IPs on your bare metal hosts, ensure that the reserved addresses don&#039;t overlap with the IP range specified here (because I had CloudStack assign a VM with the same IP as a bare metal host)&lt;br /&gt;
|[[File:CloudStack - New Zone 7.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|Specify a cluster name.&lt;br /&gt;
|[[File:CloudStack - New Zone 8.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|Add your first bare metal host. &lt;br /&gt;
You must add one host now and can add additional ones later.&lt;br /&gt;
|[[File:CloudStack - New Zone 9.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|Specify your primary storage.&lt;br /&gt;
The server should be accessible from the storage network.&lt;br /&gt;
|[[File:CloudStack - New Zone 10.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|Specify your secondary storage.&lt;br /&gt;
&lt;br /&gt;
You need to have at least one NFS secondary storage that has been seeded with the system VM template. &lt;br /&gt;
&lt;br /&gt;
Secondary storage pools should be accessible from the management network (&#039;&#039;&#039;confirm&#039;&#039;&#039;?)&lt;br /&gt;
|[[File:CloudStack - New Zone 11.png|left|thumb]]&lt;br /&gt;
|-&lt;br /&gt;
|Launch the zone.&lt;br /&gt;
This step might take a few minutes. If all goes well, you can then enable the zone shortly after. If you run into any problems, check the logs on the management node at /var/log/cloudstack/management.&lt;br /&gt;
|[[File:CloudStack - New Zone 12.png|left|thumb]]&lt;br /&gt;
|}&lt;br /&gt;
Once your zone has been enabled, it should automatically start a Console Proxy VM and secondary storage VM. You can find this under Infrastructure -&amp;gt; System VMs. If for some reason the System VMs are not starting, check that your systemvm template is available in your secondary storage and that the cloud0 bridge on each host is up. You should be able to ping the link local IP address (the 169.254.x.x address) from the hypervisor.&lt;br /&gt;
&lt;br /&gt;
Once the two system VMs are running, verify that you&#039;re able to create new guest networks or VPCs. These networks should create a virtual router. &lt;br /&gt;
&lt;br /&gt;
==Configuration==&lt;br /&gt;
&lt;br /&gt;
===Service offerings===&lt;br /&gt;
&lt;br /&gt;
====Deployment planner====&lt;br /&gt;
There are a few deployment techniques that can be used. These are set within a compute offering and cannot be changed after it&#039;s been created (&#039;&#039;&#039;really?&#039;&#039;&#039; can we change it via API?). The options are:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Deployment planner&lt;br /&gt;
!Description&lt;br /&gt;
|-&lt;br /&gt;
|First fit&lt;br /&gt;
|Placed on the first host that has sufficient capacity&lt;br /&gt;
|-&lt;br /&gt;
|User dispersing&lt;br /&gt;
|Evenly distributes VMs by account across clusters&lt;br /&gt;
|-&lt;br /&gt;
|User concentrated&lt;br /&gt;
|Opposite of the above.&lt;br /&gt;
|-&lt;br /&gt;
|Implicit dedication&lt;br /&gt;
|requires or prefers (depending on planner mode) a dedicated host&lt;br /&gt;
|-&lt;br /&gt;
|Bare metal&lt;br /&gt;
|requires a bare metal host&lt;br /&gt;
|}&lt;br /&gt;
More information from [https://docs.cloudstack.apache.org/en/4.15.2.0/adminguide/service_offerings.html#compute-and-disk-service-offerings CloudStack&#039;s documentation on Compute and Disk Service Offerings].&lt;br /&gt;
&lt;br /&gt;
===Enable SAML2 authentication===&lt;br /&gt;
Enable the SAML2 plugin by setting &amp;lt;code&amp;gt;saml2.enabled=true&amp;lt;/code&amp;gt; under Global Settings. &lt;br /&gt;
&lt;br /&gt;
Set up SAML authentication by specifying the following settings:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Setting&lt;br /&gt;
!Description&lt;br /&gt;
!Example value&lt;br /&gt;
|-&lt;br /&gt;
|saml2.default.idpid&lt;br /&gt;
|The URL of the identity provider. This is likely obtained from the metadata URL and set by the SAML2 plugin every time CloudStack starts.&lt;br /&gt;
|&amp;lt;nowiki&amp;gt;https://sts.windows.net/c609a0ec-xxx-xxx-xxx-xxxxxxxxxxxx/&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|saml2.idp.metadata.url&lt;br /&gt;
|The metadata XML URL&lt;br /&gt;
|&amp;lt;nowiki&amp;gt;https://login.microsoftonline.com/609a0ec-xxx-xxx-xxx-xxxxxxxxxxxx/federationmetadata/2007-06/federationmetadata.xml?appid=c5b8df24-xxx-xxx-xxx-xxxxxxxxxxxx&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|saml2.sp.id&lt;br /&gt;
|The identifier string for this application&lt;br /&gt;
|cloudstack-test.my-organization.tld&lt;br /&gt;
|-&lt;br /&gt;
|saml2.redirect.url&lt;br /&gt;
|The redirect URL using your cloudstack domain.&lt;br /&gt;
|&amp;lt;nowiki&amp;gt;https://cloudstack-test.my-organization.tld/client&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|saml2.user.attribute&lt;br /&gt;
|The attribute to use as the username. &lt;br /&gt;
If you&#039;re not sure what&#039;s available, look at the management logs after a login attempt.&lt;br /&gt;
|For Azure AD, use the email address attribute: &amp;lt;nowiki&amp;gt;http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
Restart the management server. To allow a user access, create the user and enable SSO. The user&#039;s username must match the value that&#039;s obtained from the saml2.user.attribute field.&lt;br /&gt;
&lt;br /&gt;
====Bugs====&lt;br /&gt;
&lt;br /&gt;
=====SAML Request being rejected by Azure AD=====&lt;br /&gt;
If you are using Azure AD, you may have issues authenticating because the SAML request ID that&#039;s generated might begin with a number. When this happens, you will get an error similar to: &amp;lt;code&amp;gt;AADSTS7500529: The value &#039;692rv91k6dgmdas33vr3b2keahr4lqjv&#039; is not a valid SAML ID. The ID must not begin with a number.&amp;lt;/code&amp;gt;. For more information, see: https://github.com/apache/cloudstack/issues/5548&lt;br /&gt;
&lt;br /&gt;
=====Users cannot login via SSO=====&lt;br /&gt;
Users that will be using SAML for authentication will need to have their CloudStack accounts created with SSO enabled. There seems to be a bug with the CloudStack web UI where a user&#039;s SAML IdPID isn&#039;t settable (it gets set to a &#039;0&#039;). A work-around would be to create and authorize users via CloudMonkey.&lt;br /&gt;
&lt;br /&gt;
The steps on adding a new user are:&lt;br /&gt;
&lt;br /&gt;
#Create the user:  &amp;lt;code&amp;gt;create user firstname=First lastname=User email=user1@ucalgary.ca username=user1@ucalgary.ca account=RCS state=enabled password=asdf&amp;lt;/code&amp;gt;&lt;br /&gt;
#Find the user&#039;s ID: &amp;lt;code&amp;gt;list users domainid=&amp;lt;tab&amp;gt; filter=username,id&amp;lt;/code&amp;gt;&lt;br /&gt;
#Authorize the user: &amp;lt;code&amp;gt;authorize samlsso enable=true entityid=&amp;lt;nowiki&amp;gt;https://sts.windows.net/c609a0ec-xxx-xxx-xxx-xxxxxxxxxxxx/&amp;lt;/nowiki&amp;gt; userid=user-id&amp;lt;/code&amp;gt;&lt;br /&gt;
#Verify that the user is enabled for SSO: &amp;lt;code&amp;gt;list samlauthorization filter=userid,idpid,status&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
When authorizing a user, the entityid must be the URL of the identity provider. The end slash is also mandatory.&lt;br /&gt;
&lt;br /&gt;
===Enable SSL===&lt;br /&gt;
A few things to note about enabling SSL:&lt;br /&gt;
&lt;br /&gt;
*If you added hosts via IP address, enabling SSL would likely break the management-to-client connection. You might need to re-add the host so that the certificates all match up.&lt;br /&gt;
* On CloudStack 4.16, the button to upload a new certificate in the SSL dialog box does not work. This is fixed in 4.16.1.&lt;br /&gt;
&lt;br /&gt;
==== Preparing your SSL certificates ====&lt;br /&gt;
First, generate a private key and certificate signing request and then obtain your SSL certificate from a certificate authority. For a typical CloudStack installation, you should obtain SSL certificates for both your management server as well as your console proxy.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # openssl genrsa -out server.key 4096&lt;br /&gt;
# openssl req -new -sha256 \&lt;br /&gt;
         -key server.key \&lt;br /&gt;
         -subj &amp;quot;/C=CA/ST=Alberta/O=Steamr/CN=cloudstack-test.example.com&amp;quot; \&lt;br /&gt;
         -reqexts SAN \&lt;br /&gt;
         -extensions SAN \&lt;br /&gt;
         -config &amp;lt;(cat /etc/pki/tls/openssl.cnf &amp;lt;(printf &amp;quot;[SAN]\nsubjectAltName=DNS:cloudstack-test-console.example.com&amp;quot;)) \&lt;br /&gt;
         -out server.csr&lt;br /&gt;
## With the server.csr file, upload it to your Certificate Authority to obtained a signed certificate.&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Your certificate authority should have given you your signed certificate as well as the root and any other intermediate certificates in a X.509 (.crt) format. If you need to self sign this certificate signing request, do the following:{{Highlight&lt;br /&gt;
| code = ## Run the following only if you want to self sign your certificate&lt;br /&gt;
## Make your root CA&lt;br /&gt;
# openssl genrsa -des3 -out rootCA.key 4096&lt;br /&gt;
# openssl req -x509 -new -subj &amp;quot;/C=CA/ST=Alberta/O=Steamr/CN=example.com&amp;quot; -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.crt&lt;br /&gt;
&lt;br /&gt;
## Sign the certificate&lt;br /&gt;
# openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out server.crt -days 500 -sha256&lt;br /&gt;
&lt;br /&gt;
## Check the certificate&lt;br /&gt;
# openssl x509 -in server.crt -text -noout&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}Next, you need to convert your certificate into a PKCS12 format and your private key into a PKCS8 format. This is the only format that works with the CloudStack management server. We place the PKCS12 keystore file at &amp;lt;code&amp;gt;/etc/cloudstack/management/ssl_keystore.pkcs12&amp;lt;/code&amp;gt;.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ## Combine Files&lt;br /&gt;
# cat server.key server.crt intermediate.crt root.crt &amp;gt; combined.crt&lt;br /&gt;
&lt;br /&gt;
## Create keystore&lt;br /&gt;
## You may use &#039;password&#039; as the password&lt;br /&gt;
# openssl pkcs12 -in combined.crt -export -out combined.pkcs12&lt;br /&gt;
&lt;br /&gt;
## Import keystore&lt;br /&gt;
## Provide the same password above. Eg. &#039;password&#039;&lt;br /&gt;
# keytool -importkeystore -srckeystore combined.pkcs12 -srcstoretype PKCS12 -destkeystore /etc/cloudstack/management/ssl_keystore.pkcs12 -deststoretype pkcs12&lt;br /&gt;
&lt;br /&gt;
## Convert the private key into PKCS8 format&lt;br /&gt;
## Provide the same password above. Eg. &#039;password&#039;&lt;br /&gt;
# openssl pkcs8 -topk8 -in server.key -out server.pkcs8.encrypted.key&lt;br /&gt;
# openssl pkcs8 -in server.pkcs8.encrypted.key -out server.pkcs8.key&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Upload SSL certificates ====&lt;br /&gt;
You can upload SSL certificates to CloudStack under Infrastructure -&amp;gt; Summary and then clicking on the &#039;SSL Certificates&amp;quot; button. Provide the root certificate authority, the certificate, the private key (in PKCS8 format), and the domain that the certificate applies to. Wildcard domains should be specified as &amp;lt;code&amp;gt;*.example.com&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
Alternatively, you may use the CloudMonkey tool to upload certificates using the file parameter passing feature like so:&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # cmk upload customcertificate domainsuffix=cloudstack.steamr.com id=1 name=root certificate=@root.crt&lt;br /&gt;
# cmk upload customcertificate domainsuffix=cloudstack.steamr.com id=2 name=intermediate1 certificate=@intermediate.crt&lt;br /&gt;
# cmk upload customcertificate domainsuffix=cloudstack.steamr.com id=3 privatekey=@server.pkcs8.key certificate=@domain.crt&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Enabling HTTPS ====&lt;br /&gt;
Next, you will need to enable HTTPS on both the management console and console proxy.&lt;br /&gt;
&lt;br /&gt;
Note that you may enable the HTTPS setting only after at least one certificate has been uploaded. If the server has no certificates, the option is ignored.&lt;br /&gt;
&lt;br /&gt;
===== Enable HTTPS on the management console =====&lt;br /&gt;
The management console can be configured by editing &amp;lt;code&amp;gt;/etc/cloudstack/management/server.properties&amp;lt;/code&amp;gt; with the following lines. Set the keystore password to the same password you used above to import it.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = https.enable=true&lt;br /&gt;
https.port=8443&lt;br /&gt;
https.keystore=/etc/cloudstack/management/ssl_keystore.pkcs12&lt;br /&gt;
https.keystore.password=password&lt;br /&gt;
| lang = text&lt;br /&gt;
}}Restart the management server for this to apply.&lt;br /&gt;
{{Info&lt;br /&gt;
| title = Why port 8443?&lt;br /&gt;
| message = Because CloudStack runs under a non-root account, it can only bind to high port (&amp;gt; 1024) numbers. &lt;br /&gt;
&lt;br /&gt;
You can still have CloudStack visible on port 443 if you use a [[IPTables#Mapping incoming traffic to a different internal port|IPTables rule]].&lt;br /&gt;
}}&lt;br /&gt;
Confirm that you are able to reach your management console via HTTPS.&lt;br /&gt;
&lt;br /&gt;
==== Enable HTTPS on the console proxy ====&lt;br /&gt;
If you enable SSL on the management console, you will also need to enable SSL for the console proxies for the VNC web sockets to work properly. If your management console certificate (from the previous sections) contain a Subject Alternative Name (SAN) or is a wildcard certificate that includes your console proxy&#039;s DNS name, SSL for the console proxy should be working. If your certificates do not include the console proxy&#039;s DNS name, you will need to obtain another SSL certificate and add it to the SSL keystore and upload it to CloudStack using the same instructions above.&lt;br /&gt;
&lt;br /&gt;
==== Renewing SSL certificate ====&lt;br /&gt;
To renew a SSL certificate, you&#039;ll have to ensure that the keystore is updated and also upload the certificate via CloudMonkey or the management console (under Summary -&amp;gt; Certificates).&lt;br /&gt;
&lt;br /&gt;
In a folder containing your certificate (&amp;lt;code&amp;gt;server.crt&amp;lt;/code&amp;gt;), intermediate and root certificates (&amp;lt;code&amp;gt;intermediate.crt&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;root.crt&amp;lt;/code&amp;gt;), and also your private key (&amp;lt;code&amp;gt;server.key&amp;lt;/code&amp;gt;), run the following to update your SSL keystore and upload the certificates via cmk:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # cat server.key server.crt intermediate.crt root.crt &amp;gt; combined.crt&lt;br /&gt;
&lt;br /&gt;
## Note the keystore location that&#039;s defined in your configs&lt;br /&gt;
# grep https.keystore /etc/cloudstack/management/server.properties&lt;br /&gt;
https.keystore=/etc/cloudstack/management/ssl_keystore.pkcs12&lt;br /&gt;
https.keystore.password=xxxxxxx&lt;br /&gt;
&lt;br /&gt;
## Create keystore and import your certificate into it.&lt;br /&gt;
# openssl pkcs12 -in combined.crt -export -out combined.pkcs12&lt;br /&gt;
# keytool -importkeystore -srckeystore combined.pkcs12 -srcstoretype PKCS12 -destkeystore ssl_keystore.pkcs12 -deststoretype pkcs12&lt;br /&gt;
&lt;br /&gt;
## Move the keystore over the existing one as defined in the server config. You may want to backup the old one just in case.&lt;br /&gt;
# mv /etc/cloudstack/management/ssl_keystore.pkcs12 /etc/cloudstack/management/ssl_keystore.pkcs12-org&lt;br /&gt;
# mv ssl_keystore.pkcs12 /etc/cloudstack/management/ssl_keystore.pkcs12&lt;br /&gt;
&lt;br /&gt;
## Convert your key to pkcs8 if you haven&#039;t already done so. Use the same password for both commands.&lt;br /&gt;
# openssl pkcs8 -topk8 -in server.key -out server.pkcs8.key-encrypted&lt;br /&gt;
# openssl pkcs8 -in server.pkcs8.key-encrypted -out server.pkcs8.key&lt;br /&gt;
&lt;br /&gt;
## Upload your certificate&lt;br /&gt;
# for domain in $(openssl x509 -in server.crt -text -noout {{!}} grep DNS: {{!}} tr -d , {{!}} sed &#039;s/DNS://g&#039;) ; do&lt;br /&gt;
        echo &amp;quot;Uploading domain for $domain&amp;quot;&lt;br /&gt;
&lt;br /&gt;
        cmk upload customcertificate domainsuffix=$domain id=1 name=root certificate=@root.crt&lt;br /&gt;
        cmk upload customcertificate domainsuffix=$domain id=2 name=intermediate1 certificate=@intermediate.crt&lt;br /&gt;
        cmk upload customcertificate domainsuffix=$domain id=3 privatekey=@server.pkcs8.key certificate=@server.crt&lt;br /&gt;
done&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Tasks ==&lt;br /&gt;
===Re-add existing KVM bare metal host===&lt;br /&gt;
Once a host has been added to CloudStack, the CloudStack agent will have generated some public/private keys and configured itself to talk to the management node.  If you need to remove and re-add a host, you will need to clean up the agent before re-adding it back to CloudStack again. Based on my experience, I had to do the following:&lt;br /&gt;
&lt;br /&gt;
#Before removing the host from CloudStack, drain it of all VMs. &amp;lt;code&amp;gt;virsh list&amp;lt;/code&amp;gt; should be empty. If not and you&#039;ve removed the host from the management server already, manually kill each VM with &amp;lt;code&amp;gt;virsh destroy&amp;lt;/code&amp;gt;.&lt;br /&gt;
#&amp;lt;code&amp;gt;systemctl stop cloudstack-agent&amp;lt;/code&amp;gt;&lt;br /&gt;
#&amp;lt;code&amp;gt;rm -rf /etc/cloudstack/agent/cloud*&amp;lt;/code&amp;gt;&lt;br /&gt;
#unmount any primary storages with &amp;lt;code&amp;gt;umount /mnt/*&amp;lt;/code&amp;gt; and clean up with &amp;lt;code&amp;gt;rmdir /mnt/*&amp;lt;/code&amp;gt;&lt;br /&gt;
#&amp;lt;code&amp;gt;systemctl stop libvirtd&amp;lt;/code&amp;gt;&lt;br /&gt;
#&amp;lt;code&amp;gt;rm -rf /var/lib/libvirt/qemu&amp;lt;/code&amp;gt;&lt;br /&gt;
#You may need to edit &amp;lt;code&amp;gt;/etc/sysconfig/libvirtd&amp;lt;/code&amp;gt; to not use the listen flag. This might prevent libvirtd (and subsequently cloudstack-agent) from starting.&lt;br /&gt;
#Edit &amp;lt;code&amp;gt;/etc/cloudstack/agent/agent.properties&amp;lt;/code&amp;gt; and remove the keystore passphrase, any UUIDs, cluster/pod/zone, and the host. You should keep the guid or regenerate it with uuidgen. You should also keep the public/private/guest network devices set.&lt;br /&gt;
#Restart with &amp;lt;code&amp;gt;systemctl start cloudstack-agent&amp;lt;/code&amp;gt; (libvirt should come up automatically as it&#039;s a dependency). Ensure that it comes up OK.&lt;br /&gt;
&lt;br /&gt;
You may then re-add the host back to CloudStack.&lt;br /&gt;
&lt;br /&gt;
===Building RPMs===&lt;br /&gt;
To build the RPM packages from scratch, you&#039;ll need to install a bunch of dependencies and then run the build script. For more information, see:&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;nowiki&amp;gt;https://docs.cloudstack.apache.org/en/4.15.2.0/installguide/building_from_source.html#building-rpms-from-source&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # yum groupinstall &amp;quot;Development Tools&amp;quot;&lt;br /&gt;
# yum install java-11-openjdk-devel genisoimage mysql mysql-server createrepo&lt;br /&gt;
# yum install epel-release&lt;br /&gt;
&lt;br /&gt;
# curl -sL https://rpm.nodesource.com/setup_12.x {{!}} sudo bash -&lt;br /&gt;
# yum install nodejs&lt;br /&gt;
&lt;br /&gt;
# cat &amp;lt;&amp;lt;EOF &amp;gt; /etc/yum.repos.d/mysql.repo&lt;br /&gt;
[mysql-community]&lt;br /&gt;
name=MySQL Community connectors&lt;br /&gt;
baseurl=http://repo.mysql.com/yum/mysql-connectors-community/el/$releasever/$basearch/&lt;br /&gt;
gpgkey=http://repo.mysql.com/RPM-GPG-KEY-mysql&lt;br /&gt;
enabled=1&lt;br /&gt;
gpgcheck=1&lt;br /&gt;
EOF&lt;br /&gt;
# yum -y install mysql-connector-python&lt;br /&gt;
&lt;br /&gt;
enable powertools&lt;br /&gt;
&lt;br /&gt;
# yum install jpackage-utils maven&lt;br /&gt;
&lt;br /&gt;
# git clone https://github.com/apache/cloudstack.git&lt;br /&gt;
# cd cloudstack&lt;br /&gt;
# git checkout 4.15&lt;br /&gt;
&lt;br /&gt;
# cd packaging&lt;br /&gt;
# sh package.sh --distribution centos8&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Rebuilding UI ===&lt;br /&gt;
Instructions to rebuild the UI are available on the [https://github.com/apache/cloudstack/tree/main/ui README file under the ui directory.]&lt;br /&gt;
&lt;br /&gt;
In summary, to rebuild the CloudStack UI, such as to test a new feature or bug fix, you must compile the VueJS + Ant design files and place have the CloudStack management server serve these files. This is accomplished by doing the following:&lt;br /&gt;
&lt;br /&gt;
# On a server with npm installed, clone the CloudStack git repo and checkout the branch with the UI fix/feature&lt;br /&gt;
# Navigate to &amp;lt;code&amp;gt;cloudstack/ui/&amp;lt;/code&amp;gt;&lt;br /&gt;
# Run &amp;lt;code&amp;gt;npm install&amp;lt;/code&amp;gt;&lt;br /&gt;
# Run &amp;lt;code&amp;gt;npm run build&amp;lt;/code&amp;gt;&lt;br /&gt;
# Copy the &amp;lt;code&amp;gt;dist/&amp;lt;/code&amp;gt; directory to &amp;lt;code&amp;gt;/usr/share/cloudstack-management/webapp/&amp;lt;/code&amp;gt;&lt;br /&gt;
# Edit &amp;lt;code&amp;gt;/etc/cloudstack/management/server.properties&amp;lt;/code&amp;gt; and make sure that &amp;lt;code&amp;gt;webapp.dir&amp;lt;/code&amp;gt; is set to: &amp;lt;code&amp;gt;webapp.dir=/usr/share/cloudstack-management/webapp&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Restart the CloudStack management service and then reload the console page. Ensure that the vue app isn&#039;t cached.&lt;br /&gt;
&lt;br /&gt;
==== Installing and using CloudStack&#039;s prebuilt UI ====&lt;br /&gt;
The cloudstack-ui package contains the prebuilt CloudStack UI. The location of the UI files are placed under &amp;lt;code&amp;gt;/usr/share/cloudstack-ui&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
As of CloudStack 4.18, when I tried using this prebuilt package, I had to do the following things:&lt;br /&gt;
&lt;br /&gt;
* Edit &amp;lt;code&amp;gt;/etc/cloudstack/management/server.properties&amp;lt;/code&amp;gt; and set &amp;lt;code&amp;gt;webapp.dir=/usr/share/cloudstack-ui&amp;lt;/code&amp;gt;&lt;br /&gt;
* In &amp;lt;code&amp;gt;/usr/share/cloudstack-ui&amp;lt;/code&amp;gt;, run: &amp;lt;code&amp;gt;find . -type d -exec chmod -v o+x {} \;&amp;lt;/code&amp;gt; because the directories aren&#039;t executable by the &#039;cloud&#039; user.&lt;br /&gt;
* Create /usr/share/cloudstack-ui/WEB-INF and place this [https://github.com/apache/cloudstack/blob/main/client/src/main/webapp/WEB-INF/web.xml web.xml file] within. Otherwise, requests to the API break.&lt;br /&gt;
&lt;br /&gt;
===Usage server===&lt;br /&gt;
Install &amp;lt;code&amp;gt;cloudstack-usage&amp;lt;/code&amp;gt;. Start it and restart the management server. Set &amp;lt;code&amp;gt;enable.usage.server=true&amp;lt;/code&amp;gt; in global settings.&lt;br /&gt;
&lt;br /&gt;
The usage data will be stored in the usage database on your management server. Metrics are gathered daily and can be viewed through Cloud Monkey. There is no option to view this data in the management console.&lt;br /&gt;
&lt;br /&gt;
The collected data is coarse in nature, but it should be sufficient enough for you to determine an account or VM&#039;s resource utilization over a time period of a day or more and should be good enough to implement a rough billing / showback amount.&lt;br /&gt;
&lt;br /&gt;
===Adding some Linux templates===&lt;br /&gt;
You can add the &amp;quot;Generic Cloud&amp;quot; qcow2 disk images as a system template to CloudStack. &lt;br /&gt;
&lt;br /&gt;
Because these cloud images uses cloud-init, you will need to provide some custom userdata when deploying these images. Userdata will only work when the VM is deployed on a network that offers the &amp;quot;User Data&amp;quot; service offering. If you can&#039;t use userdata or if you want the VMs to come up with a specific root password, you can use [[virt-customize]] to set the root password on the qcow2 file.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Distro&lt;br /&gt;
!Type&lt;br /&gt;
!URL&lt;br /&gt;
|-&lt;br /&gt;
|Rocky Linux 8.4&lt;br /&gt;
|CentOS 8&lt;br /&gt;
|https://download.rockylinux.org/pub/rocky/8.4/images/Rocky-8-GenericCloud-8.4-20210620.0.x86_64.qcow2&lt;br /&gt;
|-&lt;br /&gt;
|CentOS 8.4&lt;br /&gt;
|CentOS 8&lt;br /&gt;
|https://cloud.centos.org/centos/8/x86_64/images/CentOS-8-GenericCloud-8.4.2105-20210603.0.x86_64.qcow2&lt;br /&gt;
|-&lt;br /&gt;
|Fedora 34&lt;br /&gt;
|Fedora Linux (64 bit)&lt;br /&gt;
|https://download.fedoraproject.org/pub/fedora/linux/releases/34/Cloud/x86_64/images/Fedora-Cloud-Base-34-1.2.x86_64.qcow2&lt;br /&gt;
|-&lt;br /&gt;
|Ubuntu Server 21.04&lt;br /&gt;
|&lt;br /&gt;
|http://cloud-images.ubuntu.com/hirsute/current/hirsute-server-cloudimg-amd64.img&lt;br /&gt;
You need to convert img to qcow with qemu-img:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;qemu-img create -F qcow2 -b cloudimg-amd64.img -f qcow2 cloudimg-adm64.qcow2 10G&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
Here&#039;s an example of a cloud-init configuration which you would put in the userdata field when deploying a VM:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = #cloud-config&lt;br /&gt;
hostname: vm01&lt;br /&gt;
manage_etc_hosts: true&lt;br /&gt;
users:&lt;br /&gt;
  - name: vmadm&lt;br /&gt;
    sudo: ALL=(ALL) NOPASSWD:ALL&lt;br /&gt;
    groups: users, admin&lt;br /&gt;
    home: /home/vmadm&lt;br /&gt;
    shell: /bin/bash&lt;br /&gt;
    lock_passwd: false&lt;br /&gt;
ssh_pwauth: true&lt;br /&gt;
disable_root: false&lt;br /&gt;
chpasswd:&lt;br /&gt;
  list: {{!}}&lt;br /&gt;
    vmadm:vmadm&lt;br /&gt;
  expire: false&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Importing a VMware Virtual Machine ===&lt;br /&gt;
To import a VMware virtual machine:&lt;br /&gt;
&lt;br /&gt;
# Copy the virtual machine&#039;s .vmdk disk file to a CloudStack node&lt;br /&gt;
# Convert the .vmdk into a .qcow format using the qemu-img convert command. Eg. &amp;lt;code&amp;gt;qemu-img convert -f linux.vmdk -O linux.qcow2&amp;lt;/code&amp;gt;&lt;br /&gt;
# Run the file command on the qcow disk and make a note of its size (in bytes).&lt;br /&gt;
# Create a new virtual machine in CloudStack. Use an ISO and not a template. Set the size of the VM&#039;s ROOT disk to match the disk size noted from the previous step.&lt;br /&gt;
# Start and stop the VM to ensure the virtual disk is created. Make a note of the virtual disk&#039;s ID.&lt;br /&gt;
# Copy the converted qcow disk over the existing virtual disk image in the primary storage.&lt;br /&gt;
# Restart the VM in CloudStack.&lt;br /&gt;
&lt;br /&gt;
Some things to note with this process:&lt;br /&gt;
&lt;br /&gt;
* The disk subsystem might differ between KVM and VMware. As a result, you may need to [[Rebuilding the initial ramdisk|rebuild the initrd file]] so that it has the necessary drivers to boot properly.&lt;br /&gt;
&lt;br /&gt;
=== Increasing the management console&#039;s timeout ===&lt;br /&gt;
The default timeout is 30 minutes. You may adjust the number of minutes in the &amp;lt;code&amp;gt;session.timeout&amp;lt;/code&amp;gt; value stored in &amp;lt;code&amp;gt;/etc/cloudstack/management/server.properties&amp;lt;/code&amp;gt;.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = session.timeout=60&lt;br /&gt;
| lang = xml&lt;br /&gt;
}}&lt;br /&gt;
Restart the cloudstack-management service to apply.&lt;br /&gt;
&lt;br /&gt;
=== Upgrade CloudStack ===&lt;br /&gt;
Before upgrading CloudStack, review the upgrade instructions from CloudStack&#039;s documentation. For 4.17 to 4.18, see:  https://docs.cloudstack.apache.org/en/4.18.0.0/upgrading/upgrade/upgrade-4.17.html&lt;br /&gt;
&lt;br /&gt;
In a nutshell, upgrading CloudStack for KVM hosts requires the following steps:&lt;br /&gt;
&lt;br /&gt;
# Before upgrading, load the next systemvm template image. System templates are available from: http://download.cloudstack.org/systemvm/. The systemvm template for KVM should be named something like: &amp;lt;code&amp;gt;systemvm-kvm-4.18.0&amp;lt;/code&amp;gt;. When adding the template, specify qcow2 as its format.&lt;br /&gt;
# Backup your CloudStack and usage database. {{Highlight&lt;br /&gt;
| code = $ mysqldump -u root -p -R cloud &amp;gt; cloud-backup_$(date +%Y-%m-%d-%H%M%S)&lt;br /&gt;
$ mysqldump -u root -p cloud_usage &amp;gt; cloud_usage-backup_$(date +%Y-%m-%d-%H%M%S)&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# If you have outstanding system packages to upgrade, do so now (excluding CloudStack packages) and reboot.&lt;br /&gt;
# Stop the CloudStack Management server. Manually unmount any CloudStack mounts. I had to do when I upgraded from 4.16 to 4.17 since it prevented CloudStack from starting. Upgrade the cloudstack-management and cloudstack-common packages. Restart CloudStack. {{Highlight&lt;br /&gt;
| code = # systemctl stop cloudstack-management&lt;br /&gt;
# umount /var/cloudstack/mnt/*&lt;br /&gt;
# yum -y update cloudstack\*&lt;br /&gt;
# systemctl start cloudstack-management&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
# Ensure things are running. Watch the logs in &amp;lt;code&amp;gt;tail -f /var/log/cloud/management/*log&amp;lt;/code&amp;gt;. Verify that the management server can still communicate with the hosts.&lt;br /&gt;
# For each CloudStack host, drain it of hosts, stop the cloudstack-agent service, do a full upgrade, reboot. {{Highlight&lt;br /&gt;
| code = # systemctl stop cloudstack-agent&lt;br /&gt;
# yum -y update cloudstack-agent&lt;br /&gt;
&lt;br /&gt;
## Restart the service or reboot just to make sure the host can come up by itself&lt;br /&gt;
## reboot&lt;br /&gt;
# systemctl restart cloudstack-agent&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Updating the system VMs ====&lt;br /&gt;
Check your list of virtual routers under Infrastructure -&amp;gt; Virtual routers. Update any VMs that are marked with &#039;requires upgrade&#039;. Do so by selecting the VM and clicking on the &#039;upgrade router to use newer template&#039; button.&lt;br /&gt;
&lt;br /&gt;
If the virtual router doesn&#039;t start up properly after performing an upgrade, make sure that the VM is running on a node with an appropriate CloudStack agent version. Virtual routers that land on a node with an older version of the agent won&#039;t start properly.&lt;br /&gt;
&lt;br /&gt;
==== Other upgrade notes ====&lt;br /&gt;
Things to watch out for:&lt;br /&gt;
&lt;br /&gt;
* Don&#039;t upgrade CloudStack packages on a server until you stop the CloudStack services. For some reason, I&#039;ve had issues in the past where something with Java gets corrupted if you try to do an upgrade while the java processes are still running. This then results in some odd class loader issue which results in the service being unable to start after the upgrade.&lt;br /&gt;
* System VM template: I upgraded the CloudStack management server to 4.16.1 using a custom compiled RPM package. However, the management server didn&#039;t start and inspecting the logs show that it was expecting a system VM template at &amp;lt;code&amp;gt;/usr/share/cloudstack-management/templates/systemvm/systemvmtemplate-4.16.1-kvm.qcow2.bz2&amp;lt;/code&amp;gt;. This is easily fixed by downloading the template and restarting the management server.  &amp;lt;code&amp;gt;wget &amp;lt;nowiki&amp;gt;http://download.cloudstack.org/systemvm/4.16/systemvmtemplate-4.16.1-kvm.qcow2.bz2&amp;lt;/nowiki&amp;gt; -O /usr/share/cloudstack-management/templates/systemvm/systemvmtemplate-4.16.1-kvm.qcow2.bz2&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Traefik ===&lt;br /&gt;
&lt;br /&gt;
==== Using Traefik for SSL termination ====&lt;br /&gt;
With the console proxy served using SSL, we could put a reverse proxy in front of both the management UI and the console proxy service VMs with a valid certificate. This allows us to &#039;mask&#039; the self-signed certificate with Traefik&#039;s ability to request for a proper certificate from Let&#039;s Encrypt.&lt;br /&gt;
&lt;br /&gt;
In my test version of CloudStack, I&#039;ve set up Traefik with the following configs. I updated the console proxy to use a dynamic URL by setting &amp;lt;code&amp;gt;consoleproxy.url.domain&amp;lt;/code&amp;gt; to something like &amp;lt;code&amp;gt;*.cloudstack-test.example.com&amp;lt;/code&amp;gt;. CloudStack&#039;s console proxy service will translate the &amp;lt;code&amp;gt;*&amp;lt;/code&amp;gt; by the system VM&#039;s IP address (Eg. 10.1.1.1 becomes 10-1-1-1). We&#039;ll tell Traefik to reverse proxy these domains for both HTTPS and WSS on ports 443 and 8080 respectively. My dynamic traefik configs to make this happen looks like the following:{{Highlight&lt;br /&gt;
| code = http:&lt;br /&gt;
  serversTransports:&lt;br /&gt;
    ignorecert:&lt;br /&gt;
      insecureSkipVerify: true&lt;br /&gt;
&lt;br /&gt;
  routers:&lt;br /&gt;
    cloudstack:&lt;br /&gt;
      rule: Host(`cloudstack-test.example.com`)&lt;br /&gt;
      service: cloudstack-poc&lt;br /&gt;
      entrypoints:&lt;br /&gt;
        - http&lt;br /&gt;
      middlewares:&lt;br /&gt;
        - https-redirect&lt;br /&gt;
&lt;br /&gt;
    cloudstack-https:&lt;br /&gt;
      rule: Host(`cloudstack-test.example.com`)&lt;br /&gt;
      service: cloudstack-poc&lt;br /&gt;
      entrypoints:&lt;br /&gt;
        - https&lt;br /&gt;
      tls:&lt;br /&gt;
        certresolver: letsencrypt&lt;br /&gt;
&lt;br /&gt;
    cloudstack-pub-ip-136-159-1-100:&lt;br /&gt;
      rule: Host(`136-159-1-1.cloudstack-test.example.com`)&lt;br /&gt;
      service: 136-159-1-100&lt;br /&gt;
      entrypoints:&lt;br /&gt;
        - https&lt;br /&gt;
      tls:&lt;br /&gt;
        certresolver: letsencrypt&lt;br /&gt;
&lt;br /&gt;
    cloudstack-pub-ip-136-159-1-100-ws:&lt;br /&gt;
      rule: Host(`136-159-1-1.cloudstack-test.example.com`)&lt;br /&gt;
      service: 136-159-1-100-ws&lt;br /&gt;
      entrypoints:&lt;br /&gt;
        - httpws&lt;br /&gt;
      tls:&lt;br /&gt;
        certresolver: letsencrypt&lt;br /&gt;
		&lt;br /&gt;
  services:&lt;br /&gt;
    cloudstack-poc:&lt;br /&gt;
      loadBalancer:&lt;br /&gt;
        servers:&lt;br /&gt;
          - url: &amp;quot;http://172.19.12.141:8080&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    136-159-1-100:&lt;br /&gt;
      loadBalancer:&lt;br /&gt;
        servers:&lt;br /&gt;
          - url: &amp;quot;https://136.159.1.100&amp;quot;&lt;br /&gt;
        serversTransport: ignorecert&lt;br /&gt;
&lt;br /&gt;
    136-159-1-100-ws:&lt;br /&gt;
      loadBalancer:&lt;br /&gt;
        servers:&lt;br /&gt;
          - url: &amp;quot;https://136.159.1.100:8080&amp;quot;&lt;br /&gt;
        serversTransport: ignorecert&lt;br /&gt;
&lt;br /&gt;
  middlewares:&lt;br /&gt;
    https-redirect:&lt;br /&gt;
      redirectscheme:&lt;br /&gt;
        scheme: https&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}And the following traefik configs:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = entryPoints:&lt;br /&gt;
  http:&lt;br /&gt;
    address: &amp;quot;:80&amp;quot;&lt;br /&gt;
  https:&lt;br /&gt;
    address: &amp;quot;:443&amp;quot;&lt;br /&gt;
  httpws:&lt;br /&gt;
    address: &amp;quot;:8080&amp;quot;&lt;br /&gt;
&lt;br /&gt;
certificatesResolvers:&lt;br /&gt;
  letsencrypt:&lt;br /&gt;
    acme:&lt;br /&gt;
      email: user@example.com&lt;br /&gt;
      storage: &amp;quot;/config/acme.json&amp;quot;&lt;br /&gt;
      httpChallenge:&lt;br /&gt;
        entryPoint: http&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Change guest VM CPU flags ===&lt;br /&gt;
The default CPU flags that guest VMs sees are set to qemu64 compatible features. The qemu64 feature set covers a very small subset of the available features that modern CPUs have which makes the guest VM be compatible to nearly all available CPUs at the cost of reduced features. The feature flags in qemu64 are: &amp;lt;code&amp;gt;fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pse36 clflush mmx fxsr sse sse2 ht syscall nx lm rep_good nopl xtopology cpuid tsc_known_freq pni cx16 x2apic hypervisor lahf_lm cpuid_fault pti&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For virtualized workloads that require additional feature sets, you can edit the CloudStack agent to use a different guest CPU mode. Select one of:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;custom&#039;&#039;&#039;: This is the default and it defaults to the x86_qemu64 feature set defined in &amp;lt;code&amp;gt;/usr/share/libvirt/cpu_map/x86_qemu64.xml&amp;lt;/code&amp;gt;. You may select a different CPU map by specifying &amp;lt;code&amp;gt;guest.cpu.model&amp;lt;/code&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;host-model&#039;&#039;&#039;: Uses a CPU model compatible with your host. Most feature flags are available. Guest CPUs will identify itself as a generic CPU of that family such as &amp;lt;code&amp;gt;Intel Xeon Processor (Icelake)&amp;lt;/code&amp;gt; (note the lack of &#039;(R)&#039; after Intel and Xeon brands and no specific CPU model number).&lt;br /&gt;
* &#039;&#039;&#039;host-passthrough&#039;&#039;&#039;: Use CPU passthrough; feature flags match exactly. Migrations only work with matching CPUs and may still fail when using this mode. Guest CPUs will identify itself as the underlying CPU in that hypervisor (such as &amp;lt;code&amp;gt;Intel(R) Xeon(R) Gold 5320 CPU @ 2.20GHz&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
For CloudStack clusters with identical CPUs, it&#039;s recommended to use host-model. I&#039;ve tried using host-passthrough on matching hosts using a Intel Xeon Silver 4316 and migrations sometimes fail and the VM requires a reset to be brought back up.&lt;br /&gt;
&lt;br /&gt;
For more information, see: http://docs.cloudstack.apache.org/en/4.15.0.0/installguide/hypervisor/kvm.html#install-and-configure-the-agent&lt;br /&gt;
&lt;br /&gt;
To change the CPU mode, you simply need to add the appropriate line into the agent properties file and restart the agent:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ## Matching host model&lt;br /&gt;
# echo &amp;quot;guest.cpu.mode=host-model&amp;quot; &amp;gt;&amp;gt; /etc/cloudstack/agent/agent.properties&lt;br /&gt;
# systemctl restart cloudstack-agent.service&lt;br /&gt;
&lt;br /&gt;
## Passthrough&lt;br /&gt;
# echo &amp;quot;guest.cpu.mode=host-passthrough&amp;quot; &amp;gt;&amp;gt; /etc/cloudstack/agent/agent.properties&lt;br /&gt;
# systemctl restart cloudstack-agent.service&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Using Open vSwitch and DPDK ===&lt;br /&gt;
Getting DPDK working with Open vSwitch is relatively straight forward. You need to install the DPDK packages, configure the kernel to use hugepages and IO passthrough, enable the vfio driver on your network interfaces for DPDK support, reconfigure Open vSwitch to use the DPDK device, and enable DPDK on the CloudStack agent.&lt;br /&gt;
&lt;br /&gt;
There are some existing resources that might help.&lt;br /&gt;
&lt;br /&gt;
*https://www.shapeblue.com/openvswitch-with-dpdk-support-on-cloudstack/&lt;br /&gt;
* https://access.redhat.com/documentation/en-us/red_hat_openstack_platform/10/html/ovs-dpdk_end_to_end_troubleshooting_guide/configure_and_test_lacp_bonding_with_open_vswitch_dpdk&lt;br /&gt;
&lt;br /&gt;
Install DPDK tools:&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # yum -y install dpdk dpdk-tools&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Reconfigure your kernel by editing &amp;lt;code&amp;gt;/etc/default/grub&amp;lt;/code&amp;gt;. Add the following. Adjust the &amp;lt;code&amp;gt;isolcpus&amp;lt;/code&amp;gt; depending on your CPUs available. I assigned 4 cores out of 80 vCPUs. I am also using 16 1GB huge pages. Adjust this according to how much memory your system has (and probably what performance you&#039;re seeing){{Highlight&lt;br /&gt;
| code = # vi /etc/default/grub&lt;br /&gt;
## default_hugepagesz=1GB hugepagesz=1G hugepages=16 iommu=pt intel_iommu=on isolcpus=1-19,21-39,41-59,61-79 intel_pstate=disable nosoftlockup&lt;br /&gt;
&lt;br /&gt;
# grub2-mkconfig -o /boot/grub2/grub.cfg&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}You can also configure huge pages by sysctl (optional if you set it in the kernel cmdline){{Highlight&lt;br /&gt;
| code = # echo &#039;vm.nr_hugepages=16&#039; &amp;gt; /etc/sysctl.d/hugepages.conf&lt;br /&gt;
# sysctl -w vm.nr_hugepages=16&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}Load the &amp;lt;code&amp;gt;vfio-pci&amp;lt;/code&amp;gt; kernel module on boot&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # echo vfio-pci &amp;gt; /etc/modules-load.d/vfio-pci.conf&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Reboot the machine. When it comes back, verify that you have hugepages and vfio-pci loaded, and that IOMMU is working.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # cat /proc/cmdline {{!}} grep iommu=pt&lt;br /&gt;
# cat /proc/cmdline {{!}} grep intel_iommu=on&lt;br /&gt;
# dmesg {{!}} grep -e DMAR -e IOMMU&lt;br /&gt;
# grep HugePages_ /proc/meminfo&lt;br /&gt;
# lsmod {{!}} grep vfio-pci&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Set the network interfaces you wish to use DPDK on to the vfio-pci driver. This is done using the &amp;lt;code&amp;gt;dpdk-devbind.py&amp;lt;/code&amp;gt; script that&#039;s provided by the DPDK tools package.{{Highlight&lt;br /&gt;
| code = # modprobe vfio-pci&lt;br /&gt;
# dpdk-devbind.py --bind=vfio-pci ens2f0&lt;br /&gt;
# dpdk-devbind.py --bind=vfio-pci ens2f1&lt;br /&gt;
## Verify&lt;br /&gt;
# dpdk-devbind.py --status&lt;br /&gt;
&lt;br /&gt;
Network devices using DPDK-compatible driver&lt;br /&gt;
============================================&lt;br /&gt;
0000:31:00.0 &#039;Ethernet Controller X710 for 10GBASE-T 15ff&#039; drv=vfio-pci unused=i40e&lt;br /&gt;
0000:31:00.1 &#039;Ethernet Controller X710 for 10GBASE-T 15ff&#039; drv=vfio-pci unused=i40e&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}Enable DPDK on Open vSwitch. pmd-cpu-mask defines what cores are used for data path packet processing. The dpdk-lcore-mask defines cores that non-datapath OVS-DPDK threads such as handler and revalidator threads run. These two masks should not overlap. For more information on these parameters, see: https://developers.redhat.com/blog/2017/06/28/ovs-dpdk-parameters-dealing-with-multi-numa&amp;lt;nowiki/&amp;gt;.{{Highlight&lt;br /&gt;
| code = &lt;br /&gt;
# ovs-vsctl --no-wait set Open_vSwitch . other_config:dpdk-init=true&lt;br /&gt;
# ovs-vsctl --no-wait set Open_vSwitch . other_config:dpdk-lcore-mask=0x00000001  &lt;br /&gt;
# ovs-vsctl --no-wait set Open_vSwitch . other_config:pmd-cpu-mask=0x17c0017c                   &lt;br /&gt;
# ovs-vsctl --no-wait set Open_vSwitch . other_config:dpdk-socket-mem=&amp;quot;1024&amp;quot;&lt;br /&gt;
&lt;br /&gt;
## Verify&lt;br /&gt;
# ovs-vsctl get Open_vSwitch . dpdk_initialized&lt;br /&gt;
# ovs-vsctl get Open_vSwitch . dpdk_version&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}If Open vSwitch is already configured to use these interfaces by name, you will just need to change the interface type to dpdk and set its PCI address.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ovs-vsctl set interface ens2f0 type=dpdk&lt;br /&gt;
# ovs-vsctl set interface ens2f0 options:dpdk-devargs=0000:31:00.0&lt;br /&gt;
# ovs-vsctl set interface ens2f1 type=dpdk&lt;br /&gt;
# ovs-vsctl set interface ens2f1 options:dpdk-devargs=0000:31:00.1&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
The bridge that these interfaces are connected to must also have its datapath_type updated:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ovs-vsctl set bridge nic0 datapath_type=netdev&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Restart Open vSwitch for these to apply properly and confirm that it&#039;s working&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # systemctl restart openvswitch&lt;br /&gt;
# ovs-vsctl show&lt;br /&gt;
...&lt;br /&gt;
        Port bond0&lt;br /&gt;
            Interface ens2f1&lt;br /&gt;
                type: dpdk&lt;br /&gt;
                options: {dpdk-devargs=&amp;quot;0000:31:00.1&amp;quot;}&lt;br /&gt;
            Interface ens2f0&lt;br /&gt;
                type: dpdk&lt;br /&gt;
                options: {dpdk-devargs=&amp;quot;0000:31:00.0&amp;quot;}&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Update the CloudStack agent so that this host has the DPDK capability. Edit &amp;lt;code&amp;gt;/etc/cloudstack/agent/agent.properties&amp;lt;/code&amp;gt;. Note that the keyword is &amp;lt;code&amp;gt;openvswitch.dpdk.enabled&amp;lt;/code&amp;gt; (enabled ending with -ed). The example from ShapeBlue&#039;s blog post is wrong.{{Highlight&lt;br /&gt;
| code = network.bridge.type=openvswitch&lt;br /&gt;
libvirt.vif.driver=com.cloud.hypervisor.kvm.resource.OvsVifDriver&lt;br /&gt;
openvswitch.dpdk.enabled=true&lt;br /&gt;
openvswitch.dpdk.ovs.path=/var/run/openvswitch/&lt;br /&gt;
| lang = text&lt;br /&gt;
}}Restart the CloudStack agent for this capability to be visible by the management server. You should be able to call &amp;lt;code&amp;gt;list hosts filter=capabilities,name&amp;lt;/code&amp;gt; and have the host list dpdk as a capability. Eg:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = (localcloud) 🐱 &amp;gt; list hosts filter=capabilities,name&lt;br /&gt;
count = 22&lt;br /&gt;
host:&lt;br /&gt;
+-------------------+----------+&lt;br /&gt;
{{!}}   CAPABILITIES    {{!}}   NAME   {{!}}&lt;br /&gt;
+-------------------+----------+&lt;br /&gt;
{{!}} hvm,snapshot,dpdk {{!}} cs9      {{!}}&lt;br /&gt;
{{!}} hvm,snapshot,dpdk {{!}} cs10     {{!}}&lt;br /&gt;
{{!}} hvm,snapshot,dpdk {{!}} cs11     {{!}}&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
If you don&#039;t see this, double check your agent configs and restart it again.&lt;br /&gt;
&lt;br /&gt;
For VMs to take advantage of DPDK, you must either set extraconfig on the virtual machine or create a new compute service offering. Extraconfig might get overwritten whenever the VM is updated, so it&#039;s not a reliable solution. Extraconfig is a URL encoded config and you cannot use single quotes in it or else you will break the VM deployment. Eg:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = (localcloud) 🐱 &amp;gt; update virtualmachine extraconfig=dpdk-hugepages:%0A%3CmemoryBacking%3E%0A%20%20%20%3Chugepages%3E%0A%20%20%20%20%3C/hugepages%3E%0A%3C/memoryBacking%3E%0A%0Adpdk-numa:%0A%3Ccpu%20mode=%22host-passthrough%22%3E%0A%20%20%20%3Cnuma%3E%0A%20%20%20%20%20%20%20%3Ccell%20id=%220%22%20cpus=%220%22%20memory=%229437184%22%20unit=%22KiB%22%20memAccess=%22shared%22/%3E%0A%20%20%20%3C/numa%3E%0A%3C/cpu%3E%0A%0Adpdk-interface-queue:%0A%3Cdriver%20name=%22vhost%22%20queues=%22128%22/%3E id=af64cc80-a4e4-4c17-9c7d-c34ed234dc6a&lt;br /&gt;
virtualmachine = map[account:RCS affinitygroup:[] cpunumber:2 cpuspeed:1000 cpuused:5.88% created:2022-05-03T13:16:02-0600 details:map[Message.ReservedCapacityFreed.Flag:false dpdk-hugepages:a extraconfig-dpdk-hugepages:&amp;lt;memoryBacking&amp;gt;&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Troubleshooting ====&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = 2022-05-05T22:35:28.312Z{{!}}281704{{!}}netdev_dpdk{{!}}INFO{{!}}vHost Device &#039;/var/run/openvswitch/csdpdk-1&#039; connection has been destroyed&lt;br /&gt;
2022-05-05T22:35:28.312Z{{!}}281705{{!}}netdev_dpdk{{!}}INFO{{!}}vHost Device &#039;/var/run/openvswitch/csdpdk-1&#039; connection has been destroyed&lt;br /&gt;
2022-05-05T22:35:28.313Z{{!}}281706{{!}}netdev_dpdk{{!}}INFO{{!}}vHost Device &#039;/var/run/openvswitch/csdpdk-1&#039; connection has been destroyed&lt;br /&gt;
2022-05-05T22:35:28.313Z{{!}}281707{{!}}netdev_dpdk{{!}}INFO{{!}}vHost Device &#039;/var/run/openvswitch/csdpdk-1&#039; connection has been destroyed&lt;br /&gt;
2022-05-05T22:35:28.313Z{{!}}281708{{!}}netdev_dpdk{{!}}INFO{{!}}vHost Device &#039;/var/run/openvswitch/csdpdk-1&#039; connection has been destroyed&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
Check the agent logs for issues from qemu. I had defined an invalid property which prevented the VM from starting.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = [root@cs10 agent]# grep qemu agent.log&lt;br /&gt;
org.libvirt.LibvirtException: internal error: process exited while connecting to monitor: 2022-05-05T22:35:52.060450Z qemu-kvm: -netdev vhost-user,chardev=charnet0,queues=256,id=hostnet0: you are asking more queues than supported: 128&lt;br /&gt;
2022-05-05T22:35:52.060633Z qemu-kvm: -netdev vhost-user,chardev=charnet0,queues=256,id=hostnet0: you are asking more queues than supported: 128&lt;br /&gt;
2022-05-05T22:35:52.060817Z qemu-kvm: -netdev vhost-user,chardev=charnet0,queues=256,id=hostnet0: you are asking more queues than supported: 128&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==Tools==&lt;br /&gt;
&lt;br /&gt;
===CloudMonkey===&lt;br /&gt;
Get started:&lt;br /&gt;
&lt;br /&gt;
*Download from: https://github.com/apache/cloudstack-cloudmonkey/releases/tag/6.1.0&lt;br /&gt;
*Documentation at: https://cwiki.apache.org/confluence/display/CLOUDSTACK/CloudStack+cloudmonkey+CLI&lt;br /&gt;
&lt;br /&gt;
When you first run CloudMonkey, you will need to set the CloudStack instance URL and credentials and then run sync.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = $ cmk&lt;br /&gt;
&amp;gt; set url http://172.19.12.141:8080/client/api&lt;br /&gt;
&amp;gt; set username admin&lt;br /&gt;
&amp;gt; set password password&lt;br /&gt;
&amp;gt; sync&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
The settings are then saved to &amp;lt;code&amp;gt;~/.cmk/config&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The sync command fetches all the available API calls that your account can use. Once that is done, you can then use tab completion while in the CloudMonkey CLI.&lt;br /&gt;
&lt;br /&gt;
====Cheat sheet====&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!What&lt;br /&gt;
!Command&lt;br /&gt;
|-&lt;br /&gt;
|Change output format&lt;br /&gt;
|&amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;set display table|json&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|Create compute offering&lt;br /&gt;
|&amp;lt;code&amp;gt;create serviceoffering name=rcs.c2 displaytext=Medium cpunumber=2 cpuspeed=750 memory=2048 storagetype=shared provisioningtype=thin offerha=false limitcpuuse=false isvolatile=false issystem=false deploymentplanner=UserDispersingPlanner cachemode=none customized=false&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|Add a new host&lt;br /&gt;
|&amp;lt;code&amp;gt;add host clusterid=XX podid=XX zoneid=XX hypervisor=KVM password=**** username=root url=&amp;lt;nowiki&amp;gt;http://bm01&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
====Automate zone deployments====&lt;br /&gt;
There is an example script on how to automate a basic zone deployment at: https://github.com/apache/cloudstack-cloudmonkey/wiki/Usage&lt;br /&gt;
&lt;br /&gt;
=== Terraform ===&lt;br /&gt;
The Terraform CloudStack provide works for the most part. However, for CloudStack 4.16, you&#039;ll need to recompile it from scratch because the distributed binaries don&#039;t work properly (resulting in deployments hanging indefinitely). To build the Terraform provider, I will use Docker:&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # git clone https://github.com/apache/cloudstack-terraform-provider.git&lt;br /&gt;
# cd cloudstack-terraform-provide&lt;br /&gt;
# git clone https://github.com/tetra12/cloudstack-go.git&lt;br /&gt;
# cat &amp;lt;&amp;lt;EOF &amp;gt;&amp;gt; go.mod&lt;br /&gt;
replace github.com/apache/cloudstack-go/v2 =&amp;gt; ./cloudstack-go&lt;br /&gt;
exclude github.com/apache/cloudstack-go/v2 v2.11.0&lt;br /&gt;
EOF&lt;br /&gt;
# docker run --rm -ti -v /home/me/cloudstack-terraform-provider/:/build golang bash &lt;br /&gt;
&amp;gt; cd /build&lt;br /&gt;
&amp;gt; go build&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Copy the resulting binary to your terraform plugins path. Because I ran terraform init, it placed it in my terraform directory under &amp;lt;code&amp;gt;.terraform/providers/registry.terraform.io/cloudstack/cloudstack/0.4.0/linux_amd64/terraform-provider-cloudstack_v0.4.0&amp;lt;/code&amp;gt;. Edit the metadata file in the same directory as the provider executable and remove the file hash so that terraform runs the provider.&lt;br /&gt;
&lt;br /&gt;
See also: [[Terraform#CloudStack]]&lt;br /&gt;
&lt;br /&gt;
=== Packer ===&lt;br /&gt;
The Packer CloudStack provider also works for the most part, but is limited in that it cannot enter keyboard inputs. Any OS deployments will require some sort of manual inputs or require that the ISO media you use is completely automated. I also had to compile the provider manually since the default plugin that&#039;s fetched by packer doesn&#039;t quite work due to API changes.&lt;br /&gt;
&lt;br /&gt;
See also: [[Packer#CloudStack]]&lt;br /&gt;
&lt;br /&gt;
==Troubleshooting==&lt;br /&gt;
When you run into issues, check the logs in &amp;lt;code&amp;gt;/var/log/cloudstack/&amp;lt;/code&amp;gt;. There&#039;s typically a stacktrace which gets generated whenever you encounter an error.&lt;br /&gt;
&lt;br /&gt;
===Can&#039;t create shared network in a advanced zone using Open vSwitch===&lt;br /&gt;
Whenever I try creating a shared network in an advanced zone that is using OVS, the step fails with: &amp;quot;Unable to convert network offering with specified id to network profile&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Stack trace shows that the [https://github.com/apache/cloudstack/blob/bf6266188c89a5487383f216333ae10e878d2c10/plugins/network-elements/ovs/src/main/java/com/cloud/network/guru/OvsGuestNetworkGuru.java#L99 OVS guest network guru] isn&#039;t able at designing the network because the zone isn&#039;t capable of handling this network offering.&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = 2021-09-28 16:36:26,416 DEBUG [c.c.a.ApiServer] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) CIDRs from which account &#039;Acct[76a1585d-1bf6-11ec-a3c5-8f3e88f01ab1-admin]&#039; is allowed to perform API calls: 0.0.0.0/0,::/0&lt;br /&gt;
2021-09-28 16:36:26,439 DEBUG [c.c.u.AccountManagerImpl] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Access granted to Acct[76a1585d-1bf6-11ec-a3c5-8f3e88f01ab1-admin] to [Network Offering [7-Guest-DefaultSharedNetworkOffering] by AffinityGroupAccessChecker&lt;br /&gt;
2021-09-28 16:36:26,517 DEBUG [c.c.n.g.BigSwitchBcfGuestNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Refusing to design this network, the physical isolation type is not BCF_SEGMENT&lt;br /&gt;
2021-09-28 16:36:26,521 DEBUG [o.a.c.n.c.m.ContrailGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Refusing to design this network&lt;br /&gt;
2021-09-28 16:36:26,524 DEBUG [c.c.n.g.NiciraNvpGuestNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Refusing to design this network&lt;br /&gt;
2021-09-28 16:36:26,527 DEBUG [o.a.c.n.o.OpendaylightGuestNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Refusing to design this network&lt;br /&gt;
2021-09-28 16:36:26,530 DEBUG [c.c.n.g.OvsGuestNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Refusing to design this network&lt;br /&gt;
2021-09-28 16:36:26,536 DEBUG [c.c.n.g.DirectNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) GRE: VLAN&lt;br /&gt;
2021-09-28 16:36:26,536 DEBUG [c.c.n.g.DirectNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) GRE: VXLAN&lt;br /&gt;
2021-09-28 16:36:26,536 INFO  [c.c.n.g.DirectNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Refusing to design this network&lt;br /&gt;
2021-09-28 16:36:26,539 INFO  [c.c.n.g.DirectNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Refusing to design this network&lt;br /&gt;
2021-09-28 16:36:26,543 DEBUG [o.a.c.n.g.SspGuestNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) SSP not configured to be active&lt;br /&gt;
2021-09-28 16:36:26,546 DEBUG [c.c.n.g.BrocadeVcsGuestNetworkGuru] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Refusing to design this network&lt;br /&gt;
2021-09-28 16:36:26,549 DEBUG [o.a.c.e.o.NetworkOrchestrator] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Releasing lock for Acct[76a0531f-1bf6-11ec-a3c5-8f3e88f01ab1-system]&lt;br /&gt;
2021-09-28 16:36:26,624 DEBUG [c.c.u.d.T.Transaction] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) Rolling back the transaction: Time = 172 Name =  qtp1816147548-400; called by -TransactionLegacy.rollback:888-TransactionLegacy.removeUpTo:831-TransactionLegacy.close:655-Transaction.execute:38-Transaction.execute:47-NetworkOrches&lt;br /&gt;
trator.createGuestNetwork:2572-NetworkOrchestrator.createGuestNetwork:2327-NetworkServiceImpl$4.doInTransaction:1502-NetworkServiceImpl$4.doInTransaction:1450-Transaction.execute:40-NetworkServiceImpl.commitNetwork:1450-NetworkServiceImpl.createGuestNetwork:1366&lt;br /&gt;
2021-09-28 16:36:26,667 ERROR [c.c.a.ApiServer] (qtp1816147548-400:ctx-291672d1 ctx-3f19296a) (logid:83c45c2a) unhandled exception executing api command: [Ljava.lang.String;@69a8823d&lt;br /&gt;
com.cloud.utils.exception.CloudRuntimeException: Unable to convert network offering with specified id to network profile&lt;br /&gt;
        at org.apache.cloudstack.engine.orchestration.NetworkOrchestrator.setupNetwork(NetworkOrchestrator.java:739)&lt;br /&gt;
        at org.apache.cloudstack.engine.orchestration.NetworkOrchestrator$10.doInTransaction(NetworkOrchestrator.java:2634)&lt;br /&gt;
        at org.apache.cloudstack.engine.orchestration.NetworkOrchestrator$10.doInTransaction(NetworkOrchestrator.java:2572)&lt;br /&gt;
        at com.cloud.utils.db.Transaction$2.doInTransaction(Transaction.java:50)&lt;br /&gt;
        at com.cloud.utils.db.Transaction.execute(Transaction.java:40)&lt;br /&gt;
        at com.cloud.utils.db.Transaction.execute(Transaction.java:47)&lt;br /&gt;
...&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
====Possible answer====&lt;br /&gt;
The guest network was set up with GRE isolation. This however isn&#039;t supported with KVM as the hypervisor (see [https://events.static.linuxfound.org/sites/events/files/slides/CloudStack%20Collab%20Hypervisor.pdf this presentation]). After re-creating the zone with the guest physical network set up with just VLAN isolation, I was able to create a regular shared guest network that all tenants within the zone can see and use.&lt;br /&gt;
&lt;br /&gt;
To make the shared network SNAT out, I created another shared network offering that also has SourceNat and StaticNat.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = $ cmk list serviceofferings issystem=true name=&#039;System Offering For Software Router&#039;&lt;br /&gt;
$ cmk create networkoffering \&lt;br /&gt;
name=SharedNetworkOfferingWithSourceNatService displaytext=&amp;quot;Shared Network Offering with Source NAT Service&amp;quot; traffictype=GUEST guestiptype=shared conservemode=true specifyvlan=true specifyipranges=true \&lt;br /&gt;
serviceofferingid=307b14d8-afd1-43ea-948c-ffe882cd5926 \&lt;br /&gt;
supportedservices=Dhcp,Dns,Firewall,SourceNat,StaticNat,PortForwarding \&lt;br /&gt;
serviceProviderList[0].service=Dhcp serviceProviderList[0].provider=VirtualRouter \&lt;br /&gt;
serviceProviderList[1].service=Dns serviceProviderList[1].provider=VirtualRouter \&lt;br /&gt;
serviceProviderList[2].service=Firewall serviceProviderList[2].provider=VirtualRouter \&lt;br /&gt;
serviceProviderList[3].service=SourceNat serviceProviderList[3].provider=VirtualRouter \&lt;br /&gt;
serviceProviderList[4].service=StaticNat serviceProviderList[4].provider=VirtualRouter \&lt;br /&gt;
serviceProviderList[5].service=PortForwarding serviceProviderList[5].provider=VirtualRouter \&lt;br /&gt;
servicecapabilitylist[0].service=SourceNat servicecapabilitylist[0].capabilitytype=SupportedSourceNatTypes servicecapabilitylist[0].capabilityvalue=peraccount&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Using this network offering, I was able to create a shared network in the advanced networking zone that has a NAT service which is visible to all accounts. The only issue with this approach is that there isn&#039;t a way to create a port forwarding for a specific VM because the account that owns this network is &#039;system&#039;.&lt;br /&gt;
&lt;br /&gt;
=== Libvirtd can&#039;t start due to expired certificate ===&lt;br /&gt;
For some reason, the host stopped renewing agent certs with the management server. As a result, libvirtd will not restart. I only noticed this after rebooting an affected node after migrating all the VMs off the system.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # libvirtd -l&lt;br /&gt;
2023-11-30 18:16:25.116+0000: 39448: info : libvirt version: 8.0.0, package: 22.module+el8.9.0+1405+b6048078 (infrastructure@rockylinux.org, 2023-07-31-18:01:38, )&lt;br /&gt;
2023-11-30 18:16:25.116+0000: 39448: info : hostname: cs1&lt;br /&gt;
2023-11-30 18:16:25.116+0000: 39448: error : virNetTLSContextCheckCertTimes:142 : The server certificate /etc/pki/libvirt/servercert.pem has expired&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Note that the certficate file &amp;lt;code&amp;gt;/etc/pki/libvirt/servercert.pem&amp;lt;/code&amp;gt; symlinks to &amp;lt;code&amp;gt;/etc/cloudstack/agent/cloud.crt&amp;lt;/code&amp;gt;. The certificates and private keys under &amp;lt;code&amp;gt;/etc/cloudstack/agent/cloud*&amp;lt;/code&amp;gt; are generated by the CloudStack management server and then sent to and saved by the agent&amp;lt;ref&amp;gt;Certificate saved by the agent: https://github.com/apache/cloudstack/blob/cb62ce67671699fa01564b3b4b0d3d83eb3d5acb/agent/src/main/java/com/cloud/agent/Agent.java#L671&amp;lt;/ref&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Since the node is already out of service, the easiest fix here is to [[CloudStack#Re-add existing KVM bare metal host|re-add this KVM bare metal host]] back into CloudStack again.&lt;br /&gt;
&lt;br /&gt;
==Open-ended questions==&lt;br /&gt;
===Compute offerings with &#039;unlimited&#039; CPU cycles?===&lt;br /&gt;
Compute offerings require a MHz value assigned. Why is this? Can we just assign a VM entire cores?&lt;br /&gt;
&lt;br /&gt;
- If you read the docs, CPU (in MHz) only has an effect if CPU cap is selected. In all other cases, the value here is something akin to &#039;cpu shares&#039;. &lt;br /&gt;
&lt;br /&gt;
- if you put in a huge number like 9999, deployment would fail though.&lt;br /&gt;
&lt;br /&gt;
===How to implement showback?===&lt;br /&gt;
Is there a way to implement showback based on resources consumed by account?&lt;br /&gt;
&lt;br /&gt;
===Monitoring resources?===&lt;br /&gt;
Is there a way to monitor resource usage by account, node? Any good way to push VMs into a CMDB like ServiceNow?&lt;br /&gt;
&lt;br /&gt;
===NetApp integration?===&lt;br /&gt;
Is it possible to do guest VM snapshots by leveraging NetApp?&lt;br /&gt;
&lt;br /&gt;
===Backups?===&lt;br /&gt;
The only backup plugins that are available are &#039;dummy&#039; which does nothing and &#039;veeam&#039; which only supports VMware + Veeam.  If you&#039;re using KVM, there doesn&#039;t seem to be any way to easily backup/restore VMs.&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br /&amp;gt;&lt;br /&gt;
{{Navbox Linux}}&lt;br /&gt;
[[Category:Linux]]&lt;br /&gt;
[[Category:LinuxUtilities]]&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=FreeIPA&amp;diff=7687</id>
		<title>FreeIPA</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=FreeIPA&amp;diff=7687"/>
		<updated>2025-06-10T04:15:24Z</updated>

		<summary type="html">&lt;p&gt;Leo: FreeIPA 4.12 upgrade failure&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Installation ==&lt;br /&gt;
Here are my notes as I fumble my way setting up FreeIPA.&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
There is an official Docker container that has a complete FreeIPA installation. This container uses systemd to start up FreeIPA along with the other related services such as OpenLDAP, Bind, and Kerberos. See more at: https://github.com/freeipa/freeipa-container&lt;br /&gt;
&lt;br /&gt;
If you are using Docker, you &#039;&#039;&#039;must disable cgroup v2&#039;&#039;&#039; (this is enabled by default on RHEL9 and above). More about this in the Troubleshooting section below.&lt;br /&gt;
&lt;br /&gt;
Use the following &amp;lt;code&amp;gt;docker-compose.yml&amp;lt;/code&amp;gt; stack to quickly get started with FreeIPA:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = version: &#039;3.3&#039;&lt;br /&gt;
&lt;br /&gt;
services:&lt;br /&gt;
&lt;br /&gt;
  freeipa:&lt;br /&gt;
    image: freeipa/freeipa-server:rocky-8&lt;br /&gt;
    restart: unless-stopped&lt;br /&gt;
    tty: true&lt;br /&gt;
    stdin_open: true&lt;br /&gt;
    hostname: ipa&lt;br /&gt;
    domainname: home.steamr.com&lt;br /&gt;
    extra_hosts:&lt;br /&gt;
      - &amp;quot;ipa.home.steamr.com:10.1.2.12&amp;quot;&lt;br /&gt;
    environment:&lt;br /&gt;
      - IPA_SERVER_HOSTNAME=ipa.home.steamr.com&lt;br /&gt;
      - IPA_SERVER_IP=10.1.2.12&lt;br /&gt;
      - DNS=10.1.0.8&lt;br /&gt;
      - TZ=America/Edmonton&lt;br /&gt;
    command:&lt;br /&gt;
      - ipa-server-install&lt;br /&gt;
      - --realm=home.steamr.com&lt;br /&gt;
      - --domain=home.steamr.com&lt;br /&gt;
      - --ds-password=xxxxxxxxxx&lt;br /&gt;
      - --admin-password=xxxxxxxxxx&lt;br /&gt;
      - --no-host-dns&lt;br /&gt;
      - --setup-dns&lt;br /&gt;
      - --auto-forwarders&lt;br /&gt;
      - --allow-zone-overlap&lt;br /&gt;
      - --no-dnssec-validation&lt;br /&gt;
      - --unattended&lt;br /&gt;
    sysctls:&lt;br /&gt;
      - net.ipv6.conf.all.disable_ipv6=0&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ./data:/data&lt;br /&gt;
      - ./logs:/var/logs&lt;br /&gt;
      - /sys/fs/cgroup:/sys/fs/cgroup:ro&lt;br /&gt;
    tmpfs:&lt;br /&gt;
      - /run&lt;br /&gt;
      - /var/cache&lt;br /&gt;
      - /tmp&lt;br /&gt;
    cap_add:&lt;br /&gt;
      - SYS_TIME&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;10.1.2.12:80:80/tcp&amp;quot;&lt;br /&gt;
      - &amp;quot;10.1.2.12:443:443/tcp&amp;quot;&lt;br /&gt;
      # DNS&lt;br /&gt;
      - &amp;quot;10.1.2.12:53:53/tcp&amp;quot;&lt;br /&gt;
      - &amp;quot;10.1.2.12:53:53/udp&amp;quot;&lt;br /&gt;
      # LDAP(S)&lt;br /&gt;
      - &amp;quot;10.1.2.12:389:389/tcp&amp;quot;&lt;br /&gt;
      - &amp;quot;10.1.2.12:636:636/tcp&amp;quot;&lt;br /&gt;
      # Kerberos&lt;br /&gt;
      - &amp;quot;10.1.2.12:88:88/tcp&amp;quot;&lt;br /&gt;
      - &amp;quot;10.1.2.12:464:464/tcp&amp;quot;&lt;br /&gt;
      - &amp;quot;10.1.2.12:88:88/udp&amp;quot;&lt;br /&gt;
      - &amp;quot;10.1.2.12:464:464/udp&amp;quot;&lt;br /&gt;
| lang = yaml&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Samba integration ==&lt;br /&gt;
There are 3 methods to using FreeIPA with Samba. I eventually settled on method #2.&lt;br /&gt;
&lt;br /&gt;
# Configure Samba to use FreeIPA as a simple LDAP server, using &#039;&#039;&#039;ldapsam&#039;&#039;&#039; as the passdb backend. This requires a schema change to include the sambaSAMAccount and sambaGroupMapping, and sambaSID object classes. There is a DNA (distributed numeric assignment) plugin that can be used to update these fields.&lt;br /&gt;
# Configure Samba to use FreeIPA using &#039;&#039;&#039;ipasam&#039;&#039;&#039; as the passdb backend. This requires the &amp;lt;code&amp;gt;ipasam.so&amp;lt;/code&amp;gt; module installed on the samba servers.&lt;br /&gt;
# Configure Samba to use &#039;&#039;&#039;Kerberos&#039;&#039;&#039;. Does not seem to allow users to use password authentication. &lt;br /&gt;
&lt;br /&gt;
=== Method 1: ldapsam ===&lt;br /&gt;
{{Warning|I didn&#039;t get this to work|I wasn&#039;t able to fully get this method to work. If you are using OpenLDAP only, this way of integrating Samba does work. The issue I had was getting the DNA plugin to work as advertised. &lt;br /&gt;
&lt;br /&gt;
I eventually settled on the ipasam method in the section below.}}&lt;br /&gt;
To use ldapsam, we need to make some changes to the FreeIPA LDAP server by adding sambaSAMAccount and sambaGroupMapping as a default user object class and group object class. &lt;br /&gt;
&lt;br /&gt;
You can either set this in the FreeIPA web interface under configuration, or run:{{Highlight&lt;br /&gt;
| code = # ldapmodify &amp;lt;&amp;lt;EOF&lt;br /&gt;
dn: cn=ipaConfig,cn=etc,dc=home,dc=steamr,dc=com&lt;br /&gt;
changetype: modify&lt;br /&gt;
add: ipaUserObjectClasses&lt;br /&gt;
ipaUserObjectClasses: sambaSAMAccount&lt;br /&gt;
-&lt;br /&gt;
add: ipaGroupObjectClasses&lt;br /&gt;
ipaGroupObjectClasses: sambaGroupMapping&lt;br /&gt;
EOF&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}We will then need to make a custom DNA (distributed numeric assignment) plugin to update these attributes whenever something related to the object (such as the password) is changed. This can be done by adding a DNA object into LDAP.{{Highlight&lt;br /&gt;
| code = ldapadd &amp;lt;&amp;lt;EOF&lt;br /&gt;
dn: cn=SambaSid,cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config&lt;br /&gt;
objectClass: top&lt;br /&gt;
objectClass: extensibleObject&lt;br /&gt;
dnatype: sambaSID&lt;br /&gt;
dnaprefix: S-1-5-21-2049073866-1371207509-1214748462&lt;br /&gt;
dnainterval: 1&lt;br /&gt;
dnamagicregen: assign&lt;br /&gt;
dnafilter: ({{!}}(objectclass=sambasamaccount)(objectclass=sambagroupmapping))&lt;br /&gt;
dnascope: dc=home,dc=steamr,dc=com&lt;br /&gt;
cn: SambaSid&lt;br /&gt;
dnanextvalue: 2&lt;br /&gt;
&lt;br /&gt;
dn: cn=sambaGroupType,cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config&lt;br /&gt;
objectClass: top&lt;br /&gt;
objectClass: extensibleObject&lt;br /&gt;
cn: sambaGroupType&lt;br /&gt;
dnatype: sambaGroupType&lt;br /&gt;
dnainterval: 1&lt;br /&gt;
dnamagicregen: assign&lt;br /&gt;
dnafilter: (objectClass=sambagroupmapping)&lt;br /&gt;
dnascope: dc=home,dc=steamr,dc=com&lt;br /&gt;
dnanextvalue: 2&lt;br /&gt;
EOF&lt;br /&gt;
| lang = bash&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
A ldapsam user entry should have these fields. I don&#039;t see these though.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = dn: uid=guest2, ou=People,dc=quenya,dc=org&lt;br /&gt;
sambaLMPassword: 878D8014606CDA29677A44EFA1353FC7&lt;br /&gt;
sambaPwdMustChange: 2147483647&lt;br /&gt;
sambaPrimaryGroupSID: S-1-5-21-2447931902-1787058256-3961074038-513&lt;br /&gt;
sambaNTPassword: 552902031BEDE9EFAAD3B435B51404EE&lt;br /&gt;
sambaPwdLastSet: 1010179124&lt;br /&gt;
sambaLogonTime: 0&lt;br /&gt;
objectClass: sambaSamAccount&lt;br /&gt;
uid: guest2&lt;br /&gt;
sambaKickoffTime: 2147483647&lt;br /&gt;
sambaAcctFlags: [UX         ]&lt;br /&gt;
sambaLogoffTime: 2147483647&lt;br /&gt;
sambaSID: S-1-5-21-2447931902-1787058256-3961074038-5006&lt;br /&gt;
sambaPwdCanChange: 0&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Configure the Samba server ====&lt;br /&gt;
You can either use a specific binding credential that&#039;s shared across all your samba servers, or use the machine&#039;s cifs service account to authenticate to the LDAP server.&lt;br /&gt;
&lt;br /&gt;
I tried to do the following using the admin account as the bind DN: (&#039;&#039;&#039;using the admin account like this is probably a bad idea, I&#039;m just testing&#039;&#039;&#039;)&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = [global]&lt;br /&gt;
	# freeipa configurations&lt;br /&gt;
	passdb backend = ipasam:ldap://home.steamr.com&lt;br /&gt;
	ldap admin dn = uid=admin,cn=users,cn=accounts,dc=home,dc=steamr,dc=com&lt;br /&gt;
	ldapsam:trusted = yes&lt;br /&gt;
	ldap suffix = cn=accounts,dc=home,dc=steamr,dc=com&lt;br /&gt;
	ldap user suffix = cn=users,cn=accounts&lt;br /&gt;
	ldap machine suffix = cn=computers,cn=accounts&lt;br /&gt;
	ldap group suffix = cn=groups,cn=accounts&lt;br /&gt;
	ldap passwd sync = only&lt;br /&gt;
	ldap ssl = no&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
Run &amp;lt;code&amp;gt;smbpasswd -w password&amp;lt;/code&amp;gt; to set your bind credential passwords.  &lt;br /&gt;
&lt;br /&gt;
=== Method 2: ipasam ===&lt;br /&gt;
See: https://bgstack15.wordpress.com/2017/05/10/samba-share-with-freeipa-auth/&lt;br /&gt;
&lt;br /&gt;
Install the adtrust components on the FreeIPA server. Install &amp;lt;code&amp;gt;ipa-server-trust-ad&amp;lt;/code&amp;gt; and run &amp;lt;code&amp;gt;ipa-adtrust-install --add-sids&amp;lt;/code&amp;gt;. This will add the additional IPASAM attributes such as ipaNtPassword in user objects.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ipa-adtrust-install --add-sids&lt;br /&gt;
## Answer yes to overwrite smb.conf&lt;br /&gt;
## Answer yes to install slapi-nis&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Ensure that your hostname is set to the FQDN of the hostname otherwise this process will fail. If you are using an external DNS server, ensure that the additional service records are present. If you are using a Docker container, set the hostname to the full FQDN (eg. ipa.example.com, rather than just &#039;ipa&#039;).&lt;br /&gt;
&lt;br /&gt;
Next, create a new user and then change the user&#039;s password. This should populate the &amp;lt;code&amp;gt;ipaNTPassword&amp;lt;/code&amp;gt; attribute. {{Highlight&lt;br /&gt;
| code = # ipa user-add leo --first=Leo --last=Leung&lt;br /&gt;
# ipa group-add-member smbgrp --users=leo&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
When modifying smb.conf, the service account or bind DN must have access to these new ipasam attributes. You need to create a new set of privilege and role and grant the account access. Create the role and permissions in the web interface or run:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ipa permission-add &amp;quot;CIFS server can read user passwords&amp;quot;  --attrs={ipaNTHash,ipaNTSecurityIdentifier} --type=user --right={read,search,compare} --bindtype=permission&lt;br /&gt;
# ipa privilege-add &amp;quot;CIFS server privilege&amp;quot;&lt;br /&gt;
# ipa privilege-add-permission &amp;quot;CIFS server privilege&amp;quot; --permission=&amp;quot;CIFS server can read user passwords&amp;quot;&lt;br /&gt;
# ipa role-add &amp;quot;CIFS server&amp;quot;&lt;br /&gt;
# ipa role-add-privilege &amp;quot;CIFS server&amp;quot; --privilege=&amp;quot;CIFS server privilege&amp;quot;&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Then, add your service account or bind DN to the &#039;CIFS server&#039; role. For example:&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ipa service-add cifs/dnas.home.steamr.com&lt;br /&gt;
# ipa role-add-member &amp;quot;CIFS server&amp;quot; --services=cifs/dnas.home.steamr.com&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Configure the Samba server ====&lt;br /&gt;
Generate a keytab file for samba.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # kinit -kt /etc/krb5.keytab&lt;br /&gt;
## Note: we ran this in the previous step.&lt;br /&gt;
## ipa service-add cifs/dnas.home.steamr.com&lt;br /&gt;
# ipa-getkeytab -s ipa.home.steamr.com -p cifs/dnas.home.steamr.com -k /etc/samba/samba.keytab&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Then, tweak smb.conf.{{Highlight&lt;br /&gt;
| code = [global]&lt;br /&gt;
passdb backend = ipasam:ldap://ipa.home.steamr.com&lt;br /&gt;
ldapsam:trusted = yes&lt;br /&gt;
ldap suffix = dc=home,dc=steamr,dc=com&lt;br /&gt;
ldap user suffix = cn=users,cn=accounts&lt;br /&gt;
ldap machine suffix = cn=computers,cn=accounts&lt;br /&gt;
ldap group suffix = cn=groups,cn=accounts&lt;br /&gt;
ldap ssl = no&lt;br /&gt;
idmap config * : backend = tdb  &lt;br /&gt;
create krb5 conf = No &lt;br /&gt;
dedicated keytab file = FILE:/etc/samba/samba.keytab&lt;br /&gt;
kerberos method = dedicated keytab&lt;br /&gt;
| lang = text&lt;br /&gt;
}}If you get: &amp;lt;code&amp;gt;NT_STATUS_BAD_TOKEN_TYPE&amp;lt;/code&amp;gt;, you need to disable MS-POC in the FreeIPA settings or disable it specifically for this cifs service account.&lt;br /&gt;
&lt;br /&gt;
The ipasam passdb provider is available from the &amp;lt;code&amp;gt;ipa-server-trust-ad&amp;lt;/code&amp;gt; package. However, this package also pulls in a ton of other IPA dependencies which aren&#039;t needed if you just want to run Samba that talks to IPA and not the entire FreeIPA server. If you just want the provider to work on a bare minimal samba server, you can simply just copy (or extract from the &amp;lt;code&amp;gt;ipa-server-trust-ad&amp;lt;/code&amp;gt; package) the &amp;lt;code&amp;gt;ipasam.so&amp;lt;/code&amp;gt; file to &amp;lt;code&amp;gt;/usr/lib64/samba/pdb/ipasam.so&amp;lt;/code&amp;gt; with this set of commands:&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # yum download ipa-server-trust-ad&lt;br /&gt;
# mkdir x &amp;amp;&amp;amp; cd x&lt;br /&gt;
# rpm2cpio ../ipa-server-trust-ad*rpm {{!}} cpio -id ./usr/lib64/samba/pdb/ipasam.so&lt;br /&gt;
# cp ./usr/lib64/samba/pdb/ipasam.so /usr/lib64/samba/pdb/ipasam.so&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Method 3: Kerberos ===&lt;br /&gt;
&lt;br /&gt;
This method is similar to the ipasam method above and you will need to set up the server in the same way. However, the way you configure Samba is different. &lt;br /&gt;
&lt;br /&gt;
==== On the Samba server ====&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ## Join the samba server to FreeIPA&lt;br /&gt;
# ipa-client-install&lt;br /&gt;
&lt;br /&gt;
## Then add the client to samba.&lt;br /&gt;
# ipa-client-samba&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Note that when you try adding the samba client, the IPA server has to be able to resolve the A record for the Samba server you&#039;re adding or else it will fail.&lt;br /&gt;
&lt;br /&gt;
This should automatically set up the cifs service accounts for this particular samba server, get the samba keytab file in &amp;lt;code&amp;gt;/etc/samba/samba.keytab&amp;lt;/code&amp;gt;, and then tweak the smb.conf file to use this keytab file. &lt;br /&gt;
&lt;br /&gt;
The smb.conf file now looks like:&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = [global]&lt;br /&gt;
    # Limit number of forked processes to avoid SMBLoris attack&lt;br /&gt;
    max smbd processes = 1000&lt;br /&gt;
    # Use dedicated Samba keytab. The key there must be synchronized&lt;br /&gt;
    # with Samba tdb databases or nothing will work&lt;br /&gt;
    dedicated keytab file = FILE:/etc/samba/samba.keytab&lt;br /&gt;
    kerberos method = dedicated keytab&lt;br /&gt;
    # Set up logging per machine and Samba process&lt;br /&gt;
    log file = /var/log/samba/log.%m&lt;br /&gt;
    log level = 1&lt;br /&gt;
    # We force &#039;member server&#039; role to allow winbind automatically&lt;br /&gt;
    # discover what is supported by the domain controller side&lt;br /&gt;
    server role = member server&lt;br /&gt;
    realm = HOME.STEAMR.COM&lt;br /&gt;
    netbios name = DNAS&lt;br /&gt;
    workgroup = HOME&lt;br /&gt;
    # Local writable range for IDs not coming from IPA or trusted domains&lt;br /&gt;
    idmap config * : range = 0 - 0&lt;br /&gt;
    idmap config * : backend = tdb&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    idmap config HOME : range = 100000 - 299999&lt;br /&gt;
    idmap config HOME : backend = sss&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Default homes share&lt;br /&gt;
[homes]&lt;br /&gt;
    read only = no&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
You need to then start winbind and smb.  For some reason, this method doesn&#039;t seem to work as winbind is stuck with &amp;quot;&amp;lt;code&amp;gt;wb_parent_idmap_setup_lookupname_done: Lookup domain name &#039;home&#039; failed &#039;NT_STATUS_DOMAIN_CONTROLLER_NOT_FOUND&#039;&amp;lt;/code&amp;gt;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
=== Debug samba issues ===&lt;br /&gt;
Use &amp;lt;code&amp;gt;smbclient&amp;lt;/code&amp;gt; to help debug issues. This utility is provided by the &amp;lt;code&amp;gt;samba-client&amp;lt;/code&amp;gt; package. You can then test authentication by running: &amp;lt;code&amp;gt;smbclient -d 10 -U leo //dnas/home&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Tasks ==&lt;br /&gt;
&lt;br /&gt;
=== Join a computer to a FreeIPA ===&lt;br /&gt;
Use the &amp;lt;code&amp;gt;ipa-client-install&amp;lt;/code&amp;gt; command to add a computer to a FreeIPA server. This should also automatically add a computer account, generate a keytab file, and tweak sssd to use FreeIPA as an authentication mechanism.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ipa-client-install -U -p admin -w $Password --server ipa.home.steamr.com --domain home.steamr.com --force-join --no-ntp --fixed-primary&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Error: did not receive Kerberos credentials ===&lt;br /&gt;
Tools such as &#039;ipa&#039; uses your session&#039;s Kerberos tickets for authentication. If you don&#039;t have any tickets or if your tickets expired, you may get an &amp;lt;code&amp;gt;ipa: ERROR: did not receive Kerberos credentials&amp;lt;/code&amp;gt; error. Fix this by running:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ## Renew/obtain Kerberos tickets for &#039;admin&#039;&lt;br /&gt;
# kinit admin&lt;br /&gt;
Password for admin@HOME.STEAMR.COM:  ****&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Verify if your tickets are available with &amp;lt;code&amp;gt;klist&amp;lt;/code&amp;gt;:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # klist&lt;br /&gt;
Ticket cache: FILE:/tmp/krb5cc_0&lt;br /&gt;
Default principal: admin@STEAMR.COM&lt;br /&gt;
&lt;br /&gt;
Valid starting     Expires            Service principal&lt;br /&gt;
03/06/22 14:44:35  03/07/22 14:39:47  krbtgt/STEAMR.COM@STEAMR.COM&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Container issues ===&lt;br /&gt;
&lt;br /&gt;
* Don&#039;t mount /var/log because the container image symlinks everything into /data. If you do mount /var/log, make sure you make the expected directories or else the installer will fail.&lt;br /&gt;
* Error with &amp;lt;code&amp;gt;AssertionError: Another instance named &#039;HOME-STEAMR-COM&#039; may already exist&amp;lt;/code&amp;gt;. I can&#039;t figure out what&#039;s causing lib389 to think there&#039;s another instance. I built a container image on top of this image with the assertion patched out. This seemed to have fixed the issue. &lt;br /&gt;
* {{Highlight&lt;br /&gt;
| code = FROM freeipa/freeipa-server:rocky-8&lt;br /&gt;
RUN sed &#039;s/assert_c(len(insts)/# assert_c(len(insts)/&#039; -i /usr/lib/python3.6/site-packages/lib389/instance/setup.py&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Can&#039;t find the ipa-adtrust-install package ====&lt;br /&gt;
The FreeIPA packages are under a different app stream repo. Enable it by running &amp;lt;code&amp;gt;dnf -y module enable idm:DL1&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== sssd: Decrypt integrity check failed ===&lt;br /&gt;
After recreating the FreeIPA server, I uninstalled and reinstalled the FreeIPA client on a machine. Kinit works as expected, but sssd authentication fails with the following error in /var/log/sssd/.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;[krb5_child[29691]] [get_and_save_tgt] (0x0020): [RID#6] 1725: [-1765328353][Decrypt integrity check failed]&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The fix here is to wipe out all the caches. &amp;lt;code&amp;gt;sss_cache -E&amp;lt;/code&amp;gt; isn&#039;t sufficient. You have to stop sssd and delete all the databases:&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # systemctl stop sssd&lt;br /&gt;
# rm -rf /var/lib/sss/db/*&lt;br /&gt;
# systemctl start sssd&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== File permission issues ===&lt;br /&gt;
I ran into issues starting FreeIPA in a Docker container. Symptoms include the following issues in the subsections below.&lt;br /&gt;
&lt;br /&gt;
==== Bind / named doesn&#039;t start: ====&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = ipa named-pkcs11[5407]: LDAP error: Invalid credentials: bind to LDAP server failed&lt;br /&gt;
ipa named-pkcs11[5407]: couldn&#039;t establish connection in LDAP connection pool: permission denied&lt;br /&gt;
ipa named-pkcs11[5407]: dynamic database &#039;ipa&#039; configuration failed: permission denied&lt;br /&gt;
ipa named-pkcs11[5407]: loading configuration: permission denied&lt;br /&gt;
ipa named-pkcs11[5407]: exiting (due to fatal error)&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
Ignore this for now. You have other issues, likely.&lt;br /&gt;
&lt;br /&gt;
==== Samba doesn&#039;t start:  Error: Invalid credentials ====&lt;br /&gt;
When trying to start smb, I get the following:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = [2023/01/24 05:17:46.170883,  0, pid=5124] ipa_sam.c:4945(bind_callback)&lt;br /&gt;
  bind_callback: cannot perform interactive SASL bind with GSSAPI. LDAP security error is 49&lt;br /&gt;
[2023/01/24 05:17:46.171155,  0, pid=5124] ../../source3/lib/smbldap.c:1059(smbldap_connect_system)&lt;br /&gt;
  failed to bind to server ldapi://%2fvar%2frun%2fslapd-HOME-STEAMR-COM.socket with dn=&amp;quot;[Anonymous bind]&amp;quot; Error: Invalid credentials&lt;br /&gt;
        (unknown)&lt;br /&gt;
[2023/01/24 05:17:46.171419,  1, pid=5124] ../../source3/lib/smbldap.c:1272(get_cached_ldap_connect)&lt;br /&gt;
  Connection to LDAP server failed for the 1 try!&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
The fix was to stop FreeIPA with &amp;lt;code&amp;gt;ipactl stop&amp;lt;/code&amp;gt;, then &amp;lt;code&amp;gt;rm /run/samba/krb5cc_samba&amp;lt;/code&amp;gt;, and restarting FreeIPA with &amp;lt;code&amp;gt;ipactl start&amp;lt;/code&amp;gt;. If the error recurrs after restarting FreeIPA, then you have other issues that&#039;s preventing Samba from starting.&lt;br /&gt;
&lt;br /&gt;
==== Tomcat doesn&#039;t start: status=5/NOTINSTALLED ====&lt;br /&gt;
When trying to start Tomcat, you get a 5 exit code, as reported by systemd:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = pki-tomcatd@pki-tomcat.service: Control process exited, code=exited, status=5/NOTINSTALLED&lt;br /&gt;
pki-tomcatd@pki-tomcat.service: Failed with result &#039;exit-code&#039;.&lt;br /&gt;
Failed to start PKI Tomcat Server pki-tomcat.&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
The exit code 5 being &#039;NOTINSTALLED&#039; is a red herring. You likely have permission issues that&#039;s preventing the user running tomcat (pkiuser) from accessing some certs or configs. Go through your Docker volumes and chown anything directory called &#039;pki&#039; to the pkiuser.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # chown -R 17:17 etc/pki etc/sysconfig/pki var/lib/pki var/lib/ipa/pki-ca&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== Tomcat still doesn&#039;t start: start-post operation timed out. Terminating. ====&lt;br /&gt;
Tomcat doesn&#039;t start as it times out.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = pki-tomcatd@pki-tomcat.service: start-post operation timed out. Terminating.&lt;br /&gt;
pki-tomcatd@pki-tomcat.service: Control process exited, code=killed, status=15/TERM&lt;br /&gt;
pki-tomcatd@pki-tomcat.service: Failed with result &#039;timeout&#039;.&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
There&#039;s likely still something tomcat can&#039;t write to.&lt;br /&gt;
&lt;br /&gt;
It seemed to get better when made the ownerships for /data/var/lib/pki/pki-tomcat/logs/ and /var/log/pki/pki-tomcat to pkiuser.&lt;br /&gt;
&lt;br /&gt;
To help troubleshoot, su as pkiuser and try running tomcat as per the service file and see if you get any stack traces.&lt;br /&gt;
&lt;br /&gt;
==== IPA Web GUI reports &amp;quot;Your session has expired. Please re-login&amp;quot; ====&lt;br /&gt;
Review the logs for dirsrv and see if there are any errors. &lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # tail -f /var/log/dirsrv/slapd-*/access&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
For this instance, this was caused when I accidentally removed &amp;lt;code&amp;gt;/etc/dirsrv/ds.keytab.&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Configuring Samba issues ===&lt;br /&gt;
When running # ipa-adtrust-install --add-sids, you get:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # ipa-adtrust-install --add-sids&lt;br /&gt;
...&lt;br /&gt;
Configuring CIFS&lt;br /&gt;
  [1/25]: validate server hostname&lt;br /&gt;
  [error] ValueError: Host reports different name than configured: &#039;ipa&#039; versus &#039;ipa.home.steamr.com&#039;. Samba requires to have the same hostname or Kerberos principal &#039;cifs/ipa.home.steamr.com&#039; will not be found in Samba keytab.&lt;br /&gt;
Unexpected error - see /var/log/ipaserver-adtrust-install.log for details:&lt;br /&gt;
ValueError: Host reports different name than configured: &#039;ipa&#039; versus &#039;ipa.home.steamr.com&#039;. Samba requires to have the same hostname or Kerberos principal &#039;cifs/ipa.home.steamr.com&#039; will not be found in Samba keytab.&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Ensure that your hostname is the full FQDN.&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # hostname&lt;br /&gt;
ipa&lt;br /&gt;
# hostname -f&lt;br /&gt;
ipa.home.steamr.com&lt;br /&gt;
&lt;br /&gt;
## You need to fix the hostname so that this is what you get:&lt;br /&gt;
# hostname ipa.home.steamr.com&lt;br /&gt;
# hostname&lt;br /&gt;
ipa.home.steamr.com&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Once the hostname is fixed, try the command again.&lt;br /&gt;
&lt;br /&gt;
=== DNS is missing A/AAAA entries for hosts ===&lt;br /&gt;
When trying to add a new host, the DNS silently gets ignored. Possible issues:&lt;br /&gt;
&lt;br /&gt;
* I think this is related to this error when running ipa-client-install: &amp;lt;code&amp;gt;Could not update DNS SSHFP records.&amp;lt;/code&amp;gt;. &lt;br /&gt;
* Possibly bad DNS entries during install was detected and it skipped the DNS tasks? {{Highlight&lt;br /&gt;
| code = Hostname (fc37.home.steamr.com) does not have A/AAAA record.&lt;br /&gt;
Failed to update DNS records.&lt;br /&gt;
Missing A/AAAA record(s) for host fc37.home.steamr.com: 10.1.2.32.&lt;br /&gt;
Incorrect reverse record(s):&lt;br /&gt;
10.1.2.32 is pointing to fc35.home.steamr.com. instead of fc37.home.steamr.com.&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Without this DNS entry set, other issues will crop up later on:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = [root@ipa /]# ipa service-add cifs/dnas.home.steamr.com&lt;br /&gt;
ipa: ERROR: Host &#039;dnas.home.steamr.com&#039; does not have corresponding DNS A/AAAA record&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
The quick work-around would be to add the DNS entry manually and try again.&lt;br /&gt;
&lt;br /&gt;
=== FreeIPA doesn&#039;t start under Docker: Failed to allocate manager object ===&lt;br /&gt;
When trying to run FreeIPA under Docker, you get the following message almost immediately on startup:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = Detected virtualization docker.&lt;br /&gt;
Detected architecture x86-64.&lt;br /&gt;
Failed to create /init.scope control group: Read-only file system&lt;br /&gt;
Failed to allocate manager object: Read-only file system&lt;br /&gt;
[!!!!!!] Failed to allocate manager object.&lt;br /&gt;
Exiting PID 1...&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
You&#039;re likely running this under a Docker host that has cgroup v2 enabled. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Possible workaround&#039;&#039;&#039; (though I couldn&#039;t get it to work before I reverted to Rocky Linux 8, but I realized after downgrading FreeIPA takes a ton of time before it appears to be functional): You will have to disable this by adding &amp;lt;code&amp;gt;systemd.unified_cgroup_hierarchy=0&amp;lt;/code&amp;gt; as a kernel arguments to &amp;lt;code&amp;gt;/etc/default/grub&amp;lt;/code&amp;gt;: . Rebuild grub configs &amp;lt;code&amp;gt;grub2-mkconfig -o /boot/grub2/grub.cfg&amp;lt;/code&amp;gt; and reboot. Bring the container up as usual.&lt;br /&gt;
&lt;br /&gt;
Alternatively, use Podman (except that I can&#039;t because I use docker-compose for all my setups).&lt;br /&gt;
&lt;br /&gt;
=== Failed to authenticate to CA REST API ===&lt;br /&gt;
After suffering a brief power outage, my FreeIPA stack stopped working. A contributing factor might have been my auto-update mechanism which pulled in the most recent version of the freeipa/freeipa:rocky-9 container image.  Looking at the container logs, I see that the container beings to shutdown after the upgrade command fails. Looking at the &amp;lt;code&amp;gt;/var/log/ipaupgrade.log&amp;lt;/code&amp;gt; log file, I see the following error:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = 2025-06-10T03:54:37Z DEBUG The ipa-server-upgrade command failed, exception: RemoteRetrieveError: Failed to authenticate to CA REST API&lt;br /&gt;
2025-06-10T03:54:37Z ERROR Unexpected error - see /var/log/ipaupgrade.log for details:&lt;br /&gt;
RemoteRetrieveError: Failed to authenticate to CA REST API&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
My first step was to try downgrading. The second oldest container image was FreeIPA 4.11 and attempting to downgrade to this version didn&#039;t work as the data I had already had migrated to 4.12.&lt;br /&gt;
&lt;br /&gt;
Some searching turned up a [https://access.redhat.com/solutions/7122683 Red Hat knowledgebase article] which states that this is a bug with ipa-server 4.12.2-14. The fix is to change the following files:&lt;br /&gt;
&lt;br /&gt;
* Add &amp;lt;code&amp;gt;/etc/pki/pki-tomcat/Catalina/localhost/rewrite.config&amp;lt;/code&amp;gt; (copy it from &amp;lt;code&amp;gt;/usr/share/pki/server/conf/Catalina/localhost/rewrite.config&amp;lt;/code&amp;gt;)&lt;br /&gt;
* Edit &amp;lt;code&amp;gt;/etc/pki/pki-tomcat/server.xml&amp;lt;/code&amp;gt; to include before the closing &amp;lt;code&amp;gt;&amp;lt;/Host&amp;gt;&amp;lt;/code&amp;gt; tag near the bottom of the file: {{Highlight&lt;br /&gt;
| code = &amp;lt;Valve className=&amp;quot;org.apache.catalina.valves.rewrite.RewriteValve&amp;quot;/&amp;gt;&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Because both files are in &amp;lt;code&amp;gt;/etc&amp;lt;/code&amp;gt;, these two files should be in the data volume that&#039;s mounted into the FreeIPA container. Edit both files from the data volume and then try restarting the container. It should start properly.&lt;br /&gt;
&lt;br /&gt;
Enterprise software? FreeIPA feels like it was put together with duct tape.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* https://bgstack15.wordpress.com/2017/05/10/samba-share-with-freeipa-auth/  - Older guide walking how to set up FreeIPA and Samba&lt;br /&gt;
* https://blog.cubieserver.de/2018/synology-nas-samba-nfs-and-kerberos-with-freeipa-ldap/&lt;br /&gt;
* https://www.freeipa.org/page/Howto/Integrating_a_Samba_File_Server_With_IPA - Samba integration using kerberos (not ipasam)&lt;br /&gt;
* https://freeipa-users.redhat.narkive.com/ez2uKpFS/authenticate-samba-3-or-4-with-freeipa&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{{Navbox Linux}}&lt;br /&gt;
[[Category:Linux]]&lt;br /&gt;
[[Category:Networking]]&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Santek_EZ_Door_Sign&amp;diff=7686</id>
		<title>Santek EZ Door Sign</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Santek_EZ_Door_Sign&amp;diff=7686"/>
		<updated>2025-03-29T03:09:30Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The Santek EZ Door Sign is cordless e-ink display intended to be mounted outside of offices or rooms. It is capable of storing 5 custom images that can be displayed and rotated with the side button.&lt;br /&gt;
&lt;br /&gt;
The 2.9&amp;quot; variant which I obtained has a resolution of 296 x 128 pixels and is capable of displaying black, white, and red.&lt;br /&gt;
&lt;br /&gt;
== Serial protocol ==&lt;br /&gt;
&lt;br /&gt;
=== Background ===&lt;br /&gt;
Santek provides a Windows based app written in C# that is able to interact with the door signs. My goal is to have some Linux based device that can periodically update these displays (such as the weather or calendar events on an hourly basis). In order to do that, I will need to figure out how exactly this device talks via the USB port.&lt;br /&gt;
&lt;br /&gt;
There are some GitHub projects that try to interface with this device already including https://github.com/m3m0r7/ez-door-sign in PHP and https://github.com/kenichi884/ezsign.py in Python. However, the documentation on the serial protocol is a bit lacking. Fortunately, the protocol doesn&#039;t seem too complicated.&lt;br /&gt;
&lt;br /&gt;
=== Serial configuration ===&lt;br /&gt;
Use 9600 baud. No parity.&lt;br /&gt;
&lt;br /&gt;
The display has to be on. Ensure that the blue LED is lit before trying to talk to it via serial.&lt;br /&gt;
&lt;br /&gt;
=== Commands and messages ===&lt;br /&gt;
Here are some of the supported commands by the display.&lt;br /&gt;
&lt;br /&gt;
Command Type is 0 if it&#039;s a command that&#039;s sent to the display. It will be a 1 if it&#039;s data that&#039;s a response to a command made by the display.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Header&lt;br /&gt;
!Command&lt;br /&gt;
Type&lt;br /&gt;
!Command&lt;br /&gt;
ID&lt;br /&gt;
!Data&lt;br /&gt;
Length&lt;br /&gt;
!Data&lt;br /&gt;
!Chksum&lt;br /&gt;
!Ending&lt;br /&gt;
!Notes&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|slide (1 byte)&lt;br /&gt;
|&amp;lt;code&amp;gt;??&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Redraws the specified slide (0 ~ 4)&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|slide (1 byte)&lt;br /&gt;
|&amp;lt;code&amp;gt;??&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Redraw slide response&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0xFE&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0xFF&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Command next slide&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0xFF&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Command previous slide (called &#039;up&#039; in C# code)&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|slide (1 byte)&lt;br /&gt;
|&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Check slide data&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|status (1 byte)&lt;br /&gt;
|&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Response to the check slide call. Status is one of:&lt;br /&gt;
&lt;br /&gt;
* 0xFF - Slide is blank or unwritten&lt;br /&gt;
* 0xFE - Slide has data&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x03&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x02&amp;lt;/code&amp;gt;&lt;br /&gt;
|slide-canvas&lt;br /&gt;
(2 bytes)&lt;br /&gt;
|&amp;lt;code&amp;gt;??&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Begin sending data for a specific slide and canvas. All data for the slide must be rewritten as everything is wiped.&lt;br /&gt;
Data is always 2 bytes long, containing:&lt;br /&gt;
&lt;br /&gt;
* slide: slide number (0 through 4)&lt;br /&gt;
* canvas: Color canvas (0 or 1)&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x03&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Response to the slide-canvas command.&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x04&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x12&amp;lt;/code&amp;gt;&lt;br /&gt;
|column-bitmap&lt;br /&gt;
(18 bytes)&lt;br /&gt;
|&amp;lt;code&amp;gt;??&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|send image bitmap data by column.&lt;br /&gt;
Data is always 18 bytes long. 2 for column number, 16 for bitmap data.&lt;br /&gt;
&lt;br /&gt;
Bitmap data here is based on the canvas setting in order to get that extra bit for color.&lt;br /&gt;
&lt;br /&gt;
* Column: 2 bytes in decimal. (0 ~ 295) (0x00,0x00 ~ 0x01,0x27)&lt;br /&gt;
* Bitmap: 16 bytes. One bit per pixel makes 128 pixels&lt;br /&gt;
This is how you can retrieve data for the 296x128 pixel display.&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x04&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|did send image row data succeed? Data should be 0x01&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x05&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x04&amp;lt;/code&amp;gt;&lt;br /&gt;
|slide-canvas-column&lt;br /&gt;
(4 bytes)&lt;br /&gt;
|&amp;lt;code&amp;gt;??&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Get image bitmap from display memory.&lt;br /&gt;
Data is always 4 bytes long.&lt;br /&gt;
&lt;br /&gt;
* Slide: Slide number (0 through 4)&lt;br /&gt;
* Canvas: Color canvas (0 or 1)&lt;br /&gt;
* Column: 2 bytes. (0 ~ 295)&lt;br /&gt;
&lt;br /&gt;
To retrieve the actual color, you have to read both canvas&lt;br /&gt;
&lt;br /&gt;
See the response payload below.&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x05&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x12&amp;lt;/code&amp;gt;&lt;br /&gt;
|column-bitmap&lt;br /&gt;
(24 bytes)&lt;br /&gt;
|&amp;lt;code&amp;gt;??&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Bitmap data received from a get command. The response message is always 24 bytes long.&lt;br /&gt;
There are 18 bytes of payload data and is the same format as the send-image-column command.&lt;br /&gt;
&lt;br /&gt;
* Column: 2 bytes. (0 ~ 295)&lt;br /&gt;
* Bitmap: 16 bytes. One bit per pixel makes 128 pixels&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
|&amp;lt;code&amp;gt;0x06&amp;lt;/code&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|iap model&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x07&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x08&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|power off&lt;br /&gt;
|}&lt;br /&gt;
=== Writing bitmap information ===&lt;br /&gt;
The display is tri-color and uses 2 bits per pixel to denote color. The 2 bits are written across two separate &#039;canvases&#039; as part of the write operation. When writing an image:&lt;br /&gt;
&lt;br /&gt;
# Call the start write command (&amp;lt;code&amp;gt;0x03&amp;lt;/code&amp;gt;) and specify the desired slide and &amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt; canvas&lt;br /&gt;
# Write the bitmap with command &amp;lt;code&amp;gt;0x04&amp;lt;/code&amp;gt;. This command operates column by column, from left to right, top to bottom. There should be 128 pixels (as the display is 128 pixels high). Black and red pixels should be 0. White should be 1. (see table below)&lt;br /&gt;
# Call the start write command (&amp;lt;code&amp;gt;0x03&amp;lt;/code&amp;gt;) again but this time set canvas to &amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
# Write the bitmap again with command &amp;lt;code&amp;gt;0x04&amp;lt;/code&amp;gt; as in step 2, but this time, set the bitmap to 0 for black and white and 1 for red.&lt;br /&gt;
&lt;br /&gt;
You must write to all addresses. The memory for the slide appears to get wiped when a write is initiaited. Skipping any of the columns for either canvases will cause the image to become corrupt.&lt;br /&gt;
&lt;br /&gt;
If you call the 0x03 begin write command but don&#039;t actually write anything, the slide data becomes uninitialized and appears completely red. The check slide command (&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;) also returns &amp;lt;code&amp;gt;0xFF&amp;lt;/code&amp;gt; rather than &amp;lt;code&amp;gt;0xFE&amp;lt;/code&amp;gt; denoting that there is no data at this slide.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!&lt;br /&gt;
!Canvas = 0&lt;br /&gt;
!Canvas = 1&lt;br /&gt;
!Color&lt;br /&gt;
|-&lt;br /&gt;
! rowspan=&amp;quot;3&amp;quot; |Pixel bit value&lt;br /&gt;
|0&lt;br /&gt;
|0&lt;br /&gt;
|Black&lt;br /&gt;
|-&lt;br /&gt;
|1&lt;br /&gt;
|0&lt;br /&gt;
|White&lt;br /&gt;
|-&lt;br /&gt;
|0&lt;br /&gt;
|1&lt;br /&gt;
|Red&lt;br /&gt;
|}&lt;br /&gt;
For entirely black and white only pictures, you still need to write all 0&#039;s on the second canvas. Otherwise, the slide is will be rendered completely red. It&#039;s likely any unwritten data is erased to 0xFF on the flash.&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Santek_EZ_Door_Sign&amp;diff=7685</id>
		<title>Santek EZ Door Sign</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Santek_EZ_Door_Sign&amp;diff=7685"/>
		<updated>2025-03-29T03:07:49Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The Santek EZ Door Sign is cordless e-ink display intended to be mounted outside of offices or rooms. It is capable of storing 5 custom images that can be displayed and rotated with the side button.&lt;br /&gt;
&lt;br /&gt;
The 2.9&amp;quot; variant which I obtained has a resolution of 296 x 128 pixels and is capable of displaying black, white, and red.&lt;br /&gt;
&lt;br /&gt;
== Serial protocol ==&lt;br /&gt;
&lt;br /&gt;
=== Background ===&lt;br /&gt;
Santek provides a Windows based app written in C# that is able to interact with the door signs. My goal is to have some Linux based device that can periodically update these displays (such as the weather or calendar events on an hourly basis). In order to do that, I will need to figure out how exactly this device talks via the USB port.&lt;br /&gt;
&lt;br /&gt;
There are some GitHub projects that try to interface with this device already including https://github.com/m3m0r7/ez-door-sign in PHP and https://github.com/kenichi884/ezsign.py in Python. However, the documentation on the serial protocol is a bit lacking. Fortunately, the protocol doesn&#039;t seem too complicated.&lt;br /&gt;
&lt;br /&gt;
=== Serial configuration ===&lt;br /&gt;
Use 9600 baud. No parity.&lt;br /&gt;
&lt;br /&gt;
The display has to be on. Ensure that the blue LED is lit before trying to talk to it via serial.&lt;br /&gt;
&lt;br /&gt;
=== Commands and messages ===&lt;br /&gt;
Here are some of the supported commands by the display.&lt;br /&gt;
&lt;br /&gt;
Command Type is 0 if it&#039;s a command that&#039;s sent to the display. It will be a 1 if it&#039;s data that&#039;s a response to a command made by the display.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Header&lt;br /&gt;
!Command&lt;br /&gt;
Type&lt;br /&gt;
!Command&lt;br /&gt;
ID&lt;br /&gt;
!Data&lt;br /&gt;
Length&lt;br /&gt;
!Data&lt;br /&gt;
!Chksum&lt;br /&gt;
!Ending&lt;br /&gt;
!Notes&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|slide (1 byte)&lt;br /&gt;
|&amp;lt;code&amp;gt;??&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Redraws the specified slide (0 ~ 4)&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|slide (1 byte)&lt;br /&gt;
|&amp;lt;code&amp;gt;??&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Redraw slide response&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0xFE&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0xFF&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Command next slide&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0xFF&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Command previous slide (called &#039;up&#039; in C# code)&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|slide (1 byte)&lt;br /&gt;
|&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Check slide data&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|status (1 byte)&lt;br /&gt;
|&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Response to the check slide call. Status is one of:&lt;br /&gt;
&lt;br /&gt;
* 0xFF - Slide is blank or unwritten&lt;br /&gt;
* 0xFE - Slide has data&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x03&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x02&amp;lt;/code&amp;gt;&lt;br /&gt;
|slide-canvas&lt;br /&gt;
(2 bytes)&lt;br /&gt;
|&amp;lt;code&amp;gt;??&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Begin sending data for a specific slide and canvas. All data for the slide must be rewritten as everything is wiped.&lt;br /&gt;
Data is always 2 bytes long, containing:&lt;br /&gt;
&lt;br /&gt;
* slide: slide number (0 through 4)&lt;br /&gt;
* canvas: Color canvas (0 or 1)&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x03&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Response to the slide-canvas command.&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x04&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x12&amp;lt;/code&amp;gt;&lt;br /&gt;
|column-bitmap&lt;br /&gt;
(18 bytes)&lt;br /&gt;
|&amp;lt;code&amp;gt;??&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|send image bitmap data by column.&lt;br /&gt;
Data is always 18 bytes long. 2 for column number, 16 for bitmap data.&lt;br /&gt;
&lt;br /&gt;
Bitmap data here is based on the canvas setting in order to get that extra bit for color.&lt;br /&gt;
&lt;br /&gt;
* Column: 2 bytes in decimal. (0 ~ 295) (0x00,0x00 ~ 0x01,0x27)&lt;br /&gt;
* Bitmap: 16 bytes. One bit per pixel makes 128 pixels&lt;br /&gt;
This is how you can retrieve data for the 296x128 pixel display.&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x04&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|did send image row data succeed? Data should be 0x01&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x05&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x04&amp;lt;/code&amp;gt;&lt;br /&gt;
|slide-canvas-column&lt;br /&gt;
(4 bytes)&lt;br /&gt;
|&amp;lt;code&amp;gt;??&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Get image bitmap from display memory.&lt;br /&gt;
Data is always 4 bytes long.&lt;br /&gt;
&lt;br /&gt;
* Slide: Slide number (0 through 4)&lt;br /&gt;
* Canvas: Color canvas (0 or 1)&lt;br /&gt;
* Column: 2 bytes. (0 ~ 295)&lt;br /&gt;
&lt;br /&gt;
To retrieve the actual color, you have to read both canvas&lt;br /&gt;
&lt;br /&gt;
See the response payload below.&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x05&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x12&amp;lt;/code&amp;gt;&lt;br /&gt;
|column-bitmap&lt;br /&gt;
(24 bytes)&lt;br /&gt;
|&amp;lt;code&amp;gt;??&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|Bitmap data received from a get command. The response message is always 24 bytes long.&lt;br /&gt;
There are 18 bytes of payload data and is the same format as the send-image-column command.&lt;br /&gt;
&lt;br /&gt;
* Column: 2 bytes. (0 ~ 295)&lt;br /&gt;
* Bitmap: 16 bytes. One bit per pixel makes 128 pixels&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
|&amp;lt;code&amp;gt;0x06&amp;lt;/code&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|iap model&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;0xBB&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x07&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x08&amp;lt;/code&amp;gt;&lt;br /&gt;
|&amp;lt;code&amp;gt;0x7E&amp;lt;/code&amp;gt;&lt;br /&gt;
|power off&lt;br /&gt;
|}&lt;br /&gt;
=== Writing bitmap information ===&lt;br /&gt;
The display is tri-color and uses 2 bits per pixel to denote color. The 2 bits are written across two separate &#039;canvases&#039; as part of the write operation. When writing an image:&lt;br /&gt;
&lt;br /&gt;
# Call the start write command (&amp;lt;code&amp;gt;0x03&amp;lt;/code&amp;gt;) and specify the desired slide and &amp;lt;code&amp;gt;0x00&amp;lt;/code&amp;gt; canvas&lt;br /&gt;
# Write the bitmap with command &amp;lt;code&amp;gt;0x04&amp;lt;/code&amp;gt;. This command operates column by column, from left to right, top to bottom. There should be 128 pixels (as the display is 128 pixels high). Black and red pixels should be 0. White should be 1. (see table below)&lt;br /&gt;
# Call the start write command (&amp;lt;code&amp;gt;0x03&amp;lt;/code&amp;gt;) again but this time set canvas to &amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;&lt;br /&gt;
# Write the bitmap again with command &amp;lt;code&amp;gt;0x04&amp;lt;/code&amp;gt; as in step 2, but this time, set the bitmap to 0 for black and white and 1 for red.&lt;br /&gt;
&lt;br /&gt;
You must write to all addresses. The memory for the slide appears to get wiped when a write is initiaited. Skipping any of the columns for either canvases will cause the image to become corrupt.&lt;br /&gt;
&lt;br /&gt;
If you call the 0x03 begin write command but don&#039;t actually write anything, the slide data becomes uninitialized and appears completely red. The check slide command (&amp;lt;code&amp;gt;0x01&amp;lt;/code&amp;gt;) also returns &amp;lt;code&amp;gt;0xFF&amp;lt;/code&amp;gt; rather than &amp;lt;code&amp;gt;0xFE&amp;lt;/code&amp;gt; denoting that there is no data at this slide.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!&lt;br /&gt;
!Canvas = 0&lt;br /&gt;
!Canvas = 1&lt;br /&gt;
!Color&lt;br /&gt;
|-&lt;br /&gt;
! rowspan=&amp;quot;3&amp;quot; |Pixel bit value&lt;br /&gt;
|0&lt;br /&gt;
|0&lt;br /&gt;
|Black&lt;br /&gt;
|-&lt;br /&gt;
|1&lt;br /&gt;
|0&lt;br /&gt;
|White&lt;br /&gt;
|-&lt;br /&gt;
|0&lt;br /&gt;
|1&lt;br /&gt;
|Red&lt;br /&gt;
|}&lt;br /&gt;
For black and white only pictures, you can effectively ignore setting canvas=1.&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Santek_EZ_Door_Sign&amp;diff=7684</id>
		<title>Santek EZ Door Sign</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Santek_EZ_Door_Sign&amp;diff=7684"/>
		<updated>2025-03-27T23:50:46Z</updated>

		<summary type="html">&lt;p&gt;Leo: /* Commands */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The Santek EZ Door Sign is cordless e-ink display intended to be mounted outside of offices or rooms. It is capable of storing 5 custom images that can be displayed and rotated with the side button.&lt;br /&gt;
&lt;br /&gt;
The 2.9&amp;quot; variant which I obtained has a resolution of 296 x 128 pixels and is capable of displaying black, white, and red.&lt;br /&gt;
&lt;br /&gt;
== Serial protocol ==&lt;br /&gt;
&lt;br /&gt;
=== Background ===&lt;br /&gt;
Santek provides a Windows based app written in C# that is able to interact with the door signs. My goal is to have some Linux based device that can periodically update these displays (such as the weather or calendar events on an hourly basis). In order to do that, I will need to figure out how exactly this device talks via the USB port.&lt;br /&gt;
&lt;br /&gt;
There are some GitHub projects that try to interface with this device already including https://github.com/m3m0r7/ez-door-sign in PHP and https://github.com/kenichi884/ezsign.py in Python. However, the documentation on the serial protocol is a bit lacking. Fortunately, the protocol doesn&#039;t seem too complicated.&lt;br /&gt;
&lt;br /&gt;
=== Serial configuration ===&lt;br /&gt;
Use 9600 baud. No parity.&lt;br /&gt;
&lt;br /&gt;
The display has to be on. Ensure that the blue LED is lit before trying to talk to it via serial.&lt;br /&gt;
&lt;br /&gt;
=== Commands and messages ===&lt;br /&gt;
Here are some of the supported commands by the display.&lt;br /&gt;
&lt;br /&gt;
Command Type is 0 if it&#039;s a command that&#039;s sent to the display. It will be a 1 if it&#039;s data that&#039;s a response to a command made by the display.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Header&lt;br /&gt;
!Command&lt;br /&gt;
Type&lt;br /&gt;
!Command&lt;br /&gt;
ID&lt;br /&gt;
!Data&lt;br /&gt;
Length&lt;br /&gt;
!Data&lt;br /&gt;
!Chksum&lt;br /&gt;
!Ending&lt;br /&gt;
!Notes&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|slide (1 byte)&lt;br /&gt;
|??&lt;br /&gt;
|0x7E&lt;br /&gt;
|Redraws the specified slide (0 ~ 4)&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x01&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|slide (1 byte)&lt;br /&gt;
|??&lt;br /&gt;
|0x7E&lt;br /&gt;
|Redraw slide response&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|0xFE&lt;br /&gt;
|0xFF&lt;br /&gt;
|0x7E&lt;br /&gt;
|next slide&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|0xFF&lt;br /&gt;
|0x00&lt;br /&gt;
|0x7E&lt;br /&gt;
|previous slide (called &#039;up&#039; in C# code)&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|0x01&lt;br /&gt;
|slide (1 byte)&lt;br /&gt;
|&lt;br /&gt;
|0x7E&lt;br /&gt;
|Check if site is blank. if returned 4th byte is 254, then isBlank is false&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x01&lt;br /&gt;
|0x01&lt;br /&gt;
|0x01&lt;br /&gt;
|status (1 byte)&lt;br /&gt;
|&lt;br /&gt;
|0x7E&lt;br /&gt;
|If the status code returned back is 0xFE, the slide is not blank.&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x03&lt;br /&gt;
|0x02&lt;br /&gt;
|slide-canvas&lt;br /&gt;
(2 bytes)&lt;br /&gt;
|??&lt;br /&gt;
|0x7E&lt;br /&gt;
|Begin sending data for a specific slide and canvas. All data for the slide must be rewritten.&lt;br /&gt;
Data is always 2 bytes long, containing:&lt;br /&gt;
&lt;br /&gt;
* slide: slide number (0 through 4)&lt;br /&gt;
* canvas: Color canvas (0 or 1)&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x01&lt;br /&gt;
|0x03&lt;br /&gt;
|0x01&lt;br /&gt;
|0x01&lt;br /&gt;
|&lt;br /&gt;
|0x7E&lt;br /&gt;
|Response to the slide-canvas command.&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x04&lt;br /&gt;
|0x12&lt;br /&gt;
|column-bitmap&lt;br /&gt;
(18 bytes)&lt;br /&gt;
|??&lt;br /&gt;
|0x7E&lt;br /&gt;
|send image bitmap data by column.&lt;br /&gt;
Data is always 18 bytes long. 2 for column number, 16 for bitmap data.&lt;br /&gt;
&lt;br /&gt;
Bitmap data here is based on the canvas setting in order to get that extra bit for color.&lt;br /&gt;
&lt;br /&gt;
* Column: 2 bytes in decimal. (0 ~ 295) (0x00,0x00 ~ 0x01,0x27)&lt;br /&gt;
* Bitmap: 16 bytes. One bit per pixel makes 128 pixels&lt;br /&gt;
This is how you can retrieve data for the 296x128 pixel display.&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x01&lt;br /&gt;
|0x04&lt;br /&gt;
|0x01&lt;br /&gt;
|0x01&lt;br /&gt;
|&lt;br /&gt;
|0x7E&lt;br /&gt;
|did send image row data succeed? Data should be 0x01&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x05&lt;br /&gt;
|0x04&lt;br /&gt;
|slide-canvas-column&lt;br /&gt;
(4 bytes)&lt;br /&gt;
|??&lt;br /&gt;
|0x7E&lt;br /&gt;
|Get image bitmap from display memory.&lt;br /&gt;
Data is always 4 bytes long.&lt;br /&gt;
&lt;br /&gt;
* Slide: Slide number (0 through 4)&lt;br /&gt;
* Canvas: Color canvas (0 or 1)&lt;br /&gt;
* Column: 2 bytes. (0 ~ 295)&lt;br /&gt;
&lt;br /&gt;
To retrieve the actual color, you have to read both canvas&lt;br /&gt;
&lt;br /&gt;
See the response payload below.&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x01&lt;br /&gt;
|0x05&lt;br /&gt;
|0x12&lt;br /&gt;
|column-bitmap&lt;br /&gt;
(24 bytes)&lt;br /&gt;
|??&lt;br /&gt;
|0x7E&lt;br /&gt;
|Bitmap data received from a get command. The response message is always 24 bytes long.&lt;br /&gt;
There are 18 bytes of payload data and is the same format as the send-image-column command.&lt;br /&gt;
&lt;br /&gt;
* Column: 2 bytes. (0 ~ 295)&lt;br /&gt;
* Bitmap: 16 bytes. One bit per pixel makes 128 pixels&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|&lt;br /&gt;
|0x06&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|0x7E&lt;br /&gt;
|iap model&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x07&lt;br /&gt;
|0x01&lt;br /&gt;
|0x00&lt;br /&gt;
|0x08&lt;br /&gt;
|0x7E&lt;br /&gt;
|power off&lt;br /&gt;
|}&lt;br /&gt;
=== Writing bitmap information ===&lt;br /&gt;
The display is tri-color and uses 2 bits per pixel to denote color. The 2 bits are written across two separate &#039;canvases&#039; as part of the write operation. When writing an image:&lt;br /&gt;
&lt;br /&gt;
# Set the canvas to 0&lt;br /&gt;
# Write the bitmap (column by column, from left to right, top to bottom). There should be 128 pixels (as the display is 128 pixels high). Black and red pixels should be 0. White should be 1. (see table below)&lt;br /&gt;
# Set the canvas to 1&lt;br /&gt;
# Write the bitmap like in step 2, but set the bitmap to 0 for black and white and 1 for red.&lt;br /&gt;
&lt;br /&gt;
You must write to all addresses. The memory for the slide appears to get wiped when a write is initiaited. Skipping any of the columns for either canvases will cause the image to become corrupt.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!&lt;br /&gt;
!Canvas = 0&lt;br /&gt;
!Canvas = 1&lt;br /&gt;
!Color&lt;br /&gt;
|-&lt;br /&gt;
! rowspan=&amp;quot;3&amp;quot; |Pixel bit value&lt;br /&gt;
|0&lt;br /&gt;
|0&lt;br /&gt;
|Black&lt;br /&gt;
|-&lt;br /&gt;
|1&lt;br /&gt;
|0&lt;br /&gt;
|White&lt;br /&gt;
|-&lt;br /&gt;
|0&lt;br /&gt;
|1&lt;br /&gt;
|Red&lt;br /&gt;
|}&lt;br /&gt;
For black and white only pictures, you can effectively ignore setting canvas=1.&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Santek_EZ_Door_Sign&amp;diff=7683</id>
		<title>Santek EZ Door Sign</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Santek_EZ_Door_Sign&amp;diff=7683"/>
		<updated>2025-03-27T07:05:31Z</updated>

		<summary type="html">&lt;p&gt;Leo: /* Commands */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The Santek EZ Door Sign is cordless e-ink display intended to be mounted outside of offices or rooms. It is capable of storing 5 custom images that can be displayed and rotated with the side button.&lt;br /&gt;
&lt;br /&gt;
The 2.9&amp;quot; variant which I obtained has a resolution of 296 x 128 pixels and is capable of displaying black, white, and red.&lt;br /&gt;
&lt;br /&gt;
== Serial protocol ==&lt;br /&gt;
&lt;br /&gt;
=== Background ===&lt;br /&gt;
Santek provides a Windows based app written in C# that is able to interact with the door signs. My goal is to have some Linux based device that can periodically update these displays (such as the weather or calendar events on an hourly basis). In order to do that, I will need to figure out how exactly this device talks via the USB port.&lt;br /&gt;
&lt;br /&gt;
There are some GitHub projects that try to interface with this device already including https://github.com/m3m0r7/ez-door-sign in PHP and https://github.com/kenichi884/ezsign.py in Python. However, the documentation on the serial protocol is a bit lacking. Fortunately, the protocol doesn&#039;t seem too complicated.&lt;br /&gt;
&lt;br /&gt;
=== Serial configuration ===&lt;br /&gt;
Use 9600 baud. No parity.&lt;br /&gt;
&lt;br /&gt;
The display has to be on. Ensure that the blue LED is lit before trying to talk to it via serial.&lt;br /&gt;
&lt;br /&gt;
=== Commands ===&lt;br /&gt;
Here are some of the supported commands by the display.&lt;br /&gt;
&lt;br /&gt;
Command Type is 0 if it&#039;s a command that&#039;s sent to the display. It will be a 1 if it&#039;s data that&#039;s a response to a command made by the display.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Header&lt;br /&gt;
!Command&lt;br /&gt;
Type&lt;br /&gt;
!Command&lt;br /&gt;
ID&lt;br /&gt;
!Data&lt;br /&gt;
Length&lt;br /&gt;
!Data&lt;br /&gt;
!Chksum&lt;br /&gt;
!Ending&lt;br /&gt;
!Notes&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x03&lt;br /&gt;
|0x02&lt;br /&gt;
|site-canvas&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|set image head data. This specifies which canvas you are overwriting.&lt;br /&gt;
Data is always 2 bytes long, containing:&lt;br /&gt;
&lt;br /&gt;
* site: slide number (0 through 4)&lt;br /&gt;
* canvas: Color canvas (0 or 1)&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x01&lt;br /&gt;
|0x03&lt;br /&gt;
|0x01&lt;br /&gt;
|0x01&lt;br /&gt;
|&lt;br /&gt;
|0x7E&lt;br /&gt;
|did set image head data succeed? Data should be 0x01&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x04&lt;br /&gt;
|0x12&lt;br /&gt;
|column-bitmap&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|send image bitmap data by column.&lt;br /&gt;
Data is always 18 bytes long. 2 for column number, 16 for bitmap data.&lt;br /&gt;
&lt;br /&gt;
Bitmap data here is based on the canvas setting in order to get that extra bit for color.&lt;br /&gt;
&lt;br /&gt;
* Column: 2 bytes in decimal. (0 ~ 295) (0x00,0x00 ~ 0x01,0x27)&lt;br /&gt;
* Bitmap: 16 bytes. One bit per pixel makes 128 pixels&lt;br /&gt;
This is how you can retrieve data for the 296x128 pixel display.&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x01&lt;br /&gt;
|0x04&lt;br /&gt;
|0x01&lt;br /&gt;
|0x01&lt;br /&gt;
|&lt;br /&gt;
|0x7E&lt;br /&gt;
|did send image row data succeed? Data should be 0x01&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x05&lt;br /&gt;
|0x04&lt;br /&gt;
|site-canvas-column&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|Get image bitmap from display memory.&lt;br /&gt;
Data is always 4 bytes long.&lt;br /&gt;
&lt;br /&gt;
* Site: Slide number (0 through 4)&lt;br /&gt;
* Canvas: Color canvas (0 or 1)&lt;br /&gt;
* Column: 2 bytes. (0 ~ 295)&lt;br /&gt;
&lt;br /&gt;
To retrieve the actual color, you have to read both canvas&lt;br /&gt;
&lt;br /&gt;
See the response payload below.&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x01&lt;br /&gt;
|0x05&lt;br /&gt;
|0x12&lt;br /&gt;
|column-bitmap&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|Bitmap data received from a get command. The response message is always 24 bytes long.&lt;br /&gt;
There are 18 bytes of payload data and is the same format as the send-image-column command.&lt;br /&gt;
&lt;br /&gt;
* Column: 2 bytes. (0 ~ 295)&lt;br /&gt;
* Bitmap: 16 bytes. One bit per pixel makes 128 pixels&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|&lt;br /&gt;
|0x06&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|0x7E&lt;br /&gt;
|iap model&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x07&lt;br /&gt;
|0x01&lt;br /&gt;
|0x00&lt;br /&gt;
|0x08&lt;br /&gt;
|0x7E&lt;br /&gt;
|power off&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|0xFE&lt;br /&gt;
|0xFF&lt;br /&gt;
|0x7E&lt;br /&gt;
|next slide&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|0xFF&lt;br /&gt;
|0x00&lt;br /&gt;
|0x7E&lt;br /&gt;
|previous slide (called &#039;up&#039; in C# code)&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|site&lt;br /&gt;
|site+1&lt;br /&gt;
|0x7E&lt;br /&gt;
|Redraws the slide (0 ~ 4)&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x01&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|site&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|Redrew the slide ok.&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|0x01&lt;br /&gt;
|site&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|Check if site is blank. if returned 4th byte is 254, then isBlank is false&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x01&lt;br /&gt;
|0x01&lt;br /&gt;
|0x01&lt;br /&gt;
|site&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|site given bacak is 0xFE, meaning slide is not blank.&lt;br /&gt;
|}&lt;br /&gt;
The checksum is calculated by summing everything &lt;br /&gt;
*&lt;br /&gt;
&lt;br /&gt;
When reading data...&lt;br /&gt;
&lt;br /&gt;
* read and discard until you see 187&lt;br /&gt;
* ???&lt;br /&gt;
&lt;br /&gt;
=== Color data ===&lt;br /&gt;
The &amp;lt;code&amp;gt;site-canvas-row&amp;lt;/code&amp;gt; data that you read/write using the commands above determine the color. Because we only use one bit per canvas, the canvas is pretty much how you define an additional bit per pixel.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!&lt;br /&gt;
!Canvas = 0&lt;br /&gt;
!Canvas = 1&lt;br /&gt;
!Color&lt;br /&gt;
|-&lt;br /&gt;
! rowspan=&amp;quot;3&amp;quot; |Pixel bit value&lt;br /&gt;
|0&lt;br /&gt;
|0&lt;br /&gt;
|Black&lt;br /&gt;
|-&lt;br /&gt;
|1&lt;br /&gt;
|0&lt;br /&gt;
|White&lt;br /&gt;
|-&lt;br /&gt;
|0&lt;br /&gt;
|1&lt;br /&gt;
|Red&lt;br /&gt;
|}&lt;br /&gt;
For black and white only pictures, you can effectively ignore setting canvas=1.&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Santek_EZ_Door_Sign&amp;diff=7682</id>
		<title>Santek EZ Door Sign</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Santek_EZ_Door_Sign&amp;diff=7682"/>
		<updated>2025-03-27T06:54:59Z</updated>

		<summary type="html">&lt;p&gt;Leo: /* Commands */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The Santek EZ Door Sign is cordless e-ink display intended to be mounted outside of offices or rooms. It is capable of storing 5 custom images that can be displayed and rotated with the side button.&lt;br /&gt;
&lt;br /&gt;
The 2.9&amp;quot; variant which I obtained has a resolution of 296 x 128 pixels and is capable of displaying black, white, and red.&lt;br /&gt;
&lt;br /&gt;
== Serial protocol ==&lt;br /&gt;
&lt;br /&gt;
=== Background ===&lt;br /&gt;
Santek provides a Windows based app written in C# that is able to interact with the door signs. My goal is to have some Linux based device that can periodically update these displays (such as the weather or calendar events on an hourly basis). In order to do that, I will need to figure out how exactly this device talks via the USB port.&lt;br /&gt;
&lt;br /&gt;
There are some GitHub projects that try to interface with this device already including https://github.com/m3m0r7/ez-door-sign in PHP and https://github.com/kenichi884/ezsign.py in Python. However, the documentation on the serial protocol is a bit lacking. Fortunately, the protocol doesn&#039;t seem too complicated.&lt;br /&gt;
&lt;br /&gt;
=== Serial configuration ===&lt;br /&gt;
Use 9600 baud. No parity.&lt;br /&gt;
&lt;br /&gt;
The display has to be on. Ensure that the blue LED is lit before trying to talk to it via serial.&lt;br /&gt;
&lt;br /&gt;
=== Commands ===&lt;br /&gt;
Here are some of the supported commands by the display.&lt;br /&gt;
&lt;br /&gt;
Command Type is 0 if it&#039;s a command that&#039;s sent to the display. It will be a 1 if it&#039;s data that&#039;s a response to a command made by the display.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Header&lt;br /&gt;
!Command&lt;br /&gt;
Type&lt;br /&gt;
!Command&lt;br /&gt;
ID&lt;br /&gt;
!Data&lt;br /&gt;
Length&lt;br /&gt;
!Data&lt;br /&gt;
!Chksum&lt;br /&gt;
!Ending&lt;br /&gt;
!Notes&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x03&lt;br /&gt;
|0x02&lt;br /&gt;
|site-canvas&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|set image head data. This specifies which canvas you are overwriting.&lt;br /&gt;
Data is always 2 bytes long, containing:&lt;br /&gt;
&lt;br /&gt;
* site: slide number (0 through 4)&lt;br /&gt;
* canvas: Color canvas (0 or 1)&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x01&lt;br /&gt;
|0x03&lt;br /&gt;
|0x01&lt;br /&gt;
|0x01&lt;br /&gt;
|&lt;br /&gt;
|0x7E&lt;br /&gt;
|did set image head data succeed? Data should be 0x01&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x04&lt;br /&gt;
|0x12&lt;br /&gt;
|column-bitmap&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|send image bitmap data by column.&lt;br /&gt;
Data is always 18 bytes long. 2 for column number, 16 for bitmap data.&lt;br /&gt;
&lt;br /&gt;
Bitmap data here is based on the canvas setting in order to get that extra bit for color.&lt;br /&gt;
&lt;br /&gt;
* Column: 2 bytes in decimal. (0 ~ 295) (0x00,0x00 ~ 0x01,0x27)&lt;br /&gt;
* Bitmap: 16 bytes. One bit per pixel makes 128 pixels&lt;br /&gt;
This is how you can retrieve data for the 296x128 pixel display.&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x01&lt;br /&gt;
|0x04&lt;br /&gt;
|0x01&lt;br /&gt;
|0x01&lt;br /&gt;
|&lt;br /&gt;
|0x7E&lt;br /&gt;
|did send image row data succeed? Data should be 0x01&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x05&lt;br /&gt;
|0x04&lt;br /&gt;
|site-canvas-column&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|Get image bitmap from display memory.&lt;br /&gt;
Data is always 4 bytes long.&lt;br /&gt;
&lt;br /&gt;
* Site: Slide number (0 through 4)&lt;br /&gt;
* Canvas: Color canvas (0 or 1)&lt;br /&gt;
* Column: 2 bytes. (0 ~ 295)&lt;br /&gt;
&lt;br /&gt;
To retrieve the actual color, you have to read both canvas&lt;br /&gt;
&lt;br /&gt;
See the response payload below.&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x01&lt;br /&gt;
|0x05&lt;br /&gt;
|0x12&lt;br /&gt;
|column-bitmap&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|Bitmap data received from a get command. The response message is always 24 bytes long.&lt;br /&gt;
There are 18 bytes of payload data and is the same format as the send-image-column command.&lt;br /&gt;
&lt;br /&gt;
* Column: 2 bytes. (0 ~ 295)&lt;br /&gt;
* Bitmap: 16 bytes. One bit per pixel makes 128 pixels&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|&lt;br /&gt;
|0x06&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|0x7E&lt;br /&gt;
|iap model&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x07&lt;br /&gt;
|0x01&lt;br /&gt;
|0x00&lt;br /&gt;
|0x08&lt;br /&gt;
|0x7E&lt;br /&gt;
|power off&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|0xFE&lt;br /&gt;
|0xFF&lt;br /&gt;
|0x7E&lt;br /&gt;
|next slide&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|0xFF&lt;br /&gt;
|0x00&lt;br /&gt;
|0x7E&lt;br /&gt;
|previous slide (called &#039;up&#039; in C# code)&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|site&lt;br /&gt;
|site+1&lt;br /&gt;
|0x7E&lt;br /&gt;
|Redraws the slide&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|0x01&lt;br /&gt;
|site&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|site is blank. if 4th byte is 254, then isBlank is false&lt;br /&gt;
|}&lt;br /&gt;
The checksum is calculated by summing everything &lt;br /&gt;
*&lt;br /&gt;
&lt;br /&gt;
When reading data...&lt;br /&gt;
&lt;br /&gt;
* read and discard until you see 187&lt;br /&gt;
* ???&lt;br /&gt;
&lt;br /&gt;
=== Color data ===&lt;br /&gt;
The &amp;lt;code&amp;gt;site-canvas-row&amp;lt;/code&amp;gt; data that you read/write using the commands above determine the color. Because we only use one bit per canvas, the canvas is pretty much how you define an additional bit per pixel.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!&lt;br /&gt;
!Canvas = 0&lt;br /&gt;
!Canvas = 1&lt;br /&gt;
!Color&lt;br /&gt;
|-&lt;br /&gt;
! rowspan=&amp;quot;3&amp;quot; |Pixel bit value&lt;br /&gt;
|0&lt;br /&gt;
|0&lt;br /&gt;
|Black&lt;br /&gt;
|-&lt;br /&gt;
|1&lt;br /&gt;
|0&lt;br /&gt;
|White&lt;br /&gt;
|-&lt;br /&gt;
|0&lt;br /&gt;
|1&lt;br /&gt;
|Red&lt;br /&gt;
|}&lt;br /&gt;
For black and white only pictures, you can effectively ignore setting canvas=1.&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Santek_EZ_Door_Sign&amp;diff=7681</id>
		<title>Santek EZ Door Sign</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Santek_EZ_Door_Sign&amp;diff=7681"/>
		<updated>2025-03-27T06:02:24Z</updated>

		<summary type="html">&lt;p&gt;Leo: /* Commands */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The Santek EZ Door Sign is cordless e-ink display intended to be mounted outside of offices or rooms. It is capable of storing 5 custom images that can be displayed and rotated with the side button.&lt;br /&gt;
&lt;br /&gt;
The 2.9&amp;quot; variant which I obtained has a resolution of 296 x 128 pixels and is capable of displaying black, white, and red.&lt;br /&gt;
&lt;br /&gt;
== Serial protocol ==&lt;br /&gt;
&lt;br /&gt;
=== Background ===&lt;br /&gt;
Santek provides a Windows based app written in C# that is able to interact with the door signs. My goal is to have some Linux based device that can periodically update these displays (such as the weather or calendar events on an hourly basis). In order to do that, I will need to figure out how exactly this device talks via the USB port.&lt;br /&gt;
&lt;br /&gt;
There are some GitHub projects that try to interface with this device already including https://github.com/m3m0r7/ez-door-sign in PHP and https://github.com/kenichi884/ezsign.py in Python. However, the documentation on the serial protocol is a bit lacking. Fortunately, the protocol doesn&#039;t seem too complicated.&lt;br /&gt;
&lt;br /&gt;
=== Serial configuration ===&lt;br /&gt;
Use 9600 baud. No parity.&lt;br /&gt;
&lt;br /&gt;
The display has to be on. Ensure that the blue LED is lit before trying to talk to it via serial.&lt;br /&gt;
&lt;br /&gt;
=== Commands ===&lt;br /&gt;
Here are some of the supported commands by the display.&lt;br /&gt;
&lt;br /&gt;
Command Type is 0 if it&#039;s a command that&#039;s sent to the display. It will be a 1 if it&#039;s data that&#039;s a response to a command made by the display.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Header&lt;br /&gt;
!Command&lt;br /&gt;
Type&lt;br /&gt;
!Command&lt;br /&gt;
ID&lt;br /&gt;
!Data&lt;br /&gt;
Length&lt;br /&gt;
!Data&lt;br /&gt;
!Chksum&lt;br /&gt;
!Ending&lt;br /&gt;
!Notes&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x03&lt;br /&gt;
|0x02&lt;br /&gt;
|site-canvas&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|set image head data. This specifies which canvas you are overwriting.&lt;br /&gt;
Data is always 2 bytes long, containing:&lt;br /&gt;
&lt;br /&gt;
* site: slide number (0 through 4)&lt;br /&gt;
* canvas: Color canvas (0 or 1)&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x04&lt;br /&gt;
|0x12&lt;br /&gt;
|row-bitmap&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|send image row data.&lt;br /&gt;
Data is always 18 bytes long. 2 for row number, 16 for bitmap data.&lt;br /&gt;
Bitmap data here is based on the canvas setting (row above)&lt;br /&gt;
&lt;br /&gt;
* Row: 2 bytes in decimal. 0x01, 0x27 = 127&lt;br /&gt;
* Bitmap: 16 bytes. One bit per pixel makes 128 pixels&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x05&lt;br /&gt;
|0x04&lt;br /&gt;
|site-canvas-row&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|Get image bitmap from display memory.&lt;br /&gt;
Data is always 4 bytes long.&lt;br /&gt;
&lt;br /&gt;
* Site: Slide number (0 through 4)&lt;br /&gt;
* Canvas: Color canvas (0 or 1)&lt;br /&gt;
* Row: 2 bytes in decimal. 0x01, 0x27 = 127&lt;br /&gt;
&lt;br /&gt;
To retrieve the actual color, you have to read both canvas&lt;br /&gt;
&lt;br /&gt;
See the response payload below.&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x01&lt;br /&gt;
|0x05&lt;br /&gt;
|0x12&lt;br /&gt;
|row-bitmap&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|Bitmap data received from a get command. The response message is always 24 bytes long.&lt;br /&gt;
There are 18 bytes of payload data and is the same format as the send-image-row command.&lt;br /&gt;
&lt;br /&gt;
* Row: 2 bytes in decimal. 0x01, 0x27 = 127&lt;br /&gt;
* Bitmap: 16 bytes. One bit per pixel makes 128 pixels&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|&lt;br /&gt;
|0x06&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|0x7E&lt;br /&gt;
|iap model&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x07&lt;br /&gt;
|0x01&lt;br /&gt;
|0x00&lt;br /&gt;
|0x08&lt;br /&gt;
|0x7E&lt;br /&gt;
|power off&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|0xFE&lt;br /&gt;
|0xFF&lt;br /&gt;
|0x7E&lt;br /&gt;
|next slide&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|0xFF&lt;br /&gt;
|0x00&lt;br /&gt;
|0x7E&lt;br /&gt;
|previous slide (called &#039;up&#039; in C# code)&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|site&lt;br /&gt;
|site+1&lt;br /&gt;
|0x7E&lt;br /&gt;
|Redraws the slide&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|0x01&lt;br /&gt;
|site&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|site is blank. if 4th byte is 254, then isBlank is false&lt;br /&gt;
|}&lt;br /&gt;
The checksum is calculated by summing everything &lt;br /&gt;
*&lt;br /&gt;
&lt;br /&gt;
When reading data...&lt;br /&gt;
&lt;br /&gt;
* read and discard until you see 187&lt;br /&gt;
* ???&lt;br /&gt;
&lt;br /&gt;
=== Color data ===&lt;br /&gt;
The &amp;lt;code&amp;gt;site-canvas-row&amp;lt;/code&amp;gt; data that you read/write using the commands above determine the color. Because we only use one bit per canvas, the canvas is pretty much how you define an additional bit per pixel.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!&lt;br /&gt;
!Canvas = 0&lt;br /&gt;
!Canvas = 1&lt;br /&gt;
!Color&lt;br /&gt;
|-&lt;br /&gt;
! rowspan=&amp;quot;3&amp;quot; |Pixel bit value&lt;br /&gt;
|0&lt;br /&gt;
|0&lt;br /&gt;
|Black&lt;br /&gt;
|-&lt;br /&gt;
|1&lt;br /&gt;
|0&lt;br /&gt;
|White&lt;br /&gt;
|-&lt;br /&gt;
|0&lt;br /&gt;
|1&lt;br /&gt;
|Red&lt;br /&gt;
|}&lt;br /&gt;
For black and white only pictures, you can effectively ignore setting canvas=1.&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Santek_EZ_Door_Sign&amp;diff=7680</id>
		<title>Santek EZ Door Sign</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Santek_EZ_Door_Sign&amp;diff=7680"/>
		<updated>2025-03-27T05:09:05Z</updated>

		<summary type="html">&lt;p&gt;Leo: Initial content&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The Santek EZ Door Sign is cordless e-ink display intended to be mounted outside of offices or rooms. It is capable of storing 5 custom images that can be displayed and rotated with the side button.&lt;br /&gt;
&lt;br /&gt;
The 2.9&amp;quot; variant which I obtained has a resolution of 296 x 128 pixels and is capable of displaying black, white, and red.&lt;br /&gt;
&lt;br /&gt;
== Serial protocol ==&lt;br /&gt;
&lt;br /&gt;
=== Background ===&lt;br /&gt;
Santek provides a Windows based app written in C# that is able to interact with the door signs. My goal is to have some Linux based device that can periodically update these displays (such as the weather or calendar events on an hourly basis). In order to do that, I will need to figure out how exactly this device talks via the USB port.&lt;br /&gt;
&lt;br /&gt;
There are some GitHub projects that try to interface with this device already including https://github.com/m3m0r7/ez-door-sign in PHP and https://github.com/kenichi884/ezsign.py in Python. However, the documentation on the serial protocol is a bit lacking. Fortunately, the protocol doesn&#039;t seem too complicated.&lt;br /&gt;
&lt;br /&gt;
=== Serial configuration ===&lt;br /&gt;
9600 baud?&lt;br /&gt;
&lt;br /&gt;
Expects CH343 device id&lt;br /&gt;
&lt;br /&gt;
=== Commands ===&lt;br /&gt;
Here are some of the supported commands by the display.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Header&lt;br /&gt;
!Command&lt;br /&gt;
Type&lt;br /&gt;
!Command&lt;br /&gt;
ID&lt;br /&gt;
!Data&lt;br /&gt;
Length&lt;br /&gt;
!Data&lt;br /&gt;
!Chksum&lt;br /&gt;
!Ending&lt;br /&gt;
!Notes&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x03&lt;br /&gt;
|0x02&lt;br /&gt;
|site-canvas&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|set image head data. This specifies which canvas you are overwriting.&lt;br /&gt;
Data is always 2 bytes long, containing:&lt;br /&gt;
&lt;br /&gt;
* site: slide number (0 through 4)&lt;br /&gt;
* canvas: Color canvas (0 or 1)&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x04&lt;br /&gt;
|0x12&lt;br /&gt;
|row-bitmap&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|send image row data.&lt;br /&gt;
Data is always 18 bytes long. 2 for row number, 16 for bitmap data.&lt;br /&gt;
Bitmap data here is based on the canvas setting (row above)&lt;br /&gt;
&lt;br /&gt;
* Row: 2 bytes in decimal. 0x01, 0x27 = 127&lt;br /&gt;
* Bitmap: 16 bytes. One bit per pixel makes 128 pixels&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x05&lt;br /&gt;
|0x04&lt;br /&gt;
|site-canvas-row&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|Get image bitmap from display memory.&lt;br /&gt;
Data is always 4 bytes long.&lt;br /&gt;
&lt;br /&gt;
* Site: Slide number (0 through 4)&lt;br /&gt;
* Canvas: Color canvas (0 or 1)&lt;br /&gt;
* Row: 2 bytes in decimal. 0x01, 0x27 = 127&lt;br /&gt;
&lt;br /&gt;
To retrieve the actual color, you have to read both canvas&lt;br /&gt;
&lt;br /&gt;
Data that&#039;s returned is...???&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|&lt;br /&gt;
|0x06&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|0x7E&lt;br /&gt;
|iap model&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x07&lt;br /&gt;
|0x01&lt;br /&gt;
|0x00&lt;br /&gt;
|0x08&lt;br /&gt;
|0x7E&lt;br /&gt;
|power off&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|0xFE&lt;br /&gt;
|0xFF&lt;br /&gt;
|0x7E&lt;br /&gt;
|next slide&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|0xFF&lt;br /&gt;
|0x00&lt;br /&gt;
|0x7E&lt;br /&gt;
|previous slide (called &#039;up&#039; in C# code)&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|site&lt;br /&gt;
|site+1&lt;br /&gt;
|0x7E&lt;br /&gt;
|&amp;quot;assign site&amp;quot;. Likely redraws the given slide #. Eg. slide=0&lt;br /&gt;
|-&lt;br /&gt;
|0xBB&lt;br /&gt;
|0x00&lt;br /&gt;
|0x01&lt;br /&gt;
|0x01&lt;br /&gt;
|site&lt;br /&gt;
|variable&lt;br /&gt;
|0x7E&lt;br /&gt;
|site is blank. if 4th byte is 254, then isBlank is false&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
*&lt;br /&gt;
&lt;br /&gt;
When reading data...&lt;br /&gt;
&lt;br /&gt;
* read and discard until you see 187&lt;br /&gt;
* ???&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
	<entry>
		<id>https://leo.leung.xyz/wiki/index.php?title=Govee_H5054&amp;diff=7679</id>
		<title>Govee H5054</title>
		<link rel="alternate" type="text/html" href="https://leo.leung.xyz/wiki/index.php?title=Govee_H5054&amp;diff=7679"/>
		<updated>2025-03-22T05:25:29Z</updated>

		<summary type="html">&lt;p&gt;Leo: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Govee H5054 is a wireless water sensor to detect water leaks. It uses the 433 MHz radio to transmit events.&lt;br /&gt;
&lt;br /&gt;
== RTL 433 ==&lt;br /&gt;
See the source code here: https://github.com/merbanan/rtl_433/blob/master/src/devices/govee.c&lt;br /&gt;
&lt;br /&gt;
Here&#039;s a sample output from rtl_433 for this device:&lt;br /&gt;
{{highlight|lang=text|code=&lt;br /&gt;
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ &lt;br /&gt;
time      : 2025-03-21 18:13:09&lt;br /&gt;
model     : Govee-Water  id        : 14029&lt;br /&gt;
event     : Button Press detect_wet: 0             Raw Code  : 36cd30547e22  Integrity : CRC&lt;br /&gt;
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ &lt;br /&gt;
time      : 2025-03-21 18:13:11&lt;br /&gt;
model     : Govee-Water  id        : 14029&lt;br /&gt;
Battery level: 0.050     Battery   : 1860 mV       event     : Battery Report Raw Code  : 36cd310507c7 Integrity : CRC&lt;br /&gt;
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ &lt;br /&gt;
time      : 2025-03-21 18:13:12&lt;br /&gt;
model     : Govee-Water  id        : 14029&lt;br /&gt;
Battery level: 0.050     Battery   : 1860 mV       event     : Battery Report Raw Code  : 36cd310507c7 Integrity : CRC&lt;br /&gt;
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ &lt;br /&gt;
time      : 2025-03-21 18:13:14&lt;br /&gt;
model     : Govee-Water  id        : 14029&lt;br /&gt;
event     : Button Press detect_wet: 0             Raw Code  : 36cd30547e22  Integrity : CRC&lt;br /&gt;
&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Home assistant integration ==&lt;br /&gt;
The rtl_433 utility is able to listen for and decode specific 433 MHz radio transmission messages using a RTL-SDR USB dongle. It supports sending decoded messages to various destinations including MQTT. As a result, we can use rtl_433 to effectively act as a gateway between 433 MHz radio transmissions and our Home Assistant MQTT broker.&lt;br /&gt;
&lt;br /&gt;
For my personal setup, I have a Nooelec RTL-SDR USB dongle plugged into a OpenWRT router. The rtl_433 package is available on OpenWRT 24&#039;s package repos. You can create a procd init script that runs rtl_433 within OpenWRT by creating a file in /etc/init.d/rtl_433 with the following contents:&lt;br /&gt;
&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = #!/bin/sh /etc/rc.common&lt;br /&gt;
USE_PROCD=1&lt;br /&gt;
START=95&lt;br /&gt;
STOP=01&lt;br /&gt;
&lt;br /&gt;
start_service() {&lt;br /&gt;
    procd_open_instance&lt;br /&gt;
    procd_set_param command /usr/bin/rtl_433 -C si -R 231 -F &amp;quot;mqtt://mqtt-broker-server:1883,devices=rtl_433/Govee-Water[/id]&amp;quot;&lt;br /&gt;
    procd_close_instance&lt;br /&gt;
}&lt;br /&gt;
| lang = text&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
If you are not using OpenWRT or if you want to run the command manually, run the following:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # rtl_433 -C si  -R 231 -F mqtt://mqtt-broker-server:1883,devices=rtl_433/Govee-Water[/id]&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}&lt;br /&gt;
Breaking the command down, the arguments are as follows: &lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;-C si&amp;lt;/code&amp;gt;: Convert units to scientific units&lt;br /&gt;
* &amp;lt;code&amp;gt;-R 231&amp;lt;/code&amp;gt;: Decode only Govee H5054 messages. You can list all the supported messages by passing -R help.&lt;br /&gt;
* &amp;lt;code&amp;gt;-F&amp;lt;/code&amp;gt; specifies the output. In our case, we want to use MQTT without authentication using the topic &amp;lt;code&amp;gt;rtl_433/Govee-Water/&amp;lt;device-id&amp;gt;&amp;lt;/code&amp;gt;. rtl_433 will replace &amp;lt;code&amp;gt;[/id]&amp;lt;/code&amp;gt; with the actual device id.&lt;br /&gt;
&lt;br /&gt;
Once rtl_433 is running, we will need to get Home Assistant configured for these sensors. Add the MQTT integration in Home Assistant. &lt;br /&gt;
&lt;br /&gt;
Next, use the auto-discovery script at: https://github.com/mekaneck/Govee-Water-Leak-Home-Assistant-Autodiscovery. The installation involves copying and pasting the script into the Home Assistant configuration file. The script contains a list of devices that should be added. You will need to determine the random 5 digit device ID that the sensors are set to from the factory, their serial number (optional but helpful for you to identify the sensors later on), and a friendly name. Save the configuration file, restart Home Assistant, and then run the script (under Settings -&amp;gt; Automation -&amp;gt; Scripts). After running the script, you should see the sensors appear under the MQTT integration.&lt;br /&gt;
&lt;br /&gt;
If you need help determining the 5 digit device ID for each sensor, you can run rtl_433 without the &amp;lt;code&amp;gt;-F&amp;lt;/code&amp;gt; flag (so that it outputs directly to standard out) and then press the sensor button.  You should be able to see the device id in the output. For example, 14029:&lt;br /&gt;
{{Highlight&lt;br /&gt;
| code = # rtl_433 -C si&lt;br /&gt;
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _&lt;br /&gt;
time      : 2025-03-21 21:49:08&lt;br /&gt;
model     : Govee-Water  id        : 14029&lt;br /&gt;
event     : Button Press detect_wet: 0             Raw Code  : 36cd30547e22  Integrity : CRC&lt;br /&gt;
| lang = terminal&lt;br /&gt;
}}The battery values are only reported when the device is first powered on, when the device is chirping due to low battery power, or about once per day. If Home Assistant isn&#039;t reporting the battery readings, try reseating the battery.&lt;br /&gt;
&lt;br /&gt;
== Other notes ==&lt;br /&gt;
The device will begin chirping when the battery voltage drops below 2 volts (verified with a bench power supply). Every time it chirps, it will broadcast its battery reading values.&lt;/div&gt;</summary>
		<author><name>Leo</name></author>
	</entry>
</feed>