Launching LXD images using templates

Hi all,

I have been using LXD on Ubuntu 16.04 for a year or so and it has been very convenient.

I have been using (trivial) LXD templates on image launch for inserting some string into some configuration file based on an environment variable (environment.*) set in a profile.

There’s really not much documentation on templates, so it’s a lot of trial and error (which is really hard as you have to change, publish and launch over and over again without any hints on why the image couldn’t be launched – at least to my knowledge).

What I’ve done is this, which is working:

{{ config_get("environment.db-library", "") }}

to prepend some DB libraries in the JDBC connection string depending on environment.

Now I would like to include one complete file when in test, another when in production.

I was self-confident and tried this in a template (based on Django/Pongo2 docs):

{% if config_get("environment.env", "") == "prod" %} {% include "prod.txt" %} {% else %} {% include "test.txt" %} {% endif %}

It did not work and I have no clue as to why.

Is there anything I could do the make my template testing easier?

Does anybody have any tip on what I should do with my specific including problem?

Regards,

Peik

1 Like

My understanding is that cloud-init is the preferred way, using profiles.

See, for example, https://blog.simos.info/how-to-preconfigure-lxd-containers-with-cloud-init/

Great, thanks.

To be honest I never really understood the cloud-init thing. I thought it was for cloud environments in some way – I run my stuff on local servers…

My problem can probably be solved using cloud-init, it appears, but how are you supposed to run conditional statements depending on variables/settings set in the profile (runcmd seems really suitable to run only simple, single commands)?

Wouldn’t it be cleaner to use templates to include only the correct file in the container filesystem instead of having all alternative files in the container and during launch/runcmd choose the correct one?

It seems to me (with my very limited knowledge) that templates are suitable for including specific files in the container during launch while cloud-init is convenient for taking simple actions during launch.

Anyway, how should if-then-else be done in the template? Even if that wouldn’t be the recommended way, it should be possible as there is such a template (cloud-init-user.tpl) in the Ubuntu image (in my at least):

{% if config_get("user.network_mode", "") == "link-local" %}manual{% else %}dhcp{% endif %}

I suppose that should work.

I tested with

{% if config_get(“environment.env”, “”) == “prod” %}include “prod.txt”{% else %}include “test.txt”{% endif %}

just to include whatever text string, but that didn’t work.

Can you post your metadata.yaml and the entire template (if that was just part of it)?

For testing, it’s usually easiest to make sure that the template triggers on start so that you can just restart the container and have it re-applied.

Your syntax does look valid to me but I’ve never played with the include tag and I do wonder relative to what any of those paths would be… We may need to tweak LXD’s behavior in this regard as we’d want that to be relative to the container’s rootfs which I’m not sure it is today.

I simplified my example even further. Here are the files:

metadata.yml:

architecture: x86_64
creation_date: 1463425145
expiry_date: 0
properties:
templates:
  /etc/hosts:
    when:
    - create
    - copy
    - start
create_only: false
template: etc.hosts.tpl
properties: {}

etc.hosts.tpl:

# {% if config_get(“environment.env”, “”) == “prod” %}include “prod.txt”{% else %}include “test.txt”{% endif %}
127.0.0.1 localhost mycontainer

In fact, it fails already with an empty script tag in the template: {% %}

By the way, I’m using an up-to-date Ubuntu 16.04.

For testing, it’s usually easiest to make sure that the template triggers on start so that you can just restart the container and have it re-applied.

Cool, that makes the process at least 3 times faster. :slight_smile: Thanks!

Your syntax does look valid to me but I’ve never played with the include tag and I do wonder relative to what any of those paths would be…

I thought I would try a path relative to the template path. The location didn’t become a problem yet, but the problem is the script tags (as you can see in my previous reply) – even an empty script tag ({% %}) breaks it.

stgraber@castiana:~$ lxc launch ubuntu:16.04 c1
Creating c1
Starting c1

stgraber@castiana:~$ sudo -s
root@castiana:~# cd /var/lib/lxd/containers/c1/
root@castiana:/var/lib/lxd/containers/c1# ls
backup.yaml  metadata.yaml  rootfs  templates

root@castiana:/var/lib/lxd/containers/c1# vim metadata.yaml 
root@castiana:/var/lib/lxd/containers/c1# cat metadata.yaml 
architecture: "x86_64"
creation_date: 1516949213
properties:
    architecture: "x86_64"
    description: "Ubuntu 16.04 LTS server (20180126)"
    os: "ubuntu"
    release: "xenial"
templates:
    /etc/hostname:
        when:
            - start
        template: hostname.tpl

root@castiana:/var/lib/lxd/containers/c1# rm templates/cloud-init-*
root@castiana:/var/lib/lxd/containers/c1# vim templates/hostname.tpl 
root@castiana:/var/lib/lxd/containers/c1# cat templates/hostname.tpl 
{{ config_get("environment.env", "unset") }}

root@castiana:/var/lib/lxd/containers# exit
stgraber@castiana:~$ lxc restart c1 --force
stgraber@castiana:~$ lxc exec c1 -- cat /etc/hostname
unset
stgraber@castiana:~$ lxc config set c1 environment.env prod
stgraber@castiana:~$ lxc restart c1 --force
stgraber@castiana:~$ lxc exec c1 -- cat /etc/hostname
prod

Yes, thanks, that is working, but change the code in the template from what you have to

{% if config_get(“environment.env”, “”) == “prod” %}include “prod.txt”{% else %}include “test.txt”{% endif %}

That does not work - for me.

As I said before, the template could even contain only this:
{% %}

… which does not work. However, if an empty script statement really should work I don’t know.

The point here was not to switch between two text strings, but really to include some file containing different application settings. :slight_smile:

{% if config_get("environment.env", "unset") == "prod" %}foo{% else %}bar{% endif %}

Works fine, so that does seem to restrict the problems to that include statement, lets see how that’s supposed to work…

With an include of “test”, I’m getting:

EROR[01-30|15:30:39] start hook failed                        container=c1 err="[Error (where: fromfile) in test | Line 1 Col 89 near 'test'] open test: no such file or directory"
DBUG[01-30|15:30:39] 
	{
		"error": "[Error (where: fromfile) in test | Line 1 Col 89 near 'test'] open test: no such file or directory",
		"error_code": 500,
		"type": "error"
	} 
EROR[01-30|15:30:39] Failed starting container                action=start created=2018-01-30T15:43:26+0000 ephemeral=false name=c1 stateful=false used=2018-01-30T20:28:49+0000
root@castiana:/var/lib/lxd/containers# echo abc > /tmp/blah
root@castiana:/var/lib/lxd/containers# cat c1/templates/hostname.tpl
{% if config_get("environment.env", "unset") == "prod" %}{% include "/tmp/blah" %}{% else %}bar{% endif %}
root@castiana:/var/lib/lxd/containers# lxc config set c1 environment.env prod
root@castiana:/var/lib/lxd/containers# lxc restart c1 --force
root@castiana:/var/lib/lxd/containers# lxc exec c1 -- cat /etc/hostname
abc

So include seems to be working here, though it’s relative to the host rather than the container.
I think we should treat that as a bug and find some way to tell pongo to make includes relative to the container instead.

1 Like

Actually, in my example above, I just print the string “include prod.txt”, but with the if-statement that fails (I have not even really included anything yet). I have to look closer at your “experiment” and I’ll report back.

Thanks.

Very strange. Now that I tested this with the if-statement you provided above, it suddenly is working!

In other words:

{% if config_get("environment.env", "") == "prod" %} {% include "/var/lib/lxd/containers/testcontainer/templates/test/prod.txt" %} {% else %} {% include "/var/lib/lxd/containers/testcontainer/templates/test/test.txt" %} {% endif %}

… is all fine. If the setting is “prod”, then it includes the corresponding file all right, the same goes for test.

Hard-coded paths are bad, so I tried giving the path to the files as a setting (environment.templatepath), but I couldn’t make that working. I’m sure it must be possible to create the argument to “include” from a variable and a hardcoded string?

I tried something like this:

{% if config_get("environment.templatepath", "") == "prod" %} {% include config_get("environment.templatepath", "")+"/prod.txt" %} {% endif %}

… but that was incorrect. Anybody knows the correct syntax?

That’s more django templating foo than I know, sorry :slight_smile:

This is the solution (one solution at least) for including a file with the file path coming as a setting (e.g. from a profile).

These are the contents of the template file:

{% if config_get("environment.env", "") == "prod" %}
    {% with filename=config_get("environment.templatepath", "")|add:"/prod.txt" %}
            {% include filename %}
    {% endwith %}
{% else %}
    {% with filename=config_get("environment.templatepath", "")|add:"/test.txt" %}
            {% include filename %}
    {% endwith %}
{% endif %}

Great success!

Thanks!

1 Like