June 28, 2021 ( last updated : June 29, 2021 )
octopi
ansible
devops
I’ve recently upgraded 3d printers to a Creality Ender 3 v2 and on the advice of a long time co-conspirator immediately departed down the path of setting up OctoPrint via OctoPi. This is great and I highly recommend it, one button printing without getting out of your chair is addictive. By default OctoPi generates self-signed certificates and allows both secure and insecure connections. We can do better.
Firstly, for any moderately sane person this process is well documented on a couple of excellent forum posts:
Having said this, handling certificates shouldn’t be a manual process - you forget how to do it and make mistakes and don’t want to do it regularly. So to the automation machine!
There are stacks of guides on how to create certificates so I won’t be re hashing that. For OctoPi the above resources are a great starting point. For what it’s worth, I’m using cfssl
to generate certificates from an intermediate certificate.
The great thing about working from OctoPi is that we have a relatively constant, known platform to work from. This code only needs to work on this device and we do not need to care for edge cases of trying to deploy Python 3 on Mandrake or anything else horrible.
Firstly, we need to get the certificates onto the OctoPi host. At present, my certificates are stored locally (that automation is coming…) so the simple approach is to directly use the copy
module:
- name: Certificates | Place the certificate in the correct location on the host
copy:
src: certs/octo_one-bundle.pem
dest: /etc/ssl/octo_one-bundle.pem
register: cert1
- name: Certificates | Place the CA bundle in the correct location on the host
copy:
src: certs/ca-bundle.pem
dest: /etc/ssl/ca-bundle.pem
You may notice that I have elected to copy both the certificate and the CA bundle to the host. It is strictly true that only the service cert (octo_one-bundle.pem
) is required, I would like to incorporate the use of certificates for client authentication in the future, which will require access to the CA. Best install it now.
As HAProxy is already in use in the default OctoPi configuration, no installation is necessary - just a minor patch to the configuration files.
Whilst Ansible is generally at its most robust when using templated configuration files, when only minor changes are required to the file it can be okay to carefully apply lineinfile
. Be warned, this can go badly if you do not have appropriate regex pattern matching to ensure that lines will be amended if changes are made in the source!
These changes are summarised by:
- name: Configuration | Require HAProxy to upgrade connections to HTTPS
ansible.builtin.lineinfile:
path: /etc/haproxy/haproxy.cfg
insertafter: 'option forwardfor except 127.0.0.1'
regexp: '^\s*redirect\s+scheme\s+https'
line: " redirect scheme https if !{ hdr(Host) -i 127.0.0.1 } !{ ssl_fc }"
- name: Configuration | Map HAProxy to the correct SSL Certificate
ansible.builtin.lineinfile:
path: /etc/haproxy/haproxy.cfg
regexp: '^\s*bind\s+:::443\s+v4v6\s+ssl\s+crt\s+'
line: " bind :::443 v4v6 ssl crt /etc/ssl/octo_one-bundle.pem"
You will notice that cert paths are currently hard coded. I only have one 3D Printer, when the next one arrives I will deal with the scaling problem.
After changing the configuration file it is required to restart HAProxy. This is done directly with the service module:
- name: Post | Restart HAProxy if required
ansible.builtin.service:
name: haproxy
state: restarted
However, this will enforce restarting HAProxy every time this playbook is executed. A good practice when dealing with Ansible is to only undertake actions that are essential, so let’s try and keep track of what changes are being made. This is done by registering the output of the previous commands as follows:
- name: Certificates | Place the certificate in the correct location on the host
copy:
src: certs/octo_one-bundle.pem
dest: /etc/ssl/octo_one-bundle.pem
register: cert1
- name: Certificates | Place the CA bundle in the correct location on the host
copy:
src: certs/ca-bundle.pem
dest: /etc/ssl/ca-bundle.pem
register: cert2
- name: Configuration | Require HAProxy to upgrade connections to HTTPS
ansible.builtin.lineinfile:
path: /etc/haproxy/haproxy.cfg
insertafter: 'option forwardfor except 127.0.0.1'
regexp: '^\s*redirect\s+scheme\s+https'
line: " redirect scheme https if !{ hdr(Host) -i 127.0.0.1 } !{ ssl_fc }"
register: config1
- name: Configuration | Map HAProxy to the correct SSL Certificate
ansible.builtin.lineinfile:
path: /etc/haproxy/haproxy.cfg
regexp: '^\s*bind\s+:::443\s+v4v6\s+ssl\s+crt\s+'
line: " bind :::443 v4v6 ssl crt /etc/ssl/octo_one-bundle.pem"
register: config2
- name: Post | Restart HAProxy if required
ansible.builtin.service:
name: haproxy
state: restarted
when: cert1.changed or cert2.changed or config1.changed or config2.changed
All this automation is great, but we don’t want it to get in the way of the number one reason for this existing - printing parts! In order to ensure minimal interruptions to the little Pi that is running this, I’d like to incorporate a check to ensure that the printer is not in the middle of a print prior to running the rest of the script. This is done by using the built in API to OctoPrint.
To interact with this API a Application Key is needed. This is generated in the OctoPrint Settings Menu. Once generated, this can be assigned to variable in Ansible. Ideally API keys should be stored securely in a credential store such as Ansible Vault, but due to the risk on this particular interaction I have chosen to store it just as a playbook variable. Another thing to fix in the next iteration!
The following block of code connects to the OctoPrint API and gets the current status. If the status is “Printing
”, the playbook fails to avoid running any further commands against the host whilst it may be busy.
- name: Printing Check | Status API Call
ansible.builtin.uri:
validate_certs: no
url: "https:///api/job"
body_format: json
headers:
X-Api-Key: ""
register: printer_status_response
connection: local
- name: Printing Check | Get State from Response
ansible.builtin.set_fact:
printer_state: ""
- name: Printing Check | Fail due to currently printing
fail:
msg: "Unable to carry out action as printer is running."
when: printer_state == "Printing"
The connection: local
directive on the uri
module call is done to enforce this coming from the host running Ansible. Although in this case it’s just my laptop, if this code were to support execution from Ansible Tower (or AWX) understanding where the API calls are running from is important to ensure appropriate security controls are in place.
So far this configuration has been all about setting up standard server side HTTPS. Provided the appropriate CA certificates have been loaded onto a client, this connection will be trusted. However, I mentioned earlier that I would eventually like to client certificate based authentication working. This enables the server to trust the client as authentic in addition to the previous situation whereby the client trusted the server.
In HAProxy this is done by including a path to the CA file as well as enforcing verification of client certificates. Modifying the previous lineinfile
task for client support is as simple as:
- name: Configuration | Map HAProxy to the correct SSL Certificate and enforce Client Certificate
ansible.builtin.lineinfile:
path: /etc/haproxy/haproxy.cfg
regexp: '^\s*bind\s+:::443\s+v4v6\s+ssl\s+crt\s+'
line: " bind :::443 v4v6 ssl crt /etc/ssl/octo_one-bundle.pem ca-file /etc/ssl/ca-bundle.pem verify required"
register: config2
This will then require the client to produce a certificate prior to connection.
I had this all configured and working with the browser, however I rapidly discovered that it breaks the Prusa-Slicer OctoPrint integration. I use this heavily, so the client certificate functionality had to go! From reading through github issues it appears that the OctoPrint integration is not actively developed by the development team and it’s probably unlike that the niche functionality of providing a client certificate will be added in the near future.
This runs locally on my laptop as a standalone Ansible Playbook. It is simple to execute and provides all the functionality I need, whilst hopefully future proofing any changes that may occur in the OctoPrint configuration files as upgrades are performed.
The complete code is:
- hosts: <YOUR HOST HERE>
gather_facts: minimal
become: yes
vars:
octoprint_api_key: <YOUR API KEY HERE>
tasks:
- name: Printing Check | Status API Call
ansible.builtin.uri:
validate_certs: no
url: "https:///api/job"
body_format: json
headers:
X-Api-Key: ""
register: printer_status_response
connection: local
- name: Printing Check | Get State from Response
ansible.builtin.set_fact:
printer_state: ""
- name: Printing Check | Fail due to currently printing
fail:
msg: "Unable to carry out action as printer is running."
when: printer_state == "Printing"
- name: Certificates | Place the certificate in the correct location on the host
copy:
src: certs/<CERT>-bundle.pem
dest: /etc/ssl/<CERT>-bundle.pem
register: cert1
- name: Certificates | Place the CA bundle in the correct location on the host
copy:
src: certs/<CA>-bundle.pem
dest: /etc/ssl/<CA>-bundle.pem
register: cert2
- name: Configuration | Require HAProxy to upgrade connections to HTTPS
ansible.builtin.lineinfile:
path: /etc/haproxy/haproxy.cfg
insertafter: 'option forwardfor except 127.0.0.1'
regexp: '^\s*redirect\s+scheme\s+https'
line: " redirect scheme https if !{ hdr(Host) -i 127.0.0.1 } !{ ssl_fc }"
register: config1
# - name: Configuration | Map HAProxy to the correct SSL Certificate and enforce Client Certificate
# ansible.builtin.lineinfile:
# path: /etc/haproxy/haproxy.cfg
# regexp: '^\s*bind\s+:::443\s+v4v6\s+ssl\s+crt\s+'
# line: " bind :::443 v4v6 ssl crt /etc/ssl/<CERT>-bundle.pem ca-file /etc/ssl/<CA>-bundle.pem verify required"
# register: config2
- name: Configuration | Map HAProxy to the correct SSL Certificate
ansible.builtin.lineinfile:
path: /etc/haproxy/haproxy.cfg
regexp: '^\s*bind\s+:::443\s+v4v6\s+ssl\s+crt\s+'
line: " bind :::443 v4v6 ssl crt /etc/ssl/<CERT>-bundle.pem"
register: config2
- name: Post | Restart HAProxy if required
ansible.builtin.service:
name: haproxy
state: restarted
when: cert1.changed or cert2.changed or config1.changed or config2.changed
If further functionality is added or I manage to convert this to a role in the future I will update this post with a link, but at the moment it honestly doesn’t seem like the effort.
I’m honestly not sure this will ever help someone, if it does that is great! It is however a great example of how trivial it can be to integrate Ansible with a third party API indicating application state.
Originally published June 28, 2021 (Updated June 29, 2021)
Related posts :