One place for hosting & domains

      Templates

      How To Create Reusable Infrastructure with Terraform Modules and Templates


      The author selected the Free and Open Source Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      One of the main benefits of Infrastructure as Code (IAC) is reusing parts of the defined infrastructure. In Terraform, you can use modules to encapsulate logically connected components into one entity and customize them using input variables you define. By using modules to define your infrastructure at a high level, you can separate development, staging, and production environments by only passing in different values to the same modules, which minimizes code duplication and maximizes conciseness.

      You are not limited to using only your custom modules. Terraform Registry is integrated into Terraform and lists modules and providers that you can incorporate in your project right away by defining them in the required_providers section. Referencing public modules can speed up your workflow and reduce code duplication. If you have a useful module and would like to share it with the world, you can look into publishing it on the Registry for other developers to use.

      In this tutorial, we’ll consider some of the ways of defining and reusing code in Terraform projects. You’ll reference modules from the Terraform Registry, separate development and production environments using modules, learn about templates and how they are used, and how to specify resource dependencies explicitly using the depends_on meta argument.

      Prerequisites

      • A DigitalOcean Personal Access Token, which you can create via the DigitalOcean control panel. You can find instructions to do that at: How to Generate a Personal Access Token.
      • Terraform installed on your local machine and a project set up with the DigitalOcean provider. Complete Step 1 and Step 2 of the How To Use Terraform with DigitalOcean tutorial and be sure to name the project folder terraform-reusability, instead of loadbalance. During Step 2, do not include the pvt_key variable and the SSH key resource.
      • The droplet-lb module available under modules in terraform-reusability. Follow the How to Build a Custom Module tutorial and work through it until the droplet-lb module is functionally complete. (That is, until the cd ../.. command in the Creating a Module section.)
      • Knowledge of Terraform project structuring approaches. For more information, see How To Structure a Terraform Project.
      • (Optional) Two separate domains whose nameservers are pointed to DigitalOcean at your registrar. Refer to the How To Point to DigitalOcean Nameservers From Common Domain Registrars tutorial to set this up. Note that you don’t need to do this if you don’t plan on deploying the project you’ll create through this tutorial.

      Note: We have specifically tested this tutorial using Terraform 0.13.

      Separating Development and Production Environments

      In this section, you’ll use modules to achieve separation between your target deployment environments. You’ll arrange these according to the structure of a more complex project. You’ll first create a project with two modules, one of which will define the Droplets and Load Balancers, and the other one will set up the DNS domain records. After, you’ll write configuration for two different environments (dev and prod), which will call the same modules.

      Creating the dns-records module

      As part of the prerequisites, you have set up the project initially under terraform-reusability and created the droplet-lb module in its own subdirectory under modules. You’ll now set up the second module, called dns-records, containing variables, outputs, and resource definitions. Assuming you’re in terraform-reusability, create dns-records by running:

      • mkdir modules/dns-records

      Navigate to it:

      This module will comprise the definitions for your domain and the DNS records that you’ll later point to the Load Balancers. You’ll first define the variables, which will become inputs that this module will expose. You’ll store them in a file called variables.tf. Create it for editing:

      Add the following variable definitions:

      terraform-reusability/modules/dns-records/variables.tf

      variable "domain_name" {}
      variable "ipv4_address" {}
      

      Save and close the file. You’ll now define the domain and the accompanying A and CNAME records in a file named records.tf. Create and open it for editing by running:

      Add the following resource definitions:

      terraform-reusability/modules/dns-records/records.tf

      resource "digitalocean_domain" "domain" {
        name = var.domain_name
      }
      
      resource "digitalocean_record" "domain_A" {
        domain = digitalocean_domain.domain.name
        type   = "A"
        name   = "@"
        value  = var.ipv4_address
      }
      
      resource "digitalocean_record" "domain_CNAME" {
        domain = digitalocean_domain.domain.name
        type   = "CNAME"
        name   = "www"
        value  = var.ipv4_address
      }
      

      First, you define the domain in your DigitalOcean account for your domain name. The cloud will automatically add the three DigitalOcean nameservers as NS records. Then, you define an A record for your domain, routing it (the @ as value signifies the true domain name, without subdomains) to the IP address supplied as the variable ipv4_address. For the sake of completeness, the CNAME record that follows specifies that the www subdomain should also point to the same IP address. Save and close the file when you’re done.

      Next, you’ll define the outputs for this module. The outputs will show the FQDN (fully qualified domain name) of the created records. Create and open outputs.tf for editing:

      Add the following lines:

      terraform-reusability/modules/dns-records/outputs.tf

      output "A_fqdn" {
        value = digitalocean_record.domain_A.fqdn
      }
      
      output "CNAME_fqdn" {
        value = digitalocean_record.domain_CNAME.fqdn
      }
      

      Save and close the file when you’re done.

      With the variables, DNS records, and outputs defined, the last thing you’ll need to specify are the provider requirements for this module. You’ll specify that the dns-records module requires the digitalocean provider in a file called provider.tf. Create and open it for editing:

      Add the following lines:

      terraform-reusability/modules/dns-records/provider.tf

      terraform {
        required_providers {
          digitalocean = {
            source = "digitalocean/digitalocean"
          }
        }
        required_version = ">= 0.13"
      }
      

      When you’re done, save and close the file. The dns-records module now requires the digitalocean provider and is functionally complete.

      Creating Different Environments

      The following is the current structure of the terraform-reusability project:

      terraform_reusability/
      ├─ modules/
      │  ├─ dns-records/
      │  │  ├─ outputs.tf
      │  │  ├─ provider.tf
      │  │  ├─ records.tf
      │  │  ├─ variables.tf
      │  ├─ droplet-lb/
      │  │  ├─ droplets.tf
      │  │  ├─ lb.tf
      │  │  ├─ outputs.tf
      │  │  ├─ provider.tf
      │  │  ├─ variables.tf
      ├─ main.tf
      ├─ provider.tf
      

      So far, you have two modules in your project: the one you just created (dns-records) and droplet-lb, which you created as part of the prerequisites.

      To facilitate different environments, you’ll store the dev and prod environment config files under a directory called environments, which will reside in the root of the project. Both environments will call the same two modules, but with different parameter values. The advantage of this is when the modules change internally in the future, you’ll only need to update the values you are passing in.

      First, navigate to the root of the project by running:

      Then, create the dev and prod directories under environments at the same time:

      • mkdir -p environments/dev && mkdir environments/prod

      The -p argument orders mkdir to create all directories in the given path.

      Navigate to the dev directory, as you’ll first configure that environment:

      You’ll store the code in a file named main.tf, so create it for editing:

      Add the following lines:

      terraform-reusability/environments/dev/main.tf

      module "droplets" {
        source   = "../../modules/droplet-lb"
      
        droplet_count = 2
        group_name    = "dev"
      }
      
      module "dns" {
        source   = "../../modules/dns-records"
      
        domain_name   = "your_dev_domain"
        ipv4_address  = module.droplets.lb_ip
      }
      

      Here you call and configure the two modules, droplet-lb and dns-records, which will together result in the creation of two Droplets. They’re fronted by a Load Balancer; the DNS records for the supplied domain are set up to point to that Load Balancer. Remember to replace your_dev_domain with your desired domain name for the dev environment, then save and close the file.

      Next, you’ll configure the DigitalOcean provider and create a variable for it to be able to accept the personal access token you’ve created as part of the prerequisites. Open a new file, called provider.tf, for editing:

      Add the following lines:

      terraform-reusability/environments/dev/provider.tf

      terraform {
        required_providers {
          digitalocean = {
            source = "digitalocean/digitalocean"
            version = "1.22.2"
          }
        }
      }
      
      variable "do_token" {}
      
      provider "digitalocean" {
        token = var.do_token
      }
      

      In this code, you require the digitalocean provider to be available and pass in the do_token variable to its instance. Save and close the file.

      Initialize the configuration by running:

      You’ll receive the following output:

      Output

      Initializing modules... - dns in ../../modules/dns-records - droplets in ../../modules/droplet-lb Initializing the backend... Initializing provider plugins... - Finding latest version of digitalocean/digitalocean... - Installing digitalocean/digitalocean v2.0.2... - Installed digitalocean/digitalocean v2.0.2 (signed by a HashiCorp partner, key ID F82037E524B9C0E8) Partner and community providers are signed by their developers. If you'd like to know more about provider signing, you can read about it here: https://www.terraform.io/docs/plugins/signing.html The following providers do not have any version constraints in configuration, so the latest version was installed. To prevent automatic upgrades to new major versions that may contain breaking changes, we recommend adding version constraints in a required_providers block in your configuration, with the constraint strings suggested below. * digitalocean/digitalocean: version = "~> 2.0.2" Terraform has been successfully initialized! You may now begin working with Terraform. Try running "terraform plan" to see any changes that are required for your infrastructure. All Terraform commands should now work. If you ever set or change modules or backend configuration for Terraform, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary.

      The configuration for the prod environment is similar. Navigate to its directory by running:

      Create and open main.tf for editing:

      Add the following lines:

      terraform-reusability/environments/prod/main.tf

      module "droplets" {
        source   = "../../modules/droplet-lb"
      
        droplet_count = 5
        group_name    = "prod"
      }
      
      module "dns" {
        source   = "../../modules/dns-records"
      
        domain_name   = "your_prod_domain"
        ipv4_address  = module.droplets.lb_ip
      }
      

      The difference between this and your dev code is that there will be five Droplets deployed. Furthermore, the domain name, which you should replace with your prod domain name, will be different. Save and close the file when you’re done.

      Then, copy over the provider configuration from dev:

      Initialize this configuration as well:

      The output of this command will be the same as the previous time you ran it.

      You can try planning the configuration to see what resources Terraform would create by running:

      • terraform plan -var "do_token=${DO_PAT}"

      The output for prod will be the following:

      Output

      ... An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # module.dns.digitalocean_domain.domain will be created + resource "digitalocean_domain" "domain" { + id = (known after apply) + name = "your_prod_domain" + urn = (known after apply) } # module.dns.digitalocean_record.domain_A will be created + resource "digitalocean_record" "domain_A" { + domain = "your_prod_domain" + fqdn = (known after apply) + id = (known after apply) + name = "@" + ttl = (known after apply) + type = "A" + value = (known after apply) } # module.dns.digitalocean_record.domain_CNAME will be created + resource "digitalocean_record" "domain_CNAME" { + domain = "your_prod_domain" + fqdn = (known after apply) + id = (known after apply) + name = "www" + ttl = (known after apply) + type = "CNAME" + value = (known after apply) } # module.droplets.digitalocean_droplet.droplets[0] will be created + resource "digitalocean_droplet" "droplets" { ... + name = "prod-0" ... } # module.droplets.digitalocean_droplet.droplets[1] will be created + resource "digitalocean_droplet" "droplets" { ... + name = "prod-1" ... } # module.droplets.digitalocean_droplet.droplets[2] will be created + resource "digitalocean_droplet" "droplets" { ... + name = "prod-2" ... } # module.droplets.digitalocean_droplet.droplets[3] will be created + resource "digitalocean_droplet" "droplets" { ... + name = "prod-3" ... } # module.droplets.digitalocean_droplet.droplets[4] will be created + resource "digitalocean_droplet" "droplets" { ... + name = "prod-4" ... } # module.droplets.digitalocean_loadbalancer.www-lb will be created + resource "digitalocean_loadbalancer" "www-lb" { ... + name = "lb-prod" ... Plan: 9 to add, 0 to change, 0 to destroy. ...

      This would deploy five Droplets with a Load Balancer. Also it would create the prod domain you specified with the two DNS records pointing to the Load Balancer. You can try planning the configuration for the dev environment as well—you’ll note that two Droplets would be planned for deployment.

      Note: You can apply this configuration for the dev and prod environments with the following command:

      • terraform apply -var "do_token=${DO_PAT}"

      The following demonstrates how you have structured this project:

      terraform_reusability/
      ├─ environments/
      │  ├─ dev/
      │  │  ├─ main.tf
      │  │  ├─ provider.tf
      │  ├─ prod/
      │  │  ├─ main.tf
      │  │  ├─ provider.tf
      ├─ modules/
      │  ├─ dns-records/
      │  │  ├─ outputs.tf
      │  │  ├─ provider.tf
      │  │  ├─ records.tf
      │  │  ├─ variables.tf
      │  ├─ droplet-lb/
      │  │  ├─ droplets.tf
      │  │  ├─ lb.tf
      │  │  ├─ outputs.tf
      │  │  ├─ provider.tf
      │  │  ├─ variables.tf
      ├─ main.tf
      ├─ provider.tf
      

      The addition is the environments directory, which holds the code for the dev and prod environments.

      The benefit of this approach is that further changes to modules automatically propagate to all areas of your project. Barring any possible customizations to module inputs, this approach is not repetitive and promotes reusability as much as possible, even across deployment environments. Overall this reduces clutter and allows you to trace the modifications using a version-control system.

      In the final two sections of this tutorial, you’ll review the depends_on meta argument and the templatefile function.

      Declaring Dependencies to Build Infrastructure in Order

      While planning actions, Terraform automatically tries to sense existing dependencies and builds them into its dependency graph. The main dependencies it can detect are clear references; for example, when an output value of a module is passed to a parameter on another resource. In this scenario the module must first complete its deployment to provide the output value.

      The dependencies that Terraform can’t detect are hidden—they have side effects and mutual references not inferable from the code. An example of this is when an object depends not on the existence, but on the behavior of another one, and does not access its attributes from code. To overcome this, you can use depends_on to manually specify the dependencies in an explicit way. Since Terraform 0.13, you can also use depends_on on modules to force the listed resources to be fully deployed before deploying the module itself. It’s possible to use the depends_on meta argument with every resource type. depends_on will also accept a list of other resources on which its specified resource depends.

      In the previous step of this tutorial, you haven’t specified any explicit dependencies using depends_on, because the resources you’ve created have no side effects not inferable from the code. Terraform is able to detect the references made from the code you’ve written, and will schedule the resources for deployment accordingly.

      depends_on accepts a list of references to other resources. Its syntax looks like this:

      resource "resource_type" "res" {
        depends_on = [...] # List of resources
      
        # Parameters...
      }
      

      Remember that you should only use depends_on as a last-resort option. If used, it should be kept well documented, because the behavior that the resources depend on may not be immediately obvious.

      Using Templates for Customization

      In Terraform, templating is substituting results of expressions in appropriate places, such as when setting attribute values on resources or constructing strings. You’ve used it in the previous steps and the tutorial prerequisites to dynamically generate Droplet names and other parameter values.

      When substituting values in strings, the values are specified and surrounded by ${}. Template substitution is often used in loops to facilitate customization of the created resources. It also allows for module customization by substituting inputs in resource attributes.

      Terraform offers the templatefile function, which accepts two arguments: the file from the disk to read and a map of variables paired with their values. The value it returns is the contents of the file rendered with the expression substituted—just as Terraform would normally do when planning or applying the project. Because functions are not part of the dependency graph, the file cannot be dynamically generated from another part of the project.

      Imagine that the contents of the template file called droplets.tmpl is as follows:

      %{ for address in addresses ~}
      ${address}:80
      %{ endfor ~}
      

      Longer declarations must be surrounded with %{}, as is the case with the for and endfor declarations, which signify the start and end of the for loop respectively. The contents and type of the droplets variable are not known until the function is called and actual values provided, like so:

      templatefile("${path.module}/droplets.tmpl", { addresses = ["192.168.0.1", "192.168.1.1"] })
      

      The value that this templatefile call will return is the following:

      Output

      192.168.0.1:80 192.168.1.1:80

      This function has its use cases, but they are uncommon. For example, you could use it when a part of the configuration is necessary to exist in a proprietary format, but is dependent on the rest of the values and must be generated dynamically. In the majority of cases, it’s better to specify all configuration parameters directly in Terraform code, where possible.

      Conclusion

      In this article, you’ve maximized code reuse in an example Terraform project. The main way is to package often-used features and configurations as a customizable module and use it whenever needed. By doing so, you do not duplicate the underlying code (which can be error prone) and enable faster turnaround times, since modifying the module is almost all you need to do to introduce changes.

      You’re not limited to your own modules. As you’ve seen, Terraform Registry provides third-party modules and providers that you can incorporate in your project.

      Check out the rest of the How To Manage Infrastructure with Terraform series.



      Source link

      Working with Django Templates & Static Files


      In our getting started with Django tutorial, I showed you how to get a Django site up and running. The templates we rendered were very basic though.

      This is definitely not how you want your site to look like.

      How do you get your site to look better? Simple! Add some styling. In this tutorial, I will show you how to add some CSS and JavaScript to your Django templates in order to make them look much better. To do that, you first need to understand the concept of static files in Django.

      Setting up a Django Project

      Let’s set up our test Django project. First, create a folder called projects which is where our app will live.

      mkdir projects && cd projects
      

      Inside projects, let’s use virtualenv to create an environment for our app’s dependencies.

      virtualenv env --python python3
      

      NOTE: If you do not have virtualenv installed, install it using the command pip install virtualenv.

      Once that is done, activate the environment by running the activate shell script.

      source env/bin/activate
      

      If that command works, you should see an (env) prompt on your terminal.

      #(env)~/projects
      $
      

      Everything look fine? Awesome! Let’s now use pip to install Django into our environment.

      #(env)~/projects
      $ pip install django
      

      That command should install Django into your environment. As of the time of writing, the Django version is 1.10.4.

      We are then going to call the django-admin script to create our Django app. Let’s do that like this:

      #(env)~/projects
      $ django-admin startproject djangotemplates
      

      If you check your projects folder structure, you should now have a new folder called djangotemplates created by Django in addition to the earlier env folder we created.

      cd into djangotemplates.

      Your folder structure should now be similar to this:

      djangotemplates
      --djangotemplates
      ----**init**.py
      ----settings.py
      ----urls.py
      ----wsgi.py
      --manage.py
      

      All done? You are now ready to begin!

      Settings for managing static files

      Static files include stuff like CSS, JavaScript and images that you may want to serve alongside your site. Django is very opinionated about how you should include your static files. In this article, I will show how to go about adding static files to a Django application.

      Open the settings.py file inside the inner djangotemplates folder. At the very bottom of the file you should see these lines:

      # djangotemplates/djangotemplates/settings.py
      
      # Static files (CSS, JavaScript, Images)
      # https://docs.djangoproject.com/en/1.10/howto/static-files/
      
      STATIC_URL = '/static/'
      

      This line tells Django to append static to the base url (in our case localhost:8000) when searching for static files. In Django, you could have a static folder almost anywhere you want. You can even have more than one static folder e.g. one in each app. However, to keep things simple, I will use just one static folder in the root of our project folder. We will create one later. For now, let’s add some lines in the settings.py file so that it looks like this.

      # djangotemplates/djangotemplates/settings.py
      
      # Static files (CSS, JavaScript, Images)
      # https://docs.djangoproject.com/en/1.10/howto/static-files/
      
      STATIC_URL = '/static/'
      
      # Add these new lines
      STATICFILES_DIRS = (
          os.path.join(BASE_DIR, 'static'),
      )
      
      STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
      

      The STATICFILES_DIRS tuple tells Django where to look for static files that are not tied to a particular app. In this case, we just told Django to also look for static files in a folder called static in our root folder, not just in our apps.

      Django also provides a mechanism for collecting static files into one place so that they can be served easily. Using the collectstatic command, Django looks for all static files in your apps and collects them wherever you told it to, i.e. the STATIC_ROOT. In our case, we are telling Django that when we run python manage.py collectstatic, gather all static files into a folder called staticfiles in our project root directory. This feature is very handy for serving static files, especially in production settings.

      App setup

      Create a folder called static on the same level as the inner djangotemplates folder and the manage.py file. You should now have this structure:

      djangotemplates
      --djangotemplates
      ----**init**.py
      ----settings.py
      ----urls.py
      ----wsgi.py
      --static
      --manage.py
      

      Inside this folder is where we will have any custom CSS and JS we choose to write. On that note, let’s add two folders inside the static folder to hold our files, one called css and the other called js. Inside css, create a file called main.css. Add a main.js in the js folder as well. Your static folder should now look like this:

      --static
      ----css
      ------main.cs
      ----js
      ------main.js
      

      Once that is done, let’s create a new Django app called example that we will be working with. Do you remember how to do that? Don’t worry, it’s quite simple.

      #(env)~/projects/djangotemplates
      $ python manage.py startapp example
      

      Once that is done, you should have a folder called example alongside djangotemplates and static. And of course you should still be able to see the manage.py file.

      djangotemplates
      --djangotemplates
      ----**init**.py
      ----settings.py
      ----urls.py
      ----wsgi.py
      --example
      --static
      --manage.py
      

      We need to tell Django about our new app. Go to the inner djangotemplates folder, open up settings.py and look for INSTALLED_APPS. Add example under the other included apps.

      # djangotemplates/djangotemplates/settings.py
      
      DEBUG = True
      
      ALLOWED_HOSTS = []
      
      
      # Application definition
      
      INSTALLED_APPS = [
          'django.contrib.admin',
          'django.contrib.auth',
          'django.contrib.contenttypes',
          'django.contrib.sessions',
          'django.contrib.messages',
          'django.contrib.staticfiles',
          'example', # Add this line
      ]
      

      Just to recap, we now have the following folder structure:

      djangotemplates
      --djangotemplates
      ----**init**.py
      ----settings.py
      ----urls.py
      ----wsgi.py
      --example
      ----migrations
      ------**init**.py
      ----admin.py
      ----apps.py
      ----models.py
      ----tests.py
      ----views.py
      --static
      ----css
      ------main.cs
      ----js
      ------main.js
      --manage.py
      

      URL definition

      Let’s define a URL to go to our new app. Let’s edit djangotemplates/djangotemplates/urls.py to effect that.

      # djangotemplates/djangotemplates/urls.py
      
      from django.conf.urls import url, include # Add include to the imports here
      from django.contrib import admin
      
      urlpatterns = [
          url(r'^admin/', admin.site.urls),
          url(r'^', include('example.urls')) # tell django to read urls.py in example app
      ]
      

      After that, in the example app folder, create a new file called urls.py and add the following code:

      # djangotemplates/example/urls.py
      
      from django.conf.urls import url
      from example import views
      
      urlpatterns = [
          url(r'^$', views.HomePageView.as_view(), name="home"), # Notice the URL has been named
          url(r'^about/$', views.AboutPageView.as_view(), name="about"),
      ]
      

      The code we have just written tells Django to match the empty route (i.e localhost:8000) to a view called HomePageView and the route /about/ to a view called AboutPageView. Remember, Django views take in HTTP requests and return HTTP responses. In our case, we shall use a TemplateView that returns a Home Page template and another one for the About page. To do this, inside your example app folder, create another folder called templates. Inside the new templates folder, create two new files called index.html and about.html. Your example app folder should now have this structure:

      --example
      ----migrations
      ------**init**.py
      ----templates
      ------index.html
      ------about.html
      ----admin.py
      ----apps.py
      ----models.py
      ----tests.py
      ----urls.py
      ----views.py
      

      Inside the index.html, paste the following code:

      <!-- djangotemplates/example/templates/index.html-->
      
      <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <title>Welcome Home</title>
      </head>
      <body>
        <p>"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 
          Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 
          Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 
          Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
        </p>
        <a href="https://www.digitalocean.com/community/tutorials/{% url"home' %}">Go Home</a>
        <a href="https://www.digitalocean.com/community/tutorials/{% url"about' %}">About This Site</a>
      </body>
      </html>
      

      Add this code to about.html:

      <!-- djangotemplates/example/templates/about.html-->
      
      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>About Us</title>
      </head>
      <body>
        <p>
        We are a group of Django enthusiasts with the following idiosyncrasies:
      
        <ol>
          <li>We only eat bananas on Saturdays.</li>
          <li>We love making playing football on rainy days.</li>
        </ol>
        </p>
        <a href="https://www.digitalocean.com/community/tutorials/{% url"home' %}">Go Home</a>
        <a href="https://www.digitalocean.com/community/tutorials/{% url"about' %}">About This Site</a>
      </body>
      </html>
      

      Notice how we are referring to our links for Go Home and About This Site in our templates. We can use Django’s automatic URL reverse lookup because we named our URLs in our urls.py. Neat, huh!

      We shall see the effect of this code in the next section.

      Wiring up the views

      Let’s add the final code to serve up our templates. We need to edit djangotemplates/example/views.py for this.

      # djangotemplates/example/views.py
      from django.shortcuts import render
      from django.views.generic import TemplateView # Import TemplateView
      
      # Add the two views we have been talking about  all this time :)
      class HomePageView(TemplateView):
          template_name = "index.html"
      
      
      class AboutPageView(TemplateView):
          template_name = "about.html"
      

      Now we can run our app. We first need to make Django’s default migrations since this is the first time we are running our app.

      #(env)~/projects/djangotemplates
      $ python manage.py migrate
      

      Once that is done, start your server.

      #(env)~/projects/djangotemplates
      $ python manage.py runserver
      

      Open your browser and navigate to http://localhost:8000. You should be able to see our home page.

      Clicking the links at the bottom should be able to navigate you between the pages. Here is the About page:

      Template Inheritance

      Let’s shift our focus to the templates folder inside the example app folder. At the moment, it contains two templates, index.html and about.html.

      We would like both these templates to have some CSS included. Instead of rewriting the same code in both of them, Django allows us to create a base template which they will both inherit from. This prevents us from having to write a lot of repeated code in our templates when we need to modify anything that is shared.

      Let’s create the base template now. Create a file called base.html in djangotemplates/example/templates. Write this code inside it:

      <!-- djangotemplates/example/templates/base.html -->
      
      {% load static %}
      <!DOCTYPE html>
      <html>
        <head>
          <meta charset="utf-8">
          <title>
            Django Sample Site - {% block title %}{% endblock %}
          </title>
      
          <script src="https://www.digitalocean.com/community/tutorials/{% static"js/main.js' %}"></script> <!-- This is how to include a static file -->
          <link rel="stylesheet" href="https://www.digitalocean.com/community/tutorials/{% static"css/main.css' %}" type="text/css" />
        </head>
        <body>
          <div class="container">
            {% block pagecontent %}
            {% endblock %}
          </div>
        </body>
      </html>
      

      The very first line in the file, {% load static %}, uses Django’s special template tag syntax to tell the template engine to use the files in the static folder in this template.

      In the title tag, we use a Django block. What this means is that in any Django template which inherits from this base template, any HTML which is inside a block named title will be plugged into the title block. The same goes for the body tag’s pagecontent block. If this sounds confusing, don’t worry. You will see it in action soon.

      If you are not running your Django server, run it by executing python manage.py runserver in your terminal. Go to http://localhost:8000. You should see the previous template.

      Now edit the index.html template to inherit from the base template.

      <!-- djangotemplates/example/templates/index.html -->
      
      {% extends 'base.html' %} <!-- Add this for inheritance -->
      
      <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <title>Welcome Home</title>
      </head>
      <body>
          <p>"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 
            Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 
            Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 
            Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
          </p>
          <a href="https://www.digitalocean.com/community/tutorials/{% url"home' %}">Go Home</a>
          <a href="https://www.digitalocean.com/community/tutorials/{% url"about' %}">About This Site</a>
      </body>
      </html>
      

      Reload the page in your browser. Nothing appears! This is because Django expects your content to be written inside the blocks we defined in the base template so that they can be rendered. Edit the index.html to add the blocks:

      <!-- djangotemplates/example/templates/index.html -->
      
      {% extends 'base.html' %}
      
      <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome Home {% endblock %}</title>
      </head>
      <body>
        {% block pagecontent %}
          <p>"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 
            Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 
            Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 
            Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
          </p>
          <a href="https://www.digitalocean.com/community/tutorials/{% url"home' %}">Go Home</a>
          <a href="https://www.digitalocean.com/community/tutorials/{% url"about' %}">About This Site</a>
        {% endblock %}
      </body>
      </html>
      

      Reload the page in the browser and voila! Your content should appear again!

      We can also edit the about.html template to use the same.

      <!-- djangotemplates/example/templates/about.html -->
      
      {% extends 'base.html' %} <!-- Add this for inheritance -->
      
      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>{% block title %}About Us {% endblock %}</title>
      </head>
      <body>
        {% block pagecontent %}
          <p>
          We are a group of Django enthusiasts with the following idiosyncrasies:
      
          <ol>
              <li>We only eat bananas on Saturdays.</li>
              <li>We love making playing football on rainy days.</li>
          </ol>
          </p>
          <a href="https://www.digitalocean.com/community/tutorials/{% url"home' %}">Go Home</a>
          <a href="https://www.digitalocean.com/community/tutorials/{% url"about' %}">About This Site</a>
        {% endblock %}
      </body>
      </html>
      

      You should now see this on the About page:

      Which is exactly the same as before!

      However, now since both templates inherit from a base template, I can easily style them. Open up main.css in your css folder and add these styles:

      .container {
          background: #eac656;
          margin: 10 10 10 10;
          border: 3px solid black;
      }
      

      This will style the container div which we are loading our content into. Refresh your browser. You should see this:

      The Home Page:

      The About Page:

      Rendering templates with data from views

      You can use Django’s template engine to display data in very powerful ways. In this section, I will create a Django view that will pass data into a template. I will then show you how to access that data in the template and display it to the user.

      First things first, open up views.py in the example app folder. We will add a new view to serve data into our yet to exist data.html template. Modify the views.py file to look like this:

      # djangotemplates/example/views.py
      
      from django.shortcuts import render
      from django.views.generic import TemplateView
      
      class HomePageView(TemplateView):
          template_name = "index.html"
      
      class AboutPageView(TemplateView):
          template_name = "about.html"
      
      # Add this view
      class DataPageView(TemplateView):
          def get(self, request, **kwargs):
              # we will pass this context object into the
              # template so that we can access the data
              # list in the template
              context = {
                  'data': [
                      {
                          'name': 'Celeb 1',
                          'worth': '3567892'
                      },
                      {
                          'name': 'Celeb 2',
                          'worth': '23000000'
                      },
                      {
                          'name': 'Celeb 3',
                          'worth': '1000007'
                      },
                      {
                          'name': 'Celeb 4',
                          'worth': '456789'
                      },
                      {
                          'name': 'Celeb 5',
                          'worth': '7890000'
                      },
                      {
                          'name': 'Celeb 6',
                          'worth': '12000456'
                      },
                      {
                          'name': 'Celeb 7',
                          'worth': '896000'
                      },
                      {
                          'name': 'Celeb 8',
                          'worth': '670000'
                      }
                  ]
              }
      
              return render(request, 'data.html', context)
      

      We are using the same kind of view we used to render the other templates. However, we are now passing a context object to the render method. The key-value pairs defined in the context will be available in the template being rendered and we can iterate through them just like any other list.

      To finish this up, go to the urls.py file in the howdy app and add the URL pattern for our new view so that it looks like this:

      # djangotemplates/example/urls.py
      
      from django.conf.urls import url
      from example import views
      
      urlpatterns = [
          url(r'^$', views.HomePageView.as_view(), name="home"),
          url(r'^about/$', views.AboutPageView.as_view(), name="about"),
          url(r'^data/$', views.DataPageView.as_view(), name="data"),  # Add this URL pattern
      ]
      

      Finally, let’s create the template. In the templates folder, create a file called data.html and write this code inside it.

      <!-- djangotemplates/example/templates/data.html -->
      
      {% extends 'base.html' %}
      <!DOCTYPE html>
      <html>
        <head>
          <meta charset="utf-8">
          <title></title>
        </head>
        <body>
          {% block pagecontent %}
          <div class="table-div">
          <!-- We will display our data in a normal HTML table using Django's
          template for-loop to generate our table rows for us-->
            <table class="table">
              <thead>
                <tr>
                  <th>Celebrity Name</th>
                  <th>Net Worth</th>
                </tr>
              </thead>
              <tbody>
                {% for celebrity in data %}
                  <tr>
                    <td>{{ celebrity.name }}</td>
                    <td>{{ celebrity.worth }}</td>
                  </tr>
                {% endfor %}
              </tbody>
            </table>
          </div>
          {% endblock %}
        </body>
      </html>
      

      In data.html, you can see that we use what is essentially a for loop to go through the data list. Binding of values in Django templates is done using {{}} curly brackets much like in AngularJS.

      With your server running, go to http://localhost:8000/data/ to see the template.

      Including snippets into your templates

      We now have three templates, index.html, about.html and data.html. Let’s link them together using a simple navigation bar. First up, let’s write the code for the navigation bar in another HTML template.

      In the templates folder inside the example app, create a new folder called partials. Inside it, create a file called nav-bar.html. The templates folder structure should now be like this:

      templates
      ----index.html
      ----about.html
      ----data.html
      ----partials
      ------nav-bar.html
      

      Edit the nav-bar.html partial so that it contains this code:

      <!-- djangotemplates/example/templates/partials/nav-bar.html -->
      
      <div class="nav">
        <a href="https://www.digitalocean.com/community/tutorials/{% url"home' %}">Go Home</a>
        <a href="https://www.digitalocean.com/community/tutorials/{% url"about' %}">About This Site</a>
        <a href="https://www.digitalocean.com/community/tutorials/{% url"data' %}">View Data</a>
      </div>
      

      Including snippets in a template is very simple. We use the includes keyword provided by Django’s templating engine. Go ahead and modify index.html to this:

      <!-- djangotemplates/example/templates/index.html -->
      
      {% extends 'base.html' %}
      
      <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome Home {% endblock %}</title>
      </head>
      <body>
        {% block pagecontent %}
          <p>"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 
            Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 
            Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 
            Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
          </p>
          {% include 'partials/nav-bar.html' %} <!--Add this-->
      
          <!-- Remove these two lines -- >
          <!-- <a href="https://www.digitalocean.com/community/tutorials/{% url"home' %}">Go Home</a> -->
          <!-- <a href="https://www.digitalocean.com/community/tutorials/{% url"about' %}">About This Site</a> -->
        {% endblock %}
      </body>
      </html>
      

      Modify about.html to this:

      <!-- djangotemplates/example/templates/about.html -->
      
      {% extends 'base.html' %}
      
      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>{% block title %}About Us {% endblock %}</title>
      </head>
      <body>
        {% block pagecontent %}
          <p>
          We are a group of Django enthusiasts with the following idiosyncrasies:
      
          <ol>
              <li>We only eat bananas on Saturdays.</li>
              <li>We love making playing football on rainy days.</li>
          </ol>
          </p>
          {% include 'partials/nav-bar.html' %} <!--Add this-->
      
          <!-- Remove these two lines -- >
          <!-- <a href="https://www.digitalocean.com/community/tutorials/{% url"home' %}">Go Home</a> -->
          <!-- <a href="https://www.digitalocean.com/community/tutorials/{% url"about' %}">About This Site</a> -->
        {% endblock %}
      </body>
      </html>
      

      Lastly, modify data.html to this:

      <!-- djangotemplates/example/templates/data.html -->
      
      {% extends 'base.html' %}
      <!DOCTYPE html>
      <html>
        <head>
          <meta charset="utf-8">
          <title></title>
        </head>
        <body>
          {% block pagecontent %}
          <div class="table-div">
            <table class="table">
              <thead>
                <tr>
                  <th>Celebrity Name</th>
                  <th>Net Worth</th>
                </tr>
              </thead>
              <tbody>
                {% for celebrity in data %}
                  <tr>
                    <td>{{ celebrity.name }}</td>
                    <td>{{ celebrity.worth }}</td>
                  </tr>
                {% endfor %}
              </tbody>
            </table>
          </div>
          {% include 'partials/nav-bar.html' %} <!--Add this-->
          {% endblock %}
        </body>
      </html>
      

      Time to check out our work! Open your browser and navigate to http://localhost:8000. You should see this:

      All the pages are now linked with the navbar so you can easily navigate back and forth through them, all with minimal code written. Here is the data.html template:

      And here is about.html:

      Note: I have added the following CSS to syle the links in the navbar. Feel free to use it or play with your own styles:

      // djangtotemplates/static/css/main.css
      
      .container {
          background: #eac656;
          margin: 10 10 10 10;
          border: 3px solid black;
      }
      
      .nav a {
          background: #dedede;
      }
      

      Filters

      Filters take data piped to them and output it in a formatted way. Django templates have access to the humanize collection of filters, which make data more human readable. Let’s make the celebrity’s networth field in the data template more readable by using some of these filters.

      To use Django’s humanize filters, you first need to edit some settings. Open up djangotemplates/settings.py and edit the INSTALLED_APPS list to this:

      # djangotemplates/djangotemplates/settings.py
      
      ALLOWED_HOSTS = []
      
      
      # Application definition
      
      INSTALLED_APPS = [
          'django.contrib.admin',
          'django.contrib.auth',
          'django.contrib.contenttypes',
          'django.contrib.sessions',
          'django.contrib.messages',
          'django.contrib.staticfiles',
          'django.contrib.humanize', # Add this line. Don't forget the trailing comma
          'example',
      ]
      

      We can now use a filter in our templates. We are going to use the intcomma filter to add comma’s in large numbers to make them easier to read. Let’s modify data.html to this:

      <!-- djangotemplates/example/templates/data.html -->
      
      {% extends 'base.html' %}
      {% load humanize %} <!-- Add this-->
      
      <!DOCTYPE html>
      <html>
        <head>
          <meta charset="utf-8">
          <title></title>
        </head>
        <body>
          {% block pagecontent %}
          <div class="table-div">
            <table class="table">
              <thead>
                <tr>
                  <th>Celebrity Name</th>
                  <th>Net Worth</th>
                </tr>
              </thead>
              <tbody>
                {% for celebrity in data %}
                  <tr>
                    <td>{{ celebrity.name }}</td>
                    <td>$ {{ celebrity.worth | intcomma }}</td> <!--Modify this line-->
                  </tr>
                {% endfor %}
              </tbody>
            </table>
          </div>
          {% include 'partials/nav-bar.html' %}
          {% endblock %}
        </body>
      </html>
      

      When you go to http://localhost:8000/data/, you should now have a more friendly list of net worth values:

      There are many more filters included in the humanize package. Read about them here

      Collecting Static Files

      Remember we talked about collecting static files? Try the following command:

      python manage.py collectstatic
      

      You should see a prompt like the following:

      You have requested to collect static files at the destination
      location as specified in your settings:
      
            /Users/amos/projects/djangotemplates/staticfiles
      
      This will overwrite existing files!
      Are you sure you want to do this?
      
      Type 'yes' to continue, or 'no' to cancel:
      

      Go ahead and say yes.

      This command will tell Django to go through all your project folders, look for all static files and store them in one place (the static root we defined in the settings). This is very efficient especially if you are deploying your site to production.

      When you run the command collectstatic, you should see a new folder called staticfiles created in the root of your project folder. You can change this location to something else by editing the static root setting in your project’s settings.py file. To use these staticfiles, in your templates you will say load staticfiles instead of load static. Everything else is the same as with using the previous static folder.

      Conclusion

      Congratulations on reaching the end of this tutorial! By now you should have a more detailed understanding of how Django templates work. If you need deeper information, remember the docs are your friend. You can find the full code for this tutorial here. Make sure to leave any thoughts, questions or concerns in the comments below.



      Source link

      Introduction to Jinja Templates for Salt


      Updated by Linode Contributed by Linode

      Use promo code DOCS10 for $10 credit on a new account.

      Introduction to Templating Languages

      Jinja is a flexible templating language for Python that can be used to generate any text based format such as HTML, XML, and YAML. Templating languages like Jinja allow you to insert data into a structured format. You can also embed logic or control-flow statements into templates for greater reusability and modularity. Jinja’s template engine is responsible for processing the code within the templates and generating the output to the final text based document.

      Templating languages are well known within the context of creating web pages in a Model View Controller architecture. In this scenario the template engine processes source data, like the data found in a database, and a web template that includes a mixture of HTML and the templating language. These two pieces are then used to generate the final web page for users to consume. Templating languages, however, are not limited to web pages. Salt, a popular Python based configuration management software, supports Jinja to allow for abstraction and reuse within Salt state files and regular files.

      This guide will provide an overview of the Jinja templating language used primarily within Salt. If you are not yet familiar with Salt concepts, review the Beginner’s Guide to Salt before continuing. While you will not be creating Salt states of your own in this guide, it is also helpful to review the Getting Started with Salt – Basic Installation and Setup guide.

      Jinja Basics

      This section provides an introductory description of Jinja syntax and concepts along with examples of Jinja and Salt states. For an exhaustive dive into Jinja, consult the official Jinja Template Designer Documentation.

      Applications like Salt can define default behaviors for the Jinja templating engine. All examples in this guide use Salt’s default Jinja environment options. These settings can be changed in the Salt master configuration file:

      /etc/salt/master
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      
      # Default Jinja environment options for all templates except sls templates
      #jinja_env:
      #  block_start_string: '{%'
      #  block_end_string: '%}'
      #  variable_start_string: '{{'
      #  variable_end_string: '}}'
      #  comment_start_string: '{#'
      #  comment_end_string: '#}'
      #  line_statement_prefix:
      #  line_comment_prefix:
      #  trim_blocks: False
      #  lstrip_blocks: False
      #  newline_sequence: 'n'
      #  keep_trailing_newline: False
      
      # Jinja environment options for sls templates
      #jinja_sls_env:
      #  block_start_string: '{%'
      #  block_end_string: '%}'
      #  variable_start_string: '{{'
      #  variable_end_string: '}}'
      #  comment_start_string: '{#'
      #  comment_end_string: '#}'
      #  line_statement_prefix:
      #  line_comment_prefix:
      #  trim_blocks: False
      #  lstrip_blocks: False

      Note

      Before including Jinja in your Salt states, be sure to review the Salt and Jinja Best Practices section of this guide to ensure that you are creating maintainable and readable Salt states. More advanced Salt tools and concepts can be used to improve the modularity and reusability of some of the Jinja and Salt state examples used throughout this guide.

      Delimiters

      Templating language delimiters are used to denote the boundary between the templating language and another type of data format like HTML or YAML. Jinja uses the following delimiters:

      Delimiter Syntax Usage
      {% ... %} Control structures
      {{ ... }} Evaluated expressions that will print to the template output
      {# ... #} Comments that will be ignored by the template engine
      # ... ## Line statements

      In this example Salt state file, you can differentiate the Jinja syntax from the YAML because of the {% ... %} delimiters surrounding the if/else conditionals:

      /srv/salt/webserver/init.sls
      1
      2
      3
      4
      5
      6
      7
      
      {% if grains['group'] == 'admin' %}
          America/Denver:
              timezone.system:
      {% else %}
          Europe/Minsk:
              timezone.system:
      {% endif %}

      See the control structures section for more information on conditionals.

      Template Variables

      Template variables are available via a template’s context dictionary. A template’s context dictionary is created automatically during the different stages of a template’s evaluation. These variables can be accessed using dot notation:

      {{ foo.bar }}
      

      Or they can be accessed by subscript syntax:

      {{ foo['bar'] }}
      

      Salt provides several context variables that are available by default to any Salt state file or file template:

      • Salt: The salt variable provides a powerful set of Salt library functions.

        {{ salt['pw_user.list_groups']('jdoe') }}
        

        You can run salt '*' sys.doc from the Salt master to view a list of all available functions.

      • Opts: The opts variable is a dictionary that provides access to the content of a Salt minion’s configuration file:

        {{ opts['log_file'] }}
        

        The location for a minion’s configuration file is /etc/salt/minion.

      • Pillar: The pillar variable is a dictionary used to access Salt’s pillar data:

        {{ pillar['my_key'] }}
        

        Although you can access pillar keys and values directly, it is recommended that you use Salt’s pillar.get variable library function, because it allows you to define a default value. This is useful when a value does not exist in the pillar:

        {{ salt['pillar.get']('my_key', 'default_value') }}
        
      • Grains: The grains variable is a dictionary and provides access to minions’ grains data:

        {{ grains['shell'] }}
        

        You can also use Salt’s grains.get variable library function to access grain data:

        {{ salt['grains.get']('shell') }}
        
      • Saltenv: You can define multiple salt environments for minions in a Salt master’s top file, such as base, prod, dev and test. The saltenv variable provides a way to access the current Salt environment within a Salt state file. This variable is only available within Salt state files.

        {{ saltenv }}
        
      • SLS: With the sls variable you can obtain the reference value for the current state file (e.g. apache, webserver, etc). This is the same value used in a top file to map minions to state files or via the include option in state files:

        {{ sls }}
        
      • Slspath: This variable provides the path to the current state file:

        {{ slspath }}
        

      Variable Assignments

      You can assign a value to a variable by using the set tag along with the following delimiter and syntax:

      {% set var_name = myvalue %}
      

      Follow Python naming conventions when creating variable names. If the variable is assigned at the top level of a template, the assignment is exported and available to be imported by other templates.

      Any value generated by a Salt template variable library function can be assigned to a new variable.

      {% set username = salt['user.info']('username') %}
      

      Filters

      Filters can be applied to any template variable via a | character. Filters are chainable and accept optional arguments within parentheses. When chaining filters, the output of one filter becomes the input of the following filter.

      {{ '/etc/salt/' | list_files | join('n') }}
      

      These chained filters will return a recursive list of all the files in the /etc/salt/ directory. Each list item will be joined with a new line.

        
        /etc/salt/master
        /etc/salt/proxy
        /etc/salt/minion
        /etc/salt/pillar/top.sls
        /etc/salt/pillar/device1.sls
        
      

      For a complete list of all built in Jinja filters, refer to the Jinja Template Design documentation. Salt’s official documentation includes a list of custom Jinja filters.

      Macros

      Macros are small, reusable templates that help you to minimize repetition when creating states. Define macros within Jinja templates to represent frequently used constructs and then reuse the macros in state files.

      /srv/salt/mysql/db_macro.sls
      1
      2
      3
      4
      5
      6
      7
      8
      
      {% macro mysql_privs(user, grant=select, database, host=localhost) %}
      {{ user }}_exampledb:
         mysql_grants.present:
          - grant: {{ grant }}
          - database: {{ database }}
          - user: {{user}}
          - host: {{ host }}
      {% endmacro %}
      db_privs.sls
      1
      2
      3
      
      {% import "/srv/salt/mysql/db_macro.sls" as db -%}
      
      db.mysql_privs('jane','exampledb.*','select,insert,update')

      The mysql_privs() macro is defined in the db_macro.sls file. The template is then imported to the db variable in the db_privs.sls state file and is used to create a MySQL grants state for a specific user.

      Refer to the Imports and Includes section for more information on importing templates and variables.

      Imports and Includes

      Imports

      Importing in Jinja is similar to importing in Python. You can import an entire template, a specific state, or a macro defined within a file.

      {% import '/srv/salt/users.sls' as users %}
      

      This example will import the state file users.sls into the variable users. All states and macros defined within the template will be available using dot notation.

      You can also import a specific state or macro from a file.

      {% from '/srv/salt/user.sls' import mysql_privs as grants %}
      

      This import targets the macro mysql_privs defined within the user.sls state file and is made available to the current template with the grants variable.

      Includes

      The {% include %} tag renders the output of another template into the position where the include tag is declared. When using the {% include %} tag the context of the included template is passed to the invoking template.

      /srv/salt/webserver/webserver_users.sls
      1
      2
      3
      4
      
      include:
        - groups
      
      {% include 'users.sls' %}

      Note

      Import Context Behavior

      By default, an import will not include the context of the imported template, because imports are cached. This can be overridden by adding with context to your import statements.

      {% from '/srv/salt/user.sls' import mysql_privs with context %}
      

      Similarly, if you would like to remove the context from an {% include %}, add without context:

      {% include 'users.sls' without context %}
      

      Whitespace Control

      Jinja provides several mechanisms for whitespace control of its rendered output. By default, Jinja strips single trailing new lines and leaves anything else unchanged, e.g. tabs, spaces, and multiple new lines. You can customize how Salt’s Jinja template engine handles whitespace in the Salt master configuration file. Some of the available environment options for whitespace control are:

      • trim_blocks: When set to True, the first newline after a template tag is removed automatically. This is set to False by default in Salt.
      • lstrip_blocks: When set to True, Jinja strips tabs and spaces from the beginning of a line to the start of a block. If other characters are present before the start of the block, nothing will be stripped. This is set to False by default in Salt.
      • keep_trailing_newline: When set to True, Jinja will keep single trailing newlines. This is set to False by default in Salt.

      To avoid running into YAML syntax errors, ensure that you take Jinja’s whitespace rendering behavior into consideration when inserting templating markup into Salt states. Remember, Jinja must produce valid YAML. When using control structures or macros, it may be necessary to strip whitespace from the template block to appropriately render valid YAML.

      To preserve the whitespace of contents within template blocks, you can set both the trim_blocks and lstrip_block options to True in the master configuration file. You can also manually enable and disable the white space environment options within each template block. A - character will set the behavior of trim_blocks and lstrip_blocks to False and a + character will set these options to True for the block:

      For example, to strip the whitespace after the beginning of the control structure include a - character before the closing %}:

      {% for item in [1,2,3,4,5] -%}
          {{ item }}
      {% endfor %}
      

      This will output the numbers 12345 without any leading whitespace. Without the - character, the output would preserve the spacing defined within the block.

      Control Structures

      Jinja provides control structures common to many programming languages such as loops, conditionals, macros, and blocks. The use of control structures within Salt states allow for fine-grained control of state execution flow.

      For Loops

      For loops allow you to iterate through a list of items and execute the same code or configuration for each item in the list. Loops provide a way to reduce repetition within Salt states.

      /srv/salt/users.sls
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      
      {% set groups = ['sudo','wheel', 'admins'] %}
      include:
        - groups
      
      jane:
        user.present:
          - fullname: Jane Doe
          - shell: /bin/zsh
          - createhome: True
          - home: /home/jane
          - uid: 4001
          - groups:
          {%- for group in groups %}
            - {{ group }}
          {%- endfor -%}

      The previous for loop will assign the user jane to all the groups in the groups list set at the top of the users.sls file.

      Conditionals

      A conditional expression evaluates to either True or False and controls the flow of a program based on the result of the evaluated boolean expression. Jinja’s conditional expressions are prefixed with if/elif/else and placed within the {% ... %} delimiter.

      /srv/salt/users.sls
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      
      {% set users = ['anna','juan','genaro','mirza'] %}
      {% set admin_users = ['genaro','mirza'] %}
      {% set admin_groups = ['sudo','wheel', 'admins'] %}
      {% set org_groups = ['games', 'webserver'] %}
      
      
      include:
        - groups
      
      {% for user in users %}
      {{ user }}:
        user.present:
          - shell: /bin/zsh
          - createhome: True
          - home: /home/{{ user }}
          - groups:
      {% if user in admin_users %}
          {%- for admin_group in admin_groups %}
            - {{ admin_group }}
          {%- endfor -%}
      {% else %}
          {%- for org_group in org_groups %}
            - {{ org_group }}
          {% endfor %}
      {%- endif -%}
      {% endfor %}

      In this example the presence of a user within the admin_users list determines which groups are set for that user in the state. Refer to the Salt Best Practices section for more information on using conditionals and control flow statements within state files.

      Template Inheritance

      With template inheritance you can define a base template that can be reused by child templates. The child template can override blocks designated by the base template.

      Use the {% block block_name %} tag with a block name to define an area of a base template that can be overridden.

      /srv/salt/users.jinja
      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      {% block user %}jane{% endblock %}:
        user.present:
          - fullname: {% block fullname %}{% endblock %}
          - shell: /bin/zsh
          - createhome: True
          - home: /home/{% block home_dir %}
          - uid: 4000
          - groups:
            - sudo

      This example creates a base user state template. Any value containing a {% block %} tag can be overridden by a child template with its own value.

      To use a base template within a child template, use the {% extends "base.sls"%} tag with the location of the base template file.

      /srv/salt/webserver_users.sls
      1
      2
      3
      4
      
      {% extends "/srv/salt/users.jinja" %}
      
      {% block fullname %}{{ salt['pillar.get']('jane:fullname', '') }}{% endblock %}
      {% block home_dir %}{{ salt['pillar.get']('jane:home_dir', 'jane') }}{% endblock %}

      The webserver_users.sls state file extends the users.jinja template and defines values for the fullname and home_dir blocks. The values are generated using the salt context variable and pillar data. The rest of the state will be rendered as the parent user.jinja template has defined it.

      Salt and Jinja Best Practices

      If Jinja is overused, its power and versatility can create unmaintainable Salt state files that are difficult to read. Here are some best practices to ensure that you are using Jinja effectively:

      • Limit how much Jinja you use within state files. It is best to separate the data from the state that will use the data. This allows you to update your data without having to alter your states.
      • Do not overuse conditionals and looping within state files. Overuse will make it difficult to read, understand and maintain your states.
      • Use dictionaries of variables and directly serialize them into YAML, instead of trying to create valid YAML within a template. You can include your logic within the dictionary and retrieve the necessary variable within your states.

        The {% load_yaml %} tag will deserialize strings and variables passed to it.

         {% load_yaml as example_yaml %}
             user: jane
             firstname: Jane
             lastname: Doe
         {% endload %}
        
         {{ example_yaml.user }}:
            user.present:
              - fullname: {{ example_yaml.firstname }} {{ example_yaml.lastname }}
              - shell: /bin/zsh
              - createhome: True
              - home: /home/{{ example_yaml.user }}
              - uid: 4001
              - groups:
                - games
        

        Use {% import_yaml %} to import external files of data and make the data available as a Jinja variable.

         {% import_yaml "users.yml" as users %}
        
      • Use Salt Pillars to store general or sensitive data as variables. Access these variables inside state files and template files.

      More Information

      You may wish to consult the following resources for additional information on this topic. While these are provided in the hope that they will be useful, please note that we cannot vouch for the accuracy or timeliness of externally hosted materials.

      Join our Community

      Find answers, ask questions, and help others.

      This guide is published under a CC BY-ND 4.0 license.



      Source link