Skip to content

Instantly share code, notes, and snippets.

@dmccuk
Last active May 9, 2025 09:51
Show Gist options
  • Save dmccuk/3824c5ebcc1368236296bb943cd82aed to your computer and use it in GitHub Desktop.
Save dmccuk/3824c5ebcc1368236296bb943cd82aed to your computer and use it in GitHub Desktop.

🧰 Morpheus + Ansible Integration (Standalone Mode) for VM Provisioning (On-Prem & Cloud)

This guide walks you through setting up Morpheus 8 to provision virtual machines both on-prem (vSphere, KVM, Hyper-V) and in the cloud (AWS, Azure, etc.), and to automatically configure them using Ansible (Standalone mode) running from the Morpheus appliance.


🧱 1. Prepare Base VM Template

Ensure your VM image (on-prem or cloud) includes:

βœ… Requirements:

  • OS: Ubuntu, CentOS, RHEL, etc.
  • User: A non-root user (e.g. ubuntu, morpheus, centos)
  • SSH Access:
    • Public key added to ~/.ssh/authorized_keys
    • sshd enabled
  • Sudo Access:
    • Passwordless sudo configured:
      echo "morpheus ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/morpheus

☁️ 2. Configure Morpheus for VM Provisioning

A. Add a Cloud Integration

Navigate to: Infrastructure > Clouds > + Add

  • For on-prem:
    • VMware vCenter
    • KVM (libvirt)
    • Hyper-V
  • For cloud:
    • AWS, Azure, GCP, etc.
  • Provide access credentials, resource pools, networks

B. Create Credentials

Navigate to: Infrastructure > Credentials > + Add

  • Type: SSH Key or Username/Password
  • Username: User in your base image
  • Private Key: Paste SSH private key
  • Save and assign to your Cloud or Instance Layout

C. Create Instance Type & Layout

Go to: Library > Instance Types > + Add

  • Name: Linux-Base (example)
  • Create a Layout:
    • Select Cloud, image/template, size
    • Assign credential and provisioning method (Cloud-Init if supported)

βš™οΈ 3. Add Ansible Standalone Integration

Navigate to: Admin > Integrations > + Add

  • Type: Ansible
  • Mode: Standalone
  • Executable Path: /usr/bin/ansible-playbook
  • Save the integration

πŸ“ 4. Upload or Link Ansible Playbook

Option A: Upload a Playbook

Navigate to: Library > Templates > Scripts > + Add

  • Name: Configure Webserver
  • Type: Ansible Playbook
  • Upload a .yml file or ZIP with site.yml
  • Choose your Ansible integration

Option B: Use Git

Navigate to: Admin > Integrations > Code Repositories > + Add

  • Add your Git repo (public or private)
  • Morpheus syncs your playbooks automatically

πŸ“œ 5. Ansible Playbook Structure

You can use a flat playbook or a role-based layout. Example below:

site.yml

---
- name: Configure VM post-provision
  hosts: all
  become: yes
  vars:
    ansible_python_interpreter: /usr/bin/python3
  roles:
    - webserver

roles/webserver/tasks/main.yml

---
- name: Install NGINX
  apt:
    name: nginx
    state: present
  when: ansible_os_family == "Debian"

- name: Start NGINX
  service:
    name: nginx
    state: started
    enabled: yes

Optional: roles/webserver/templates/nginx.conf.j2

server {
  listen 80;
  server_name {{ inventory_hostname }};
  root /var/www/html;
}

πŸ”„ 6. Create Automation Workflow

A. Create Task

Go to: Library > Automation > Tasks > + Add

  • Type: Ansible Playbook
  • Select uploaded .yml or synced playbook
  • Choose the correct Ansible integration

B. Create Workflow

Go to: Library > Automation > Workflows > + Add

  • Type: Provisioning Workflow
  • Add the Ansible Task you just created

πŸ”— 7. Attach Workflow to VM Build Process

Attach the workflow to:

  • Instance Layout
  • Blueprint
  • Or manually during provisioning

Go to: Library > Instance Types > [Layout] > Automation Tab
Attach the Provisioning Workflow


βœ… 8. Test the Workflow

Provision a new VM using your configured Instance Type. Then:

  • Navigate to: Instances > [Your VM] > History
  • View the Automation Logs to confirm Ansible executed successfully

πŸ§ͺ Optional: Use Cloud-Init to Inject SSH Key/User

Create a Cloud-Init script to inject keys and users at build time:

#cloud-config
users:
  - name: morpheus
    sudo: ALL=(ALL) NOPASSWD:ALL
    groups: sudo
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-rsa AAAAB3Nza...your_key_here...
disable_root: true
ssh_pwauth: false

Upload at: Library > Templates > Scripts > + Add
Type: Cloud-Init
Attach to your Layout’s Provisioning Scripts section


πŸ“Ž Summary Checklist

Step Action
βœ… Prepare image with SSH, user, sudo
βœ… Add Cloud (vSphere, AWS, etc.)
βœ… Create and assign Credentials
βœ… Set up Ansible Standalone Integration
βœ… Upload or sync Ansible Playbooks
βœ… Create Ansible Task & Workflow
βœ… Attach Workflow to Layout or Blueprint
βœ… Provision VM and confirm automation

🧰 Bonus: Debug Task Example

Add this task to print host info during provisioning:

- name: Debug host info
  debug:
    msg: "Hostname: {{ inventory_hostname }}, IP: {{ ansible_host }}, Cloud: {{ cloud_name | default('N/A') }}"

get_pure_volumes

- name: Connect to Pure Storage and list volumes
  hosts: localhost
  gather_facts: no

  vars_prompt:
    - name: "purefa_username"
      prompt: "Enter your Pure Storage username"
      private: no

    - name: "purefa_password"
      prompt: "Enter your Pure Storage password"
      private: yes

    - name: "purefa_ip"
      prompt: "Enter the IP of the Pure Storage array"
      private: no

    - name: "api_version"
      prompt: "Enter the Pure API version (e.g., 2.2)"
      private: no

  tasks:

    - name: Authenticate with Pure Storage and get Bearer token
      uri:
        url: "https://{{ purefa_ip }}/api/{{ api_version }}/auth/session"
        method: POST
        user: "{{ purefa_username }}"
        password: "{{ purefa_password }}"
        force_basic_auth: yes
        validate_certs: no
        status_code: 200
        return_content: yes
        headers:
          Content-Type: "application/json"
      register: auth_response

    - name: Extract the bearer token
      set_fact:
        bearer_token: "{{ auth_response.json.session.id }}"

    - name: Save bearer token to file
      copy:
        content: "{{ bearer_token }}"
        dest: "./pure_token.txt"
        mode: '0600'

    - name: Get volume info using the bearer token
      uri:
        url: "https://{{ purefa_ip }}/api/{{ api_version }}/volumes"
        method: GET
        headers:
          Authorization: "Bearer {{ bearer_token }}"
        validate_certs: no
        return_content: yes
      register: volumes_response

    - name: Show volume info
      debug:
        var: volumes_response.json

[defaults]
inventory = localhost,
collections_paths = ./collections
host_key_checking = False
retry_files_enabled = False
deprecation_warnings = False
timeout = 30
stdout_callback = yaml
interpreter_python = auto_silent

[privilege_escalation]
become = False

[ssh_connection]
pipelining = True

list_volumes.yml

- name: List all volumes on the FlashArray
  hosts: localhost
  gather_facts: false
  collections:
    - purestorage.flasharray

  tasks:
    - name: Load API token and array IP
      include_vars:
        file: "./vars/pure_connection.yml"

    - name: Get volume info
      purefa_volume_info:
        api_token: "{{ api_token }}"
        fa_url: "{{ fa_url }}"
      register: volume_info

    - name: Display volume names
      debug:
        msg: "{{ item.name }}"
      loop: "{{ volume_info.volumes }}"

list_hosts_and_wwns.yml

- name: List hosts and their WWNs
  hosts: localhost
  gather_facts: false
  collections:
    - purestorage.flasharray

  tasks:
    - name: Load API token and array IP
      include_vars:
        file: "./vars/pure_connection.yml"

    - name: Get host info
      purefa_host_info:
        api_token: "{{ api_token }}"
        fa_url: "{{ fa_url }}"
      register: host_info

    - name: Show each host's WWNs
      debug:
        msg: "Host: {{ item.name }}, WWNs: {{ item.wwn }}"
      loop: "{{ host_info.hosts }}"

list_protection_groups.yml

- name: List protection groups and member volumes
  hosts: localhost
  gather_facts: false
  collections:
    - purestorage.flasharray

  tasks:
    - name: Load API token and array IP
      include_vars:
        file: "./vars/pure_connection.yml"

    - name: Get pgroup info
      purefa_pgroup_info:
        api_token: "{{ api_token }}"
        fa_url: "{{ fa_url }}"
      register: pgroup_info

    - name: Show pgroup members
      debug:
        msg: "PGroup: {{ item.name }}, Volumes: {{ item.volumes }}"
      loop: "{{ pgroup_info.pgroups }}"

get_array_info.yml

- name: Get FlashArray system info
  hosts: localhost
  gather_facts: false
  collections:
    - purestorage.flasharray

  tasks:
    - name: Load API token and array IP
      include_vars:
        file: "./vars/pure_connection.yml"

    - name: Get array info
      purefa_array_info:
        api_token: "{{ api_token }}"
        fa_url: "{{ fa_url }}"
      register: array_info

    - name: Show array capacity and model
      debug:
        msg: "Model: {{ array_info.arrays[0].model }}, Capacity: {{ array_info.arrays[0].capacity }}"

get_bearer_token.yml

- name: Authenticate with Pure Storage and save token + IP
  hosts: localhost
  gather_facts: false

  vars_prompt:
    - name: "purefa_username"
      prompt: "Enter your Pure Storage username"
      private: no

    - name: "purefa_password"
      prompt: "Enter your Pure Storage password"
      private: yes

    - name: "purefa_ip"
      prompt: "Enter the IP of the Pure Storage array"
      private: no

    - name: "api_version"
      prompt: "Enter the Pure API version (e.g., 2.2)"
      private: no

  tasks:

    - name: Authenticate and get Bearer token
      uri:
        url: "https://{{ purefa_ip }}/api/{{ api_version }}/auth/session"
        method: POST
        user: "{{ purefa_username }}"
        password: "{{ purefa_password }}"
        force_basic_auth: yes
        validate_certs: no
        status_code: 200
        return_content: yes
        headers:
          Content-Type: "application/json"
      register: auth_response

    - name: Extract bearer token
      set_fact:
        bearer_token: "{{ auth_response.json.session.id }}"

    - name: Ensure vars directory exists
      file:
        path: "./vars"
        state: directory
        mode: '0755'

    - name: Save token and IP address to a vars file
      copy:
        content: |
          api_token: "{{ bearer_token }}"
          fa_url: "https://{{ purefa_ip }}"
        dest: "./vars/pure_connection.yml"
        mode: '0600'

    - name: Confirm saved info
      debug:
        msg: "Saved bearer token and array IP to vars/pure_connection.yml"

dummy file!!

pure_connection.yml

api_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.DUMMYTOKENVALUE.abc123
fa_url: https://10.99.55.14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment