Migration hooks provide powerful custom automation capabilities that enable running custom code at specific points during the migration process. This chapter covers local (container-based) hook development, deployment, and integration with migration workflows.

For AAP (Ansible Automation Platform) hooks that trigger remote job templates instead of running local containers, see Chapter 29: AAP Integration.

Overview: Enabling Custom Automation

What are Migration Hooks?

Migration hooks are Kubernetes resources that define custom automation to be executed during VM migration:

  • Pre-migration Hooks: Execute before VM conversion begins
  • Post-migration Hooks: Execute after VM migration completes
  • Custom Logic: Handle migration-specific requirements unique to your environment
  • Ansible-Based: Leverage Ansible playbooks for automation logic

Hook Types

Forklift supports two types of hooks:

Type Description Use Case
Local Runs a container image with an optional Ansible playbook Self-contained automation, custom images, no external dependencies
AAP Triggers a job template on Ansible Automation Platform Enterprise automation, centralized management, credential isolation

This chapter covers local hooks. For AAP hooks, see Chapter 29: AAP Integration.

Hook Execution Model

Hooks run as Kubernetes Jobs in the konveyor-forklift namespace:

  1. Job Creation: Hook container is scheduled as a Kubernetes Job
  2. Context Injection: Migration context is made available via mounted files
  3. Playbook Execution: Ansible playbook runs with access to migration data
  4. Result Handling: Job completion status determines hook success/failure

Common Use Cases

  • Pre-migration: Application quiescing, backup creation, dependency checks
  • Post-migration: Health validation, configuration updates, service restoration
  • Integration: External system notifications, monitoring updates, compliance logging
  • Troubleshooting: Debug information collection, state preservation

Hook Architecture and Context

Default Hook Image

The default hook runtime is based on Ansible Runner with Kubernetes integration:

  • Default Image: quay.io/kubev2v/hook-runner (verified from command code)
  • Based On: Ansible Runner with python-openshift and oc binary
  • Capabilities: Full Ansible automation with Kubernetes API access
  • Extensibility: Custom images supported for specialized requirements

Accessing Migration Context

Hooks receive migration context through mounted files:

plan.yml

Contains complete migration plan information:

  • Plan metadata and configuration
  • VM list and specifications
  • Mapping configurations
  • Provider references

workload.yml

Contains current VM (workload) specific data:

  • VM properties and configuration
  • IP addresses and network information
  • Disk and storage details
  • Migration-specific metadata

Hook Parameters

Hook creation supports comprehensive configuration verified from the command code:

Local hook flags:

Parameter Flag Description Default
Image --image Container image URL quay.io/kubev2v/hook-runner
Playbook --playbook Ansible playbook content or @file None
Service Account --service-account Kubernetes ServiceAccount Default
Deadline --deadline Hook timeout in seconds No timeout

AAP hook flags (mutually exclusive with --image and --playbook):

Parameter Flag Description Default
Job Template ID --aap-job-template-id AAP job template ID
AAP URL --aap-url Per-hook AAP server URL override Controller setting
AAP Token --aap-token-secret Per-hook AAP token Secret name Controller setting
AAP Timeout --aap-timeout Per-hook job poll timeout (seconds) Controller setting

See Chapter 29: AAP Integration for complete AAP hook documentation.

How-To: Creating Hooks

Basic Hook Creation

1
2
3
4
5
6
7
8
9
10
# Create hook with inline playbook
kubectl mtv create hook --name simple-notification \
  --playbook "$(cat << 'EOF'
- hosts: localhost
  tasks:
  - name: Send notification
    debug:
      msg: "Migration hook executed for {{ workload.vm.name }}"
EOF
)"

Hook Creation with File Playbook

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
# Create playbook file
cat > database-backup.yml << 'EOF'
- name: Database Backup Hook
  hosts: localhost
  tasks:
  - name: Load migration context
    include_vars:
      file: plan.yml
      name: plan
  
  - name: Load workload context  
    include_vars:
      file: workload.yml
      name: workload
  
  - name: Create database backup
    shell: |
      mysqldump -h {{ workload.vm.ipaddress }} -u backup_user \
      --all-databases > /backup/{{ workload.vm.name }}-$(date +%Y%m%d).sql
    when: workload.vm.name is match('.*database.*')
EOF

# Create hook using file
kubectl mtv create hook --name database-backup \
  --playbook @database-backup.yml \
  --service-account migration-hooks-sa \
  --deadline 1800

Advanced Hook with Custom Image

1
2
3
4
5
6
# Create hook with custom image and extended capabilities
kubectl mtv create hook --name advanced-validation \
  --image registry.company.com/migration/custom-hook:v1.2 \
  --playbook @advanced-validation.yml \
  --service-account admin-hooks-sa \
  --deadline 3600

Detailed Hook Examples

Example 1: Database Backup Hook (Pre-migration)

Create the Playbook

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# Save as db-backup-hook.yml
- name: Database Backup Pre-Migration Hook
  hosts: localhost
  tasks:
  - name: Load migration plan context
    include_vars:
      file: plan.yml
      name: plan

  - name: Load VM workload context
    include_vars:
      file: workload.yml
      name: workload

  - name: Create backup directory
    file:
      path: /backup/{{ plan.metadata.name }}
      state: directory
      mode: '0755'

  - name: Check if VM is database server
    set_fact:
      is_database: "{{ workload.vm.name | regex_search('(database|db|sql|mysql|postgres)', ignorecase=True) | bool }}"

  - name: Create database backup
    shell: |
      BACKUP_FILE="/backup/{{ plan.metadata.name }}/{{ workload.vm.name }}-backup-$(date +%Y%m%d_%H%M%S).sql"
      mysqldump -h {{ workload.vm.ipaddress }} -u {{ backup_user }} -p{{ backup_password }} \
        --single-transaction --routines --triggers --all-databases > "${BACKUP_FILE}"
      echo "Backup created: ${BACKUP_FILE}"
    environment:
      backup_user: "{{ lookup('kubernetes.core.k8s', api_version='v1', kind='Secret', namespace='migration', resource_name='db-credentials')['data']['username'] | b64decode }}"
      backup_password: "{{ lookup('kubernetes.core.k8s', api_version='v1', kind='Secret', namespace='migration', resource_name='db-credentials')['data']['password'] | b64decode }}"
    when: is_database

  - name: Store backup location in ConfigMap
    kubernetes.core.k8s:
      api_version: v1
      kind: ConfigMap
      name: "{{ plan.metadata.name }}-backups"
      namespace: migration-backups
      definition:
        data:
          "{{ workload.vm.name }}": "/backup/{{ plan.metadata.name }}/{{ workload.vm.name }}-backup-{{ ansible_date_time.epoch }}.sql"
    when: is_database

  - name: Log backup completion
    debug:
      msg: "Database backup completed for {{ workload.vm.name }}"
    when: is_database

Create the Hook

1
2
3
4
5
6
7
8
9
10
11
# Create database credentials secret
kubectl create secret generic db-credentials \
  --from-literal=username=backup_user \
  --from-literal=password=secure_backup_password \
  -n migration

# Create the hook
kubectl mtv create hook --name database-backup-pre \
  --playbook @db-backup-hook.yml \
  --service-account migration-admin \
  --deadline 1800

Example 2: Application Health Check Hook (Post-migration)

Create the Playbook

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# Save as health-check-hook.yml
- name: Post-Migration Health Check
  hosts: localhost
  tasks:
  - name: Load migration contexts
    include_vars:
      file: "{{ item }}"
      name: "{{ item | basename | regex_replace('\\.yml$', '') }}"
    loop:
      - plan.yml
      - workload.yml

  - name: Wait for VM to be ready
    kubernetes.core.k8s_info:
      api_version: kubevirt.io/v1
      kind: VirtualMachine
      name: "{{ workload.vm.name }}"
      namespace: "{{ plan.spec.targetNamespace | default('default') }}"
      wait: true
      wait_condition:
        type: Ready
        status: 'True'
      wait_timeout: 600

  - name: Check application endpoints
    uri:
      url: "http://{{ workload.vm.ipaddress }}:{{ item.port }}{{ item.path }}"
      method: GET
      status_code: 200
      timeout: 30
    register: health_checks
    loop:
      - { port: 8080, path: "/health" }
      - { port: 8080, path: "/ready" }
      - { port: 9090, path: "/metrics" }
    ignore_errors: true

  - name: Validate service responses
    assert:
      that:
        - item.status == 200
      fail_msg: "Health check failed for {{ item.url }}"
      success_msg: "Health check passed for {{ item.url }}"
    loop: "{{ health_checks.results }}"
    when: item.status is defined

  - name: Update monitoring configuration
    kubernetes.core.k8s:
      api_version: v1
      kind: ConfigMap
      name: monitoring-targets
      namespace: monitoring
      definition:
        data:
          "{{ workload.vm.name }}": |
            - targets: ['{{ workload.vm.ipaddress }}:9090']
              labels:
                instance: '{{ workload.vm.name }}'
                environment: '{{ plan.metadata.labels.environment | default("production") }}'

  - name: Send notification
    uri:
      url: "{{ notification_webhook }}"
      method: POST
      body_format: json
      body:
        text: "Migration completed for {{ workload.vm.name }} - Health checks passed"
        vm: "{{ workload.vm.name }}"
        plan: "{{ plan.metadata.name }}"
        status: "healthy"
    vars:
      notification_webhook: "{{ lookup('kubernetes.core.k8s', api_version='v1', kind='Secret', namespace='migration', resource_name='notification-config')['data']['webhook_url'] | b64decode }}"

Create the Hook

1
2
3
4
5
6
7
8
9
10
# Create notification webhook secret
kubectl create secret generic notification-config \
  --from-literal=webhook_url=https://hooks.slack.com/services/YOUR/WEBHOOK/URL \
  -n migration

# Create the hook
kubectl mtv create hook --name health-check-post \
  --playbook @health-check-hook.yml \
  --service-account migration-monitor \
  --deadline 900

Example 3: Shell Script Hook Integration

Create Shell Script Hook

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# Save as shell-script-hook.yml
- name: Shell Script Integration Hook
  hosts: localhost
  tasks:
  - name: Load migration context
    include_vars:
      file: workload.yml
      name: workload

  - name: Execute custom shell script
    script: |
      #!/bin/bash
      
      VM_NAME="{{ workload.vm.name }}"
      VM_IP="{{ workload.vm.ipaddress }}"
      PLAN_NAME="{{ plan.metadata.name | default('unknown') }}"
      
      echo "Processing VM: $VM_NAME (IP: $VM_IP) in plan: $PLAN_NAME"
      
      # Custom application-specific logic
      if [[ "$VM_NAME" == *"web"* ]]; then
        echo "Configuring web server post-migration..."
        # Update load balancer configuration
        curl -X POST "http://load-balancer.internal/api/servers" \
          -H "Content-Type: application/json" \
          -d "{\"name\":\"$VM_NAME\",\"ip\":\"$VM_IP\",\"status\":\"active\"}"
      fi
      
      if [[ "$VM_NAME" == *"database"* ]]; then
        echo "Validating database connectivity..."
        # Test database connection
        timeout 30 bash -c "until nc -z $VM_IP 3306; do sleep 1; done"
        echo "Database is responding on $VM_IP:3306"
      fi
      
      # Update external monitoring
      curl -X POST "http://monitoring.internal/api/targets" \
        -H "Authorization: Bearer $MONITORING_TOKEN" \
        -d "host=$VM_IP&name=$VM_NAME&plan=$PLAN_NAME"
    environment:
      MONITORING_TOKEN: "{{ lookup('kubernetes.core.k8s', api_version='v1', kind='Secret', namespace='migration', resource_name='monitoring-token')['data']['token'] | b64decode }}"
    register: script_result

  - name: Log script execution results
    debug:
      var: script_result.stdout_lines

Create and Use the Hook

1
2
3
4
5
6
7
8
9
10
# Create monitoring token secret
kubectl create secret generic monitoring-token \
  --from-literal=token=your-monitoring-api-token \
  -n migration

# Create the hook
kubectl mtv create hook --name shell-integration \
  --playbook @shell-script-hook.yml \
  --service-account migration-integration \
  --deadline 600

Adding Hooks via Plan Creation Flags

Plan-Level Hook Integration

Plan creation supports adding hooks to all VMs in the plan:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Add pre-hook to all VMs in the plan
kubectl mtv create plan --name hooked-migration \
  --source vsphere-prod \
  --pre-hook database-backup-pre \
  --vms "database-01,database-02,app-server-01"

# Add both pre and post hooks
kubectl mtv create plan --name comprehensive-hooks \
  --source vsphere-prod \
  --pre-hook preparation-hook \
  --post-hook validation-hook \
  --vms "where name ~= '.*prod.*'"

# Combined with other migration settings
kubectl mtv create plan --name production-with-hooks \
  --source vsphere-prod \
  --target-namespace production \
  --migration-type warm \
  --network-mapping prod-network-map \
  --storage-mapping prod-storage-map \
  --pre-hook backup-and-quiesce \
  --post-hook health-and-notify \
  --vms @production-vms.yaml

Hook Execution Order

When multiple hooks are configured:

  1. Pre-hooks execute: Before VM conversion begins
  2. Migration proceeds: VM conversion and data transfer
  3. Post-hooks execute: After VM migration completes

Managing Hooks via PlanVM Configuration

Per-VM Hook Configuration

Individual VMs can have specific hooks using the PlanVMS format:

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
28
29
30
31
32
# vm-specific-hooks.yaml
- name: database-primary
  targetName: db-prod-01
  hooks:
  - step: PreHook
    hook:
      name: database-backup-pre
      namespace: migration-hooks
  - step: PostHook
    hook:
      name: database-health-check
      namespace: migration-hooks

- name: web-server-01
  targetName: web-prod-01
  hooks:
  - step: PreHook
    hook:
      name: web-drain-connections
      namespace: migration-hooks
  - step: PostHook
    hook:
      name: web-health-check
      namespace: migration-hooks

- name: cache-server-01
  targetName: cache-prod-01
  hooks:
  - step: PostHook
    hook:
      name: cache-warmup
      namespace: migration-hooks

Using VM-Specific Hooks

1
2
3
4
5
6
# Create plan with VM-specific hooks
kubectl mtv create plan --name vm-specific-hooks \
  --source vsphere-prod \
  --vms @vm-specific-hooks.yaml \
  --network-mapping prod-network-map \
  --storage-mapping prod-storage-map

Hook Management via Plan Patching

Hooks can be added or modified after plan creation using the patch planvm command:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Add a pre-migration hook to a specific VM
kubectl mtv patch planvm --plan-name existing-plan \
  --vm-name additional-vm \
  --add-pre-hook new-preparation-hook

# Add a post-migration hook to a specific VM
kubectl mtv patch planvm --plan-name existing-plan \
  --vm-name additional-vm \
  --add-post-hook health-check-post

# Remove a specific hook from a VM
kubectl mtv patch planvm --plan-name existing-plan \
  --vm-name additional-vm \
  --remove-hook new-preparation-hook

# Clear all hooks from a VM
kubectl mtv patch planvm --plan-name existing-plan \
  --vm-name additional-vm \
  --clear-hooks

Advanced Hook Development

Custom Hook Images

Building Custom Hook Image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Custom hook image with additional tools
FROM quay.io/kubev2v/hook-runner:latest

# Install additional packages
USER root
RUN dnf install -y postgresql mysql jq curl wget && \
    dnf clean all

# Install custom Python packages
RUN pip3 install psycopg2-binary pymongo redis

# Add custom scripts
COPY scripts/ /usr/local/bin/
RUN chmod +x /usr/local/bin/*

# Return to ansible user
USER ansible

Using Custom Image

1
2
3
4
5
6
7
8
9
# Build and push custom image
docker build -t registry.company.com/migration/custom-hook:v1.0 .
docker push registry.company.com/migration/custom-hook:v1.0

# Create hook with custom image
kubectl mtv create hook --name custom-database-hook \
  --image registry.company.com/migration/custom-hook:v1.0 \
  --playbook @database-migration-hook.yml \
  --service-account database-migration-sa

Hook Development Best Practices

Error Handling and Logging

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
28
29
30
31
32
33
34
35
# Error handling in hook playbooks
- name: Robust Hook with Error Handling
  hosts: localhost
  tasks:
  - name: Set failure flag
    set_fact:
      hook_failed: false

  - name: Critical operation with error handling
    block:
      - name: Perform critical task
        # Task that might fail
        shell: risky_command_here
        register: result
        
    rescue:
      - name: Handle failure
        set_fact:
          hook_failed: true
          
      - name: Log failure details
        debug:
          msg: "Hook failed: {{ ansible_failed_result.msg }}"
          
      - name: Send failure notification
        # Notification logic here
        
    always:
      - name: Cleanup operations
        # Cleanup logic here

  - name: Fail hook if critical operations failed
    fail:
      msg: "Hook execution failed"
    when: hook_failed

Secret and ConfigMap Access

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Secure credential access in hooks
- name: Secure Credential Management
  hosts: localhost
  tasks:
  - name: Load database credentials
    kubernetes.core.k8s_info:
      api_version: v1
      kind: Secret
      name: database-credentials
      namespace: migration-secrets
    register: db_creds

  - name: Use credentials securely
    # Use credentials from db_creds.resources[0].data
    no_log: true  # Don't log sensitive operations

Timeout and Deadline Management

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Timeout management in hook operations
- name: Hook with Timeout Management
  hosts: localhost
  tasks:
  - name: Operation with timeout
    async: 600  # 10 minute timeout
    poll: 10    # Check every 10 seconds
    # Long running operation here
    
  - name: Wait for background task
    async_status:
      jid: "{{ operation_result.ansible_job_id }}"
    register: job_result
    until: job_result.finished
    retries: 60
    delay: 10

Hook Integration Scenarios

Scenario 1: Enterprise Database Migration

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
28
29
30
31
32
33
34
35
36
37
38
39
# Complete database migration hook workflow
- name: Enterprise Database Migration Hooks
  hosts: localhost
  vars:
    notification_webhook: "{{ lookup('env', 'NOTIFICATION_WEBHOOK') }}"
    
  tasks:
  # Pre-migration tasks
  - name: Database pre-migration validation
    block:
      - name: Check database connectivity
        # Validate source database accessibility
        
      - name: Create backup
        # Full database backup
        
      - name: Quiesce applications
        # Stop application connections
        
      - name: Validate backup integrity
        # Verify backup completed successfully

  # Post-migration tasks  
  - name: Database post-migration validation
    block:
      - name: Validate target database startup
        # Ensure database started correctly
        
      - name: Run data integrity checks
        # Verify data migration completeness
        
      - name: Update DNS records
        # Point applications to new database
        
      - name: Resume application connections
        # Allow applications to reconnect
        
      - name: Send completion notification
        # Notify operations team

Scenario 2: Multi-Tier Application Migration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Coordinated multi-tier application migration
- name: Multi-Tier Application Migration
  hosts: localhost
  tasks:
  - name: Load balancer update
    when: workload.vm.name is match('.*web.*')
    # Remove from load balancer (pre) / Add to load balancer (post)
    
  - name: Session store migration  
    when: workload.vm.name is match('.*cache.*')
    # Handle session data migration
    
  - name: Database coordination
    when: workload.vm.name is match('.*database.*')
    # Database-specific migration logic
    
  - name: Service discovery update
    # Update service registry entries

Scenario 3: Compliance and Auditing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Compliance and audit trail hooks
- name: Compliance Migration Hook
  hosts: localhost
  tasks:
  - name: Log migration start
    # Create audit trail entry
    
  - name: Validate security compliance
    # Check security configurations
    
  - name: Document configuration changes
    # Record all migration changes
    
  - name: Generate compliance report
    # Create detailed compliance documentation
    
  - name: Store audit evidence
    # Archive all compliance artifacts

Hook Monitoring and Troubleshooting

Hook Execution Monitoring

1
2
3
4
5
6
7
8
# Monitor hook job execution
kubectl get jobs -n konveyor-forklift | grep hook

# Check hook pod logs
kubectl logs -n konveyor-forklift job/hook-job-name

# Monitor hook completion
kubectl get jobs -n konveyor-forklift -w

Hook Debugging

1
2
3
4
5
6
7
8
9
10
11
12
# Describe failed hook job
kubectl describe job hook-job-name -n konveyor-forklift

# Get hook pod details
kubectl get pods -n konveyor-forklift -l job-name=hook-job-name

# Access hook execution environment
kubectl exec -it hook-pod-name -n konveyor-forklift -- /bin/bash

# Check hook context files
kubectl exec hook-pod-name -n konveyor-forklift -- cat /tmp/hook/plan.yml
kubectl exec hook-pod-name -n konveyor-forklift -- cat /tmp/hook/workload.yml

Common Hook Issues

Timeout Issues

1
2
3
4
5
# Check hook deadline configuration
kubectl describe hook hook-name

# Monitor long-running hooks
kubectl logs -f job/hook-job-name -n konveyor-forklift

Permission Issues

1
2
3
4
5
# Verify ServiceAccount permissions
kubectl describe serviceaccount hook-service-account -n konveyor-forklift

# Check RBAC configurations
kubectl auth can-i create configmaps --as=system:serviceaccount:konveyor-forklift:hook-service-account

Playbook Errors

1
2
3
4
5
# Validate playbook syntax
ansible-playbook --syntax-check playbook.yml

# Test playbook locally (with mock context)
ansible-playbook -i localhost, playbook.yml

Hook Security and Best Practices

Security Considerations

  1. Least Privilege: Use minimal required ServiceAccount permissions
  2. Secret Management: Store sensitive data in Kubernetes Secrets
  3. Network Isolation: Limit hook network access when possible
  4. Image Security: Use trusted, regularly updated hook images

Operational Best Practices

  1. Testing: Thoroughly test hooks in non-production environments
  2. Idempotency: Design hooks to be safely re-runnable
  3. Monitoring: Implement comprehensive hook execution monitoring
  4. Documentation: Maintain clear hook documentation and runbooks

Performance Optimization

  1. Efficient Playbooks: Minimize unnecessary operations in playbooks
  2. Parallel Execution: Use Ansible parallel features where appropriate
  3. Resource Limits: Set appropriate resource limits on hook jobs
  4. Cleanup: Implement proper cleanup for temporary resources

Next Steps

After mastering migration hooks:

  1. Advanced Plan Management: Learn dynamic plan modification in Chapter 18: Advanced Plan Patching
  2. Execute Migrations: Manage complete migration lifecycle in Chapter 19: Plan Lifecycle Execution
  3. Troubleshooting: Master debugging techniques in Chapter 20: Debugging and Troubleshooting
  4. Best Practices: Learn operational excellence in Chapter 21: Best Practices and Security

Previous: Chapter 16: Migration Process Optimization
Next: Chapter 18: Advanced Plan Patching