vRA SaltStack Config Header

Deploying a Windows VM using vRealize Automation & configuring with SaltStack Config – Part 2

The first blog post of this two part series covered getting our vRealize Automation and SaltStack Config environments prepared, a Windows Server template prepared, and a testing a successful deployment that has the SaltStack minion installed.

In this second part, we’ll now focus on setting up out state files to configure the Windows Server to our example requirements and start deploying virtual machines using vRA that are configured by SaltStack Config.

We’ll cover the following areas:

For both blog posts I’ve also recorded an accompanying video detailing the configuration. Below is part two, and you can see the part one video on first blog post.

Configure the SaltStack State Files

The main part of SaltStack is the SLS, or SaLt State file. This is a representation of the state in which a system should be in, contains the configuration information that the system should adhere to, or be configured to. By default, a State file is built using the YAML format.

You can read more about State Files on the official Salt website which gives you a good introduction in getting started, and background information on the configuration I am going to detail below.

As a quick overview, but I really suggest you read the above link if this is your first look into Salt.

# The first line is the ID for the data that follows
my_first_state:
# The second line, two space indented, is state module to be run, in the format {module.function}
  service.enabled:
# The third line, four space indented, are the parameters for the state module
    - name: Spooler

Within a state file, you also have the ability to use a templating language, such as Jinja, which is the default for State Files. This language within a state file is evaluated and computed before the YAML itself, making it useful for writing statements, and computing user inputs or dynamic variables. You can learn more about this templating language in the Salt documentation.

For this blog post example, I am going to configure the following file structure, and explain what is going on in each step. I think the naming makes it obvious which configurations I’ll be passing through to my deployed Windows Server VMs.

Base
- Windows
  - ad-join
  - baseline
  - remote-desktop
  - services
  - software-install
  - users

Each state file will be configured on the SaltStack Config file server, via the UI. The file server, is actually a database on the backend, but looks like a file server configuration within the UI (we won’t dive into how the backend works in this blog post).

To create your folders and file structure:

  • Expand Config in the left-hand navigation pane
  • Select File Server
  • Click the blue “+ Create” button
  • Select which Salt Environment you want to create the file
    • I recommend that you use base for your files as you get started. There are many ways to use Salt environments, however they can also introduce complexity if misused or misunderstood.
  • Provide the folder/file location and name
    • If the Folder doesn’t already exist, it will be created as part of the save operation
    • Its good practice for your main or only state files for given action to be named “init.sls”, as salt will automatically call this file name with the folder structure without additional references.
  • Input your configuration
  • Click the blue “Save” button

vRA SaltStack Config - File Server - Create SLS file

I’ll be creating my state files in the following structure under the Base environment. I have included a screenshot of my final structure as well, so when I say I’ve created the Users state as an init.sls file, that will be within the folder structure /Windows/users/init.sls

You can also find the files for everything detailed in this blog located on this git repo.

Windows/
  ad-join/
  baseline/
  remote-desktop/
  services/
  software-install/
  users/

vRA SaltStack Config - File Server - Hierarchy example

Now let’s explore my configuration in detail.

Active Directory Join

The first state is to set the computer name, before we add it to Active Directory. We get the name from the Grain information of the Minion Target that the state is executed against. Using the grain ‘id’, is the name that the minion is registered as in SaltStack Config, this will mirror the resource object name in vRA, computed in part by the name property on the VM resource object.

The second state adds the machine to the specified active directory domain. The username and password to run this code are located in a Pillar configuration, Pillar is a salt data structure. (I’ll show you the configuration of this further below). The final call out in this configuration is the use of the salt requisite (a system used to create relationships between states) “require”, which ensures that the named states have executed successfully before the state runs. In my example, I need to ensure the computer name is configured correctly before the machine is added to Active Directory.

States used:

I have named this file “init.sls” – check the file structure future down.

# Set Hostname
set_hostname:
  system.computer_name:
    # This Jinja control structure pulls the id value from the grain of the minion
    - name: {{ grains['id']  }}

join_to_domain:
  system.join_domain:
    - name: simon.local
    # This Jinja control structure retrieves the named data objects from the pillar called "ad-join"
    - username: {{ salt['pillar.get']('ad-join:username')}}
    - password: {{ salt['pillar.get']('ad-join:password')}}
    - restart: True
    - account_ou: OU=salt-managed,DC=simon,DC=local
    # The require statement builds a dependency that prevents a state from executing until all required states execute successfully
    - require: 
      - set_hostname

To configure Pillar data:

  • Click on Config in the left-hand navigation
  • Select Pillars
  • Click the blue “+ Create” button
  • Provide a name for the Pillar
  • Input your data, this has to be in JSON format
  • Click the blue “Save” button

Hopefully now with the below screenshot, you can reference the JSON formatting of the object data, back to the Salt State above.

vRA SaltStack Config - Pillar - Create

Remote Desktop

In this state file, I want to ensure that the Remote Desktop service is enabled and running, as well as configuring the required Windows registry keys.

In this configuration, we have added some programming logic to the state file, an If statement, which looks up the minion targets grain data, to see if the OS is reported as Windows. If this is true, then proceeding states are run, if the OS is reports as a different operating system, the configurations are not run, as they are all contained within the single If statement.

States used:

I have named this file “init.sls” – check the file structure futher down.

# If statement to check the Minion targets grain reports the OS as Windows
{% if grains.os == 'Windows' %}

enable_service_rds:
  service.enabled:
    - name: TermService
  
start_service_rds:
  service.running:
    - name: TermService
    
reg_enable_rds_connections:
  reg.present:    
    - name: HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server
    - vname: fDenyTSConnections
    - vdata: 0
    - vtype: REG_DWORD
    
{% endif %}
Configuring Windows Services

This might be getting a little simpler now, this state file configures a few services in the Windows OS, using the same If statement as described above to ensure it is only applied to Windows Guest Operating Systems.

The Services Name in Windows is not always the same as the friendly name, you can look this up by viewing the Service on a Windows machine, as highlighted in the below screenshot.

vRA SaltStack Config - Windows Services

States used:

I have named this file “init.sls” – check the file structure further down.

{% if grains.os == 'Windows' %}

disable_print_spooler:
  service.disabled:
    - name: Spooler

stop_print_spooler:
  service.dead:
    - name: Spooler
    
disable_win_telemetry:
  service.disabled:
    - name: DiagTrack

stop_win_telemetry:
  service.dead:
    - name: DiagTrack

{% endif %}
Configuring local Windows users

To configure the local user settings on the Windows Server, I am disabling the Built-in Administrator account, and renaming it. To add a little more configuration options, I’ve used Jinja to set a variable at the start of the state and calling that from a dynamic pillar (generated on the fly). This means if I wanted, I could provide this variable in my vRA Cloud Template to be passed down, but maybe thats for another blog post! There is enough to learn in this one.

States used:

I have named this file “init.sls” – check the file structure furher down.

# Set a variable using Jinja and the pillar function to provide the name to the state
{%- set RENAME_ADMIN = pillar.get('RENAME_ADMIN', 'localadmin') %}

rename_administrator:
  lgpo.set:
    - computer_policy:
        "Accounts: Rename administrator account": {{ RENAME_ADMIN }}

{% set DomainRole = salt['system.get_system_info']()['os_type'] %}
{% if DomainRole == 'Domain Controller' %}
notify_notMS-administrator_account_disable:
  test.nop:
    - name: 'This system is not a Windows Member Server'
{%- else %}
administrator_account_status_disable:
  lgpo.set:
    - computer_policy:
        "Accounts: Administrator account status": 'Disabled'
    - require:
      - rename_administrator
{% endif %}
Installing Software (Two different ways)

I’ve left the software installation section until the end of the blog post, as I want to cover two ways to do this.

The first one will be using something called Win-Repo or Windows Package Manager, which essentially is a set of Salt configurations to allow a Linux yum/apt package manager like experience.

Once configured on your SaltStack Config Appliance, this is one of the easiest options to manage software on your Windows Machines.

Option 1 – Win Repo

To get started:

  • SSH to your SaltStack Config Appliance
  • Install Git and GitPython
    • yum install git
      pip install GitPython

     

  • Run the following command to download the win_repo data
    • salt-run winrepo.update_git_repos

vRA SaltStack Config - yum install git

vRA SaltStack Config - pip install GitPython

vRA SaltStack Config - salt-run winrepo.update_git_repos

You will now have a folder structure as shown in the above screenshot in Green. Let’s take a quick look at what is contained within those folders.

vRA SaltStack Config - win-repo folder and files

The files are held in this public git repo, which you can also contribute to. It is also possible to use Win-Repo with your own configuration files and offline repository files as well.

Finally let’s take a look at one of the state files for an application. (I’ve removed some of the details to make it shorter, you can see the original file in the Git location linked above.

  • First a variable is set using Jinja to get the installation package language that should be used.
  • A Jinja control structure is used to define the version numbers for the software package
    • An if statement is used to help set the version that should be installed via a compare
  • The full name of the installation package is defined
  • An installer location is provided, in this case the Firefox download website, with the variable of the version being provided as necessary based on the State that will be run providing this.
  • Installer flags are specified to install the package
  • Uninstaller location is defined
  • Uninstaller flags are defined
  • Boolean to define is msiexe should be used to install the package
  • Boolean to define if the machine should be rebooted after the install.T

This configuration makes the package management very dynamic, with the ability to set which application version is installed, and how to install it and uninstall it from the system. These options will be used by the Salt State when specified, such as pkg.installed will use the installer location and flags, pkg.remove will use the uninstall location and flags.

I have named this file “firefox.sls” – check the file structure futher down.

{%- set lang = salt['config.get']('firefox:pkg:lang', 'en-US') %}

firefox_x64:
  {% for version in ['98.0.1', '98.0', '97.0.2', ... , '43.0.2', '43.0.1', '43.0', '42.0'] %}
  '{{ version }}':
    {% if salt["pkg.compare_versions"](version, "<", "90.0") -%}
    {%   set display_version = " " ~ version -%}
    {% endif -%}
    full_name: 'Mozilla Firefox{{ display_version | default("") }} (x64 {{ lang }})'
    installer: 'https://download-installer.cdn.mozilla.net/pub/firefox/releases/{{ version }}/win64/{{ lang }}/Firefox%20Setup%20{{ version }}.exe'
    install_flags: '/S'
    uninstaller: '%ProgramFiles%\Mozilla Firefox\uninstall\helper.exe'
    uninstall_flags: '/S'
    msiexec: False
    locale: en_US
    reboot: False
  {% endfor %}


Back to my state file, below I’m specifying the pkg inputs, which is basically the file name from the folder location, with the .sls removed.

States used:

ensure_firefox_installed:
  pkg.installed:
    - pkgs:
      - firefox_x64

Option 2 – Manually install the application via the automation of a Salt state

This second method is a bit more old school, and for those of you who have used scripts to install software before will feel a little more at home.

Essentially, it’s a state file which first checks to see if the software is already installed, if it isn’t, it will get the installation package from a provided location, and then install the package with the commands and flags provided.

This state file was already well documented, so I’m not going to expand on it any further, it also includes the links to the states used as well. However, I do want to call out one piece of configuration in this file.

Because my file location will cause an issue when this state is run, as it’s “c:\store\npp.exe” and when Jinja is processing the template file, it will find the characters “\n” which are escape characters, in particular, this one will cause a line break. To avoid this in the State file, we can simple escape any characters that cause issues by using “\” (a backslash before the character that needs escaping). In the below state my file location now becomes “C:\store\\npp.exe”.

Note: please set the folder location to one that exists on your template image otherwise this will fail.

Alternatively you could create a state file to create the folder such as the example below:

## State to create the Store folder - thank you Lars Olsson for pointing this out to me
store-folder:
  file.directory:
    - name:  c:\store

I have named this file “notepadplusplus.sls” – check the file structure further down.

# This is a fairly generic Salt state which can be repurposed to install
# agents or applications on Windows minions

# Explanation of Variables
# 
# AGENT - Name of the agent as it appears in the Windows "Apps and Features". It will be used
#         to test if the agent is already installed
#
# FILE_TARGET - Location where the agent installer file will be copied to the minion
#
# FILE_SOURCE - Source for the installer file. This can be HTTPS / FTP / S3 / Salt filesystem / etc. 
#               In this state, 'salt://' indicates the file is on the SaltStack config VM at /srv/salt.
#
# INSTALL_COMMAND - Switches and options required by the Windows package for installation
 
# Test if the target minion is running Windows before proceeding
{% if grains.os == 'Windows' %}

# Set variables using the Jinja templating language
# For more information:
# https://docs.saltproject.io/en/latest/topics/jinja/index.html
{% set AGENT = "Notepad" %}
{% set FILE_TARGET = 'C:\store\\npp.exe' %}
{% set FILE_SOURCE = "https://github.com/notepad-plus-plus/notepad-plus-plus/releases/download/v8.4.4/npp.8.4.4.Installer.x64.exe" %}
{% set INSTALL_COMMAND = "/S" %}

# Test if the agent is already installed by listing the applications and comparing the output to the
# AGENT variable
# For more information on test states:
# https://docs.saltproject.io/en/latest/ref/states/all/salt.states.test.html
{% if AGENT | substring_in_list(salt['pkg.list_pkgs']('versions_as_list=True')) %}
agent_present:
  test.nop:
    - name: {{ AGENT }} is already installed.
    {% set AGENT_ALREADY_INSTALLED = 'Yes' %}
{% else %}
agent_absent:
  test.nop:
    - name: {{ AGENT }} is not present. Installing now.
    {% set AGENT_ALREADY_INSTALLED = 'No' %}
{% endif %}

{% if AGENT_ALREADY_INSTALLED == 'No' %}
# More information on file.managed:
# https://docs.saltproject.io/en/latest/ref/states/all/salt.states.file.html#salt.states.file.managed
agent_copy:
  file.managed:
     - name: {{ FILE_TARGET }}
     - source: {{ FILE_SOURCE }}
     - skip_verify: True

# More information on cmd.run:
# https://docs.saltproject.io/en/latest/ref/states/all/salt.states.cmd.html
agent_install:
  cmd.run:
    - name: {{ FILE_TARGET }} {{ INSTALL_COMMAND }}

# More information on file.absent:
# https://docs.saltproject.io/en/latest/ref/states/all/salt.states.file.html#salt.states.file.absent
agent_installer_cleanup:
  file.absent:
    - name: {{ FILE_TARGET }}

{% endif %}

{% endif %}
Installing multiple software packages at once

Now we have two different software installs, we need to create a init.sls file, that references all the states files to install our software. (You will notice I have a third software installation, malwarebytes, this turned out to be a bit of a terrible example, and I documented my fun and games with that software package here).

That is as simple as using the “include” parameter in our state file, which will call the other state files to be run. You need to profile the name of the sls file, but not the extension “.sls” because of the way this is computed by the system, you can read more about in the Include documentation linked above.

I have called this file init.sls

include:
- Windows/software-install/notepadplusplus
- Windows/software-install/malwarebytes
- Windows/software-install/firefox
Baseline State File

Now we have all of the state files created, I am going to create one last state file to rule them all. Or rather to call them all.

In the baseline folder, I create an init.sls file, and using the “Include” parameter again, I list each of my state files. As mentioned before, provide the name of your state file, but not the “.sls” extention.

If you need to run state files that are located in another Salt environment, you can do this by pre-appending the salt environment name and “:”, as shown in bold in the below example to run the presence state file. (In this example all my files are in the base environment, so that configuration makes no difference here).

include:
  - base: presence
  - Windows/ad-join
  - Windows/software-install
  - Windows/users
  - Windows/remote-desktop
  - Windows/services

Below is a reminder of my full end state of all the files I’ve created in my environment.

Now when we run a state.apply against my /Windows/baseline/init.sls file, this will run all the other states I’ve created.

vRA SaltStack Config - File Server - Hierarchy example

Running the state files as part of a vRA Deployment

Now we are ready to go back and update our Cloud Template, so that when a virtual machine is deployed, we can apply our new baseline state file, that will apply all the other states we created.

The configuration change is quite simple, in part 1, I already provided the presence state file, we can simply swap that out for the Baseline state file. The Cloud Assembly UI will even provide you an autocomplete of all the available state files in your provided Salt Environment.

vRA SaltStack Config - Cloud Template - stateFiles

I also decided to increase the complexity of this Cloud Template some more, by including a Count property to the Cloud Machine resource object, so that multiple VMs can be deployed at once, and added the “allocatePerInstance: true” property on the machine and SaltStack resource, this will tell SaltStack to run the configurations individually on each machine.

I’ve pasted my new Cloud Template below, with the changed highlighted in bold:

formatVersion: 1
inputs:
  Machine_Name:
    type: string
    title: Machine Name
    default: veducate
  Machine_Count:
    type: number
    title: Machine Count
    default: 2
resources:
  Cloud_SaltStack_1:
    type: Cloud.SaltStack
    allocatePerInstance: true
    properties:
      hosts:
        - ${resource.Cloud_Machine_1.id}
      masterId: saltstack_enterprise_installer
      stateFiles:
        - /Windows/baseline/init.sls
      saltEnvironment: base
      name: Windows Baseline
  Cloud_Machine_1:
    type: Cloud.vSphere.Machine
    allocatePerInstance: true
    properties:
      image: Windows Server
      flavor: small
      name: ${input.Machine_Name}
      constraints:
        - tag: env:dean
      remoteAccess:
        authentication: usernamePassword
        username: Admin
        password: VMware1!
      networks:
        - network: ${resource.NSX.id}
      count: ${input.Machine_Count}
  NSX:
    type: Cloud.NSX.Network
    properties:
      networkType: existing
      constraints:
        - tag: net:existing
        - tag: net:dean

Now let’s deploy this cloud template again and view the output of a successful deployment, you can see at the allocate phase for SaltStack, it has the names of both of my VMs as the count was set to “2” in the deployment.

vRA SaltStack Config - Cloud Template - Deploy - Multiple Machines

Below are the Logs from the “CREATE_IN_PROGRESS” task for the Saltstack Resource, and you can see I get two distinct sets of logs, for each machine.

vRA SaltStack Config - Cloud Template - Deploy - Multiple Machines - CREATE_IN_PROGRESS task for the Saltstack Resource

And finally if I check one of the State Job’s logs activity for the state.apply, I can see various bits of information about applying my state files.

vRA SaltStack Config - Cloud Template - Deploy - Multiple Machines - SaltStack JID RAW

Summary and wrap up

These two blog posts seem a little mamoth as I was typing them out, but part of that is because I’m taking you through ever little step, as well as showing you all the code and explaining it as well. Actually once you get going with SaltStack and over the hurdle of deploying a Windows VM with the minion installed, it’s quite easy to keep keep iterating on that.

When it comes to seeing all SaltStack has to offer for configuration management, we have only scratched the surface of it’s capabilities, which essentially is pushing configurations out to an endpoint. SaltStack can offer drift remediation, security compliance and vulnerability scanning and remediation as well, ontop of all this, you don’t need to just use SaltStack to manage your Operating systems, there are many customers out there who use SaltStack to configure there network and IoT devices as well.

We could achieve some of the items in this blog post by using the CloudBase-Init and “cloudConfig” feature within vRA, however the limitations of only running on first boot can be problematic, and of course, this is not configuration management, you don’t have day 2 abilities. If you are wanting to see a comparrison and also understand what can be done with the “cloudConfig” options, then I recommend this blog post by my colleague Simon Conyard.

This wrap-up will finish off leaving you with some more resources and blogs to check out:

Regards

Dean Lewis

4 thoughts on “Deploying a Windows VM using vRealize Automation & configuring with SaltStack Config – Part 2

  1. HI, excellent article, thanks!.

    Two things I have been struggling with and as suggestions for future articles if you are interested:

    1. Using a remote git repo for the salt master. The SLS files referenced by the vRA blueprint seem to be only the ones available to the raas. If you want to do source code control on your state files then you can’t use the inbuilt SSC file server and have to use a remote gitfs, but then you can’t reference them using the vRA resource. How to get around this?

    2. Deploying software to a windows minion that includes a reboot requirement – e.g. an sls to deploy a windows feature, rebooting, then another sls configures the feature.

  2. I don’t see any comments so I wanted to say this is awesome work. It was extremely useful and helpful and explained very well. I do wish you would show an example of how to use input from the cloud template as I need to change the OU the machine is being joined to but I’m not complaining at all. Very good work and thanks again.

    1. Thank you for the comment.

      For the OU query, you could simply have this as an input and list of your OUs to select from and send this through as a variable to salt for the ad-join file.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.