diff --git a/ansible/playbooks/deploy-aitutor-vm.yml b/ansible/playbooks/deploy-aitutor-vm.yml new file mode 100644 index 0000000..6b9abf0 --- /dev/null +++ b/ansible/playbooks/deploy-aitutor-vm.yml @@ -0,0 +1,16 @@ +--- +- name: Provision VM on Proxmox for AI Tutor + hosts: localhost + gather_facts: false + connection: local + + roles: + - role: proxmox_vm_deploy + +- name: Install AI Tutor on provisioned VM + hosts: aitutor_vm + gather_facts: true + become: true + + roles: + - role: aitutor_install diff --git a/ansible/roles/aitutor_install/defaults/main.yml b/ansible/roles/aitutor_install/defaults/main.yml new file mode 100644 index 0000000..3186696 --- /dev/null +++ b/ansible/roles/aitutor_install/defaults/main.yml @@ -0,0 +1,7 @@ +--- +aitutor_npm_package: "@aitutor/cli" +aitutor_npm_version: "latest" +aitutor_extra_packages: + - nodejs + - npm + - ca-certificates diff --git a/ansible/roles/aitutor_install/tasks/main.yml b/ansible/roles/aitutor_install/tasks/main.yml new file mode 100644 index 0000000..7474a46 --- /dev/null +++ b/ansible/roles/aitutor_install/tasks/main.yml @@ -0,0 +1,28 @@ +--- +- name: Ensure supported OS family + ansible.builtin.assert: + that: + - ansible_os_family == 'Debian' + fail_msg: "This role currently supports Debian-family distributions only." + +- name: Install runtime packages for AI Tutor + ansible.builtin.apt: + name: "{{ aitutor_extra_packages }}" + state: present + update_cache: true + +- name: Install or update AI Tutor CLI globally via npm + community.general.npm: + name: "{{ aitutor_npm_package }}" + version: "{{ aitutor_npm_version }}" + global: true + state: present + +- name: Verify aitutor command is available + ansible.builtin.command: which aitutor + register: aitutor_bin + changed_when: false + +- name: Show installed aitutor path + ansible.builtin.debug: + msg: "AITutor installed at {{ aitutor_bin.stdout }}" diff --git a/ansible/roles/proxmox_vm_deploy/defaults/main.yml b/ansible/roles/proxmox_vm_deploy/defaults/main.yml new file mode 100644 index 0000000..189944a --- /dev/null +++ b/ansible/roles/proxmox_vm_deploy/defaults/main.yml @@ -0,0 +1,32 @@ +--- +# Proxmox API endpoint and auth +proxmox_api_host: "10.0.0.201" +proxmox_api_user: "ansible@pve" +proxmox_api_token_id: "ansible" +proxmox_api_token_secret: "SET_IN_VAULT" +proxmox_validate_certs: false + +# VM placement +proxmox_node: "pve01" +proxmox_vmid: 9210 +proxmox_vm_name: "aitutor" +proxmox_template: "ubuntu-2404-cloudinit-template" +proxmox_storage: "local-lvm" + +# VM sizing +proxmox_cores: 2 +proxmox_memory_mb: 4096 +proxmox_disk_gb: 32 + +# Network and cloud-init +proxmox_bridge: "vmbr0" +vm_ipconfig0: "ip=10.0.0.210/24,gw=10.0.0.2" +vm_nameserver: "10.0.0.2" +vm_searchdomain: "lan" +vm_ci_user: "chester" +vm_ci_password: "SET_IN_VAULT" +vm_ssh_public_key: "" +vm_ssh_private_key_file: "~/.ssh/id_ed25519" + +# Timing +vm_boot_timeout_seconds: 300 diff --git a/ansible/roles/proxmox_vm_deploy/tasks/main.yml b/ansible/roles/proxmox_vm_deploy/tasks/main.yml new file mode 100644 index 0000000..acc835c --- /dev/null +++ b/ansible/roles/proxmox_vm_deploy/tasks/main.yml @@ -0,0 +1,114 @@ +--- +- name: Validate required Proxmox variables + ansible.builtin.assert: + that: + - proxmox_api_host | length > 0 + - proxmox_api_user | length > 0 + - proxmox_api_token_id | length > 0 + - proxmox_api_token_secret | length > 0 + - proxmox_node | length > 0 + - proxmox_template | length > 0 + - proxmox_vmid | int > 99 + - vm_ci_user | length > 0 + - vm_ipconfig0 is match('^ip=.+') + fail_msg: "Missing required VM provisioning variables." + +- name: Gather current VMs on Proxmox node + community.proxmox.proxmox_vm_info: + api_host: "{{ proxmox_api_host }}" + api_user: "{{ proxmox_api_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token_secret }}" + validate_certs: "{{ proxmox_validate_certs }}" + node: "{{ proxmox_node }}" + register: proxmox_vms + +- name: Detect whether target VM already exists + ansible.builtin.set_fact: + vm_exists: >- + {{ + (proxmox_vms.proxmox_vms | default([]) + | selectattr('vmid', 'equalto', proxmox_vmid | int) + | list + | length) > 0 + }} + +- name: Clone VM from cloud-init template when missing + community.proxmox.proxmox_kvm: + api_host: "{{ proxmox_api_host }}" + api_user: "{{ proxmox_api_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token_secret }}" + validate_certs: "{{ proxmox_validate_certs }}" + node: "{{ proxmox_node }}" + clone: "{{ proxmox_template }}" + newid: "{{ proxmox_vmid }}" + name: "{{ proxmox_vm_name }}" + storage: "{{ proxmox_storage }}" + full: true + timeout: 600 + state: present + when: not vm_exists + +- name: Apply VM hardware, network, and cloud-init settings + community.proxmox.proxmox_kvm: + api_host: "{{ proxmox_api_host }}" + api_user: "{{ proxmox_api_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token_secret }}" + validate_certs: "{{ proxmox_validate_certs }}" + node: "{{ proxmox_node }}" + vmid: "{{ proxmox_vmid }}" + name: "{{ proxmox_vm_name }}" + cores: "{{ proxmox_cores }}" + memory: "{{ proxmox_memory_mb }}" + scsihw: virtio-scsi-pci + scsi: + scsi0: "{{ proxmox_storage }}:{{ proxmox_disk_gb }}" + net: + net0: "virtio,bridge={{ proxmox_bridge }}" + ciuser: "{{ vm_ci_user }}" + cipassword: "{{ vm_ci_password }}" + ipconfig: + ipconfig0: "{{ vm_ipconfig0 }}" + nameservers: + - "{{ vm_nameserver }}" + searchdomains: + - "{{ vm_searchdomain }}" + sshkeys: "{{ vm_ssh_public_key | default(omit) }}" + onboot: true + agent: true + update: true + state: present + +- name: Ensure VM is running + community.proxmox.proxmox_kvm: + api_host: "{{ proxmox_api_host }}" + api_user: "{{ proxmox_api_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token_secret }}" + validate_certs: "{{ proxmox_validate_certs }}" + node: "{{ proxmox_node }}" + vmid: "{{ proxmox_vmid }}" + state: started + +- name: Derive VM IPv4 address from cloud-init ipconfig + ansible.builtin.set_fact: + vm_ipv4: "{{ (vm_ipconfig0.split('ip=')[1].split(',')[0]).split('/')[0] }}" + +- name: Wait for SSH on provisioned VM + ansible.builtin.wait_for: + host: "{{ vm_ipv4 }}" + port: 22 + timeout: "{{ vm_boot_timeout_seconds }}" + delay: 5 + delegate_to: localhost + +- name: Add new VM to in-memory inventory + ansible.builtin.add_host: + name: "{{ proxmox_vm_name }}" + groups: aitutor_vm + ansible_host: "{{ vm_ipv4 }}" + ansible_user: "{{ vm_ci_user }}" + ansible_ssh_private_key_file: "{{ vm_ssh_private_key_file }}" + ansible_python_interpreter: /usr/bin/python3