Vim is my favorite text editor and a tool that I use every day at work and at home. If you are in the IT field, you can relate that a text editor is one of your most powerful tools. Being comfortable and proficient in "{{ text_editor_of_your_choice }}" seams like a basic skill, that could get overlooked, but is a very valuable skill to have. As a sysadmin, you will most likely only be using the default settings in vim or nano across all the systems you manage, but on your personal system and workstation you should go nuts and customize the hell out of it. I use vundle to handle plugins and end up with a customized .vimrc file to handle the rest. If I had to do this setup manually, it would take a few hours at least. Half of that time Googling how I did something before and the other half forgetting what was there to begin with. With tools like ansible, you can easily recreate the customization you prefer across all of your systems, be that at work or your homelab and save yourself hours of frustration and repetition.

Ansible is a very reliable tool for managing your local workstation. A good place to start automating is at the base of your every day setup: text editor, user, groups, base packages, and every day tools inevitably installed anytime you get a new system or accidentally lose or kill your old one. If you are setting up Ansible for the first time, you'll want to start with their install docs. Once you've got it installed and are ready to go. My initial plan was to do this with a couple of vagrant boxes in my traditional way of testing new things by creating a Vagrantfile and going from there, but after getting started writing this and considering test kitchen, I've decided to check out molecule instead. It looks like the right tool for the job.

Table of Contents

Check out the source code before you get started, if you want to follow along. This role has been uploaded to ansible galaxy as well, if you want to use it in a playbook you cane can install it with

ansible-galaxy install jahrik.vim

ln -s ~/.ansible/roles ./molecule/default/roles



If you decide to set up a virtualenv to install molecule, you can do the following.

Create a virtual environment for molecule and install it.

mkvirtualenv -p /usr/bin/python2.7 ansible
pip install docker-py molecule

If you need an intro on python virtualenv you can find it here

To initialize a new role with molecule, you would run the following. Where -r is the role and -d is the driver.

molecule init role -r vim -d docker                                                                            

--> Initializing new role vim...
Initialized role in /home/jahrik/vim successfully.

But I had already created this role with ansible-galaxy init and had to initialize molecule this way from the role root directory.

molecule init scenario -r vim -d docker

--> Initializing new scenario default...
Initialized scenario in /home/jahrik/vim/molecule/default successfully.

With molecule initialized you should see a directory structure close to this.

├── defaults
│   └── main.yml
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── molecule
│   └── default
│       ├── create.yml
│       ├── destroy.yml
│       ├── Dockerfile.j2
│       ├── INSTALL.rst
│       ├── molecule.yml
│       ├── playbook.yml
│       ├── prepare.yml
│       └── tests
│           ├──
│           └── test_default.pyc
├── tasks
│   └── main.yml
└── vars
    └── main.yml

Set the containers you will be testing this on in ./molecule/default/molecule.yml. For me, this was defaulting to image: centos:7, so I just updated that to be the images I want to test against: ubuntu and fedora.


  name: galaxy
    role-file: requirements.yml
  name: docker
  name: yamllint
  - name: fedora
    image: fedora:27
  - name: ubuntu
    image: ubuntu:16.04
  name: ansible
    name: ansible-lint
  name: default
    - lint
    - destroy
    - dependency
    - syntax
    - create
    - prepare
    - converge
    - idempotence
    - side_effect
    - verify
    - destroy
  name: testinfra
    name: flake8


A role will allow for easy reuse of tasks. I want this role to work across multiple operating systems and be very customizable with the use of variables.

At the most basic level, if all you needed was a playbook to install vim on arch, ubuntu, and fedora, it would look like this. Very simple and uses the package module, which will act as a wrapper for many other ansible modules including: apt, yum, dnf, and pacman.


- hosts: all
    - name: Install vim
        name: vim
        state: present


Use this playbook as a task by copying it to ./tasks/main.yml without the - hosts: all or tasks: lines.


- name: Install vim
    name: vim
    state: present

With that, it's ready to go! Test it by running molecule test. There is a lot more to the output below, but these are the steps it takes.

molecule test

--> Test matrix

└── default
    ├── lint
    ├── destroy
    ├── dependency
    ├── syntax
    ├── create
    ├── prepare
    ├── converge
    ├── idempotence
    ├── side_effect
    ├── verify
    └── destroy

These are all commands molecule can perform when called individually. It's turning out to be a very beautiful tool! For example, if you just want to lint you can run the following.

molecule lint

--> Test matrix

└── default
    └── lint

--> Scenario: 'default'
--> Action: 'lint'
--> Executing Yamllint on files found in /home/jahrik/ansible/ansible-vim/...
Lint completed successfully.
--> Executing Flake8 on files found in /home/jahrik/ansible/ansible-vim/molecule/default/tests/...
Lint completed successfully.
--> Executing Ansible Lint on /home/jahrik/ansible/ansible-vim/molecule/default/playbook.yml...
Lint completed successfully.

And if I do a molecule converge it will run through all the steps up to converge. Pretty neat :-)

molecule converge

--> Test matrix

└── default
    ├── dependency
    ├── create
    ├── prepare
    └── converge


Vim should be successfully passing tests and installing in both environments by now. Install Vundle from github to handle vim plugins.


- name: create ~/.vim diretory
  become: false
    path: "~/.vim/"
    state: directory
    mode: 0755
    - vim

- name: Clone vundle from github
  become: false
    dest: "~/.vim/bundle/Vundle.vim"
    version: master
    - vim

The list of plugins to be installed with Vundle are going to very from person to person and grow and change, so these should be handled as variables. Add variables to ./defaults/main.yml. These variables will then be added to the .vimrc when it is created. The vundle variable is the list of vundle plugins that will be installed. Any and all of these variables can be over-written in a multitude of ways down the line, but if you don't, this is what they will default to. If you're familiar with vim already, some of these variables should make sense.


  - "The-NERD-tree"
  - "vim-flake8"
  - "surround.vim"
  - "vadv/vim-chef"
  - "vim-syntastic/syntastic"
  - "plasticboy/vim-markdown"
  - "ekalinin/Dockerfile.vim"
  - ""
  - ""
  - ""
  - ""
  - ""
  - laststatus=2
  - showtabline=2
  - noshowmode
  - foldmethod=indent
  - foldlevel=99
  - background=dark
  - completeopt=menuone,longest,preview
  - colorcolumn=101
  - cursorcolumn
  - cursorline
  - expandtab
  - tabstop=2
  - shiftwidth=2
  - spell
  - spelllang=en
  - spellfile=$HOME/.vim/en.utf-8.add
  - number
  # syntastic
  - statusline+=%#warningmsg#
  - statusline+=%{SyntasticStatuslineFlag()}
  - statusline+=%*

  - <c-j> <c-w>j
  - <c-k> <c-w>k
  - <c-l> <c-w>l
  - <c-h> <c-w>h
  - <leader>td <Plug>TaskLisk
  - "<leader>g :GundoToggle<CR>"
  - "<F2> :NERDTreeToggle<CR>"
  - "<F3> :setlocal spell! spelllang=en_us<CR>"

  - "<F4> :SyntasticToggleMode<CR>"
  - "<f5> :set number! number?<cr>"

  - "on"

  - slate

  - "on"
  - plugin indent on

  - g:pyflakes_use_quickfix = 0
  - g:NERDTreeDirArrows=0
  - g:syntastic_always_populate_loc_list = 1
  - g:syntastic_auto_loc_list = 1
  - g:syntastic_check_on_open = 1
  - g:syntastic_check_on_wq = 0
  - g:syntastic_ruby_checkers = ['rubocop']
  - g:syntastic_python_checkers = ['pylint', 'flake8']
  - g:syntastic_ansible_checkers = ['ansible_lint']
  - g:syntastic_chef_checkers = ['foodcritic', 'cookstyle']
  - g:syntastic_ignore_files = ['\m^roles/']

  - Fuckyou w !sudo tee %

  - FileType ruby,eruby set filetype=ruby.eruby.chef


The .vimrc file will be generated from a jinja2 template at run-time. It will pull in the variables from defaults/main.yml unless there are variables overwriting them. Read up on Ansible variable precedence, if you need more info.

Create a template file for the .vimrc.

What it's doing:

  • Configure vundle
  • lines with " are just comments
  • The {% for item in thing %} blocks
    • using all the variables from defaults/main.yml
    • create a line per list "{{ item }}"


set nocompatible              " be iMproved, required
filetype off                  " required

" set the runtime path to include Vundle and initialize
set rtp+=~/.vim/bundle/Vundle.vim/
call vundle#begin()

" let Vundle manage Vundle, required
Plugin 'gmarik/Vundle.vim'

{% for item in vundle %}
Bundle "{{ item }}"
{% endfor %}

" All of your Plugins must be added before the following line
call vundle#end()            " required
filetype plugin indent on    " required

" Brief help
" :PluginList       - lists configured plugins
" :PluginInstall    - installs plugins; append `!` to update or just
" :PluginUpdate
" :PluginSearch     - foo - searches for foo; append `!` to refresh local cache
" :PluginClean      - confirms removal of unused plugins; append `!` to
" auto-approve removal
" see :h vundle for more details or wiki for FAQ
" Put your non-Plugin stuff after this line

{% for item in set %}
set {{ item }}
{% endfor %}

{% for item in map %}
map {{ item }}
{% endfor %}

{% for item in nmap %}
nmap {{ item }}
{% endfor %}

{% for item in syntax %}
syntax {{ item }}
{% endfor %}

{% for item in colorscheme %}
colorscheme {{ item }}
{% endfor %}

{% for item in filetype %}
filetype {{ item }}
{% endfor %}

{% for item in let %}
let {{ item }}
{% endfor %}

{% for item in command %}
command {{ item }}
{% endfor %}

{% for item in autocmd %}
autocmd {{ item }}
{% endfor %}

Append the ./tasks/main.yml file with a template block to generate the .vimrc file. Notice that the src: is set to src: vimrc.j2 which pulls in our template above. This block also has something else new, notify: install plugins, which notifies a handler called install plugins.

- name: Generate ~/.vimrc
  become: false
    src: vimrc.j2
    dest: "~/.vimrc"
    mode: "0644"
  notify: install plugins
    - vim

Handlers are tasks that get called if something happens. Create a handler that installs vim plugins.


- name: install plugins
  command: vim +PluginInstall +qall
    - vim


Ansible galaxy is a public place to store roles for later use, much like github. Uploading this role was as aeasy as creating an account and linking it to github. Because I have a meta/main.yml file in my project it picked up on the vim role. It took a couple of days for me to get it to sync, but seams to be working now.

Now it is possible to download this role from galaxy.

ansible-galaxy install jahrik.vim                                                       
- downloading role 'vim', owned by jahrik
- downloading role from
- extracting jahrik.vim to /home/jahrik/.ansible/roles/jahrik.vim
- jahrik.vim (master) was installed successfully
- adding dependency: geerlingguy.git
- downloading role 'git', owned by geerlingguy
- downloading role from
- extracting geerlingguy.git to /home/jahrik/.ansible/roles/geerlingguy.git
- geerlingguy.git (2.0.0) was installed successfully

ln -s /home/jahrik/.ansible/roles molecule/default/roles

ansible-galaxy intro

generate github token

ansible-galaxy login --github-token REDACTED_TOKEN
Successfully logged into Galaxy as jahrik