diff --git a/configuration/ansible_mssql/ansible.cfg b/configuration/ansible_mssql/ansible.cfg new file mode 100644 index 0000000..2a4e2e5 --- /dev/null +++ b/configuration/ansible_mssql/ansible.cfg @@ -0,0 +1,20 @@ +[defaults] + +inventory = inventories/production/hosts.yml + +roles_path = roles + +host_key_checking = False + +remote_user = Administrator + +timeout = 30 +interpreter_python = auto_silent +forks = 20 +pipelining = True +bin_ansible_callbacks = True +retry_files_enabled = False + +[ssh_connection] +ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no +transfer_method = scp diff --git a/configuration/ansible_mssql/inventories/production/group_vars/all/all.yml b/configuration/ansible_mssql/inventories/production/group_vars/all/all.yml new file mode 100644 index 0000000..e69de29 diff --git a/configuration/ansible_mssql/inventories/production/group_vars/all/vault.yml b/configuration/ansible_mssql/inventories/production/group_vars/all/vault.yml new file mode 100644 index 0000000..a6998e0 --- /dev/null +++ b/configuration/ansible_mssql/inventories/production/group_vars/all/vault.yml @@ -0,0 +1,18 @@ +$ANSIBLE_VAULT;1.1;AES256 +34613730306335623333623230346639323439343264366138623465656138346535663236336635 +3933323931303236313761656337383735373332393366620a396664653239623437353263306563 +64643237666633343165633230363065306561376166613735396562386464623532393730643765 +3135326437616236640a363463383363626539383465663731313833306138633061386666313535 +37636564373437313933636234636339613330613935336666343433376630346664336336383538 +62623761323539363536313031623135333831643638336234643264343632393863363539653461 +32393333646131316337653636386563396562613961383335323431623835653434333839613635 +63353433656630366166336239643766653938363033373137623532353237353261383261623437 +31366237353462623731343766653832343936363738373031633231373038376435613662616234 +34373861656361353863326432616436316537643065313564306131616636383939343438333838 +37326532663639666332313239316635313637333262633636376237653036343465646638303263 +31333866633966346534303564623164373637303337646632326135666535663566333833356537 +30323061613231393332346262393261343731363462623536313964313466666238363532613762 +65666163376231313065393861643635626136316434643761386137306530346533653366643830 +63363738366134323665323232313439323865323235313331316164393431373362646630336261 +39313732376531336230663032363437343862646331613831623063336164373465343136643037 +66643130636538383339393435616563323930386262343134636134373434326561 diff --git a/configuration/ansible_mssql/inventories/production/group_vars/all/vault.yml.example b/configuration/ansible_mssql/inventories/production/group_vars/all/vault.yml.example new file mode 100644 index 0000000..8184ddb --- /dev/null +++ b/configuration/ansible_mssql/inventories/production/group_vars/all/vault.yml.example @@ -0,0 +1,9 @@ +--- +mssql_server_password: "" + +domain_name: "" +domain_join_user: "" +domain_join_password: "" +domain_ou_path: "" + +mssql_sa_password: "" diff --git a/configuration/ansible_mssql/inventories/production/hosts.yml b/configuration/ansible_mssql/inventories/production/hosts.yml new file mode 100644 index 0000000..f937527 --- /dev/null +++ b/configuration/ansible_mssql/inventories/production/hosts.yml @@ -0,0 +1,10 @@ +all: + children: + mssql_servers: + hosts: + SMSSQLDW1P.ad.cwx.hr: + ansible_host: 10.10.2.100 + ansible_connection: ssh + ansible_shell_type: powershell + ansible_user: Administrator + ansible_password: "{{ mssql_server_password }}" diff --git a/configuration/ansible_mssql/playbooks/install-mssql.yml b/configuration/ansible_mssql/playbooks/install-mssql.yml new file mode 100644 index 0000000..69e1389 --- /dev/null +++ b/configuration/ansible_mssql/playbooks/install-mssql.yml @@ -0,0 +1,8 @@ +--- +- name: Configure MSSQL servers + hosts: mssql_servers + roles: + - common + - mssql_prepare + - mssql_install + - db_restore diff --git a/configuration/ansible_mssql/roles/common/defaults/main.yml b/configuration/ansible_mssql/roles/common/defaults/main.yml new file mode 100644 index 0000000..3f54f48 --- /dev/null +++ b/configuration/ansible_mssql/roles/common/defaults/main.yml @@ -0,0 +1,12 @@ +--- +domain_name: "example.local" +domain_join_user: "{{ domain_join_user }}" +domain_join_password: "{{ domain_join_password }}" +domain_ou_path: "" + +data_disk_number: 1 +data_disk_size_gb: 80 + +sql_data_part_gb: 35 +sql_log_part_gb: 15 +sql_tempdb_part_gb: 10 diff --git a/configuration/ansible_mssql/roles/common/tasks/main.yml b/configuration/ansible_mssql/roles/common/tasks/main.yml new file mode 100644 index 0000000..62f6588 --- /dev/null +++ b/configuration/ansible_mssql/roles/common/tasks/main.yml @@ -0,0 +1,123 @@ +--- +- name: Wait for system to be fully booted + ansible.builtin.wait_for_connection: + timeout: 300 + sleep: 10 + +- name: Ensure Windows Update service is running + ansible.windows.win_service: + name: wuauserv + state: started + start_mode: auto + +- name: Ensure BITS service is running + ansible.windows.win_service: + name: BITS + state: started + start_mode: auto + +- name: Install Windows updates (loop until no more pending) + ansible.windows.win_updates: + category_names: + - SecurityUpdates + - CriticalUpdates + - UpdateRollups + - Updates + state: installed + reboot: true + reboot_timeout: 3600 + server_selection: windows_update + register: win_updates_result + until: win_updates_result.installed_update_count == 0 + retries: 5 + delay: 30 + +- name: Report Windows Update result + ansible.builtin.debug: + msg: >- + Windows Update complete. + Last pass installed {{ win_updates_result.installed_update_count }} update(s). + Reboot required: {{ win_updates_result.reboot_required }}. + +- name: Join Active Directory domain + microsoft.ad.membership: + dns_domain_name: "{{ domain_name }}" + hostname: "{{ inventory_hostname_short }}" + domain_admin_user: "{{ domain_join_user }}" + domain_admin_password: "{{ domain_join_password }}" + domain_ou_path: "{{ domain_ou_path | default(omit) }}" + state: domain + reboot: true + +- name: Initialize data disk as GPT + community.windows.win_initialize_disk: + disk_number: "{{ data_disk_number }}" + style: gpt + online: true + +- name: Create SQL Data partition (F:) + community.windows.win_partition: + disk_number: "{{ data_disk_number }}" + partition_size: "{{ sql_data_part_gb }} GiB" + drive_letter: F + state: present + +- name: Format SQL Data partition (F:) + community.windows.win_format: + drive_letter: F + file_system: NTFS + new_label: SQLData + allocation_unit_size: 65536 + +- name: Create SQL Log partition (G:) + community.windows.win_partition: + disk_number: "{{ data_disk_number }}" + partition_size: "{{ sql_log_part_gb }} GiB" + drive_letter: G + state: present + +- name: Format SQL Log partition (G:) + community.windows.win_format: + drive_letter: G + file_system: NTFS + new_label: SQLLog + allocation_unit_size: 65536 + +- name: Create SQL TempDB partition (H:) + community.windows.win_partition: + disk_number: "{{ data_disk_number }}" + partition_size: "{{ sql_tempdb_part_gb }} GiB" + drive_letter: H + state: present + +- name: Format SQL TempDB partition (H:) + community.windows.win_format: + drive_letter: H + file_system: NTFS + new_label: SQLTempDB + allocation_unit_size: 65536 + +- name: Create SQL Backup partition (I:) + community.windows.win_partition: + disk_number: "{{ data_disk_number }}" + partition_size: "{{ (data_disk_size_gb | int) - (sql_data_part_gb | int) - (sql_log_part_gb | int) - (sql_tempdb_part_gb | int) - 1 }} GiB" + drive_letter: I + state: present + +- name: Format SQL Backup partition (I:) + community.windows.win_format: + drive_letter: I + file_system: NTFS + new_label: SQLBackup + allocation_unit_size: 65536 + +- name: Create SQL Server directories + ansible.windows.win_file: + path: "{{ item }}" + state: directory + loop: + - 'F:\Data' + - 'G:\Log' + - 'H:\TempDB\Data' + - 'H:\TempDB\Log' + - 'I:\Backup' diff --git a/configuration/ansible_mssql/roles/db_restore/defaults/main.yml b/configuration/ansible_mssql/roles/db_restore/defaults/main.yml new file mode 100644 index 0000000..a873352 --- /dev/null +++ b/configuration/ansible_mssql/roles/db_restore/defaults/main.yml @@ -0,0 +1,13 @@ +--- +db_backup_dir: 'I:\Backup' +db_data_dir: 'F:\Data' +db_log_dir: 'G:\Log' + +db_backups: + - name: AdventureWorksDW2022 + filename: AdventureWorksDW2022.bak + url: "https://github.com/Microsoft/sql-server-samples/releases/download/adventureworks/AdventureWorksDW2022.bak" + + - name: WideWorldImportersDW + filename: WideWorldImportersDW-Full.bak + url: "https://github.com/Microsoft/sql-server-samples/releases/download/wide-world-importers-v1.0/WideWorldImportersDW-Full.bak" diff --git a/configuration/ansible_mssql/roles/db_restore/tasks/main.yml b/configuration/ansible_mssql/roles/db_restore/tasks/main.yml new file mode 100644 index 0000000..aab706a --- /dev/null +++ b/configuration/ansible_mssql/roles/db_restore/tasks/main.yml @@ -0,0 +1,67 @@ +--- +- name: Find sqlcmd.exe + ansible.windows.win_powershell: + script: | + $sqlcmd = Get-ChildItem 'C:\Program Files\Microsoft SQL Server' -Filter 'sqlcmd.exe' ` + -Recurse -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -notlike '*\x86\*' } | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 -ExpandProperty FullName + if (-not $sqlcmd) { throw 'sqlcmd.exe not found' } + $Ansible.Result = $sqlcmd + $Ansible.Changed = $false + register: sqlcmd_path + +- name: Download database backups + ansible.windows.win_get_url: + url: "{{ item.url }}" + dest: "{{ db_backup_dir }}\\{{ item.filename }}" + loop: "{{ db_backups }}" + +- name: Restore databases + ansible.windows.win_powershell: + script: | + $ErrorActionPreference = 'Stop' + + $sqlcmd = '{{ sqlcmd_path.result }}' + $bakFile = '{{ db_backup_dir }}\{{ item.filename }}' + $dbName = '{{ item.name }}' + + # Idempotent: skip if database already exists + $exists = (& $sqlcmd -S . -E -h -1 ` + -Q "SET NOCOUNT ON; SELECT name FROM sys.databases WHERE name = N'$dbName'" | + Out-String).Trim() + if ($exists -eq $dbName) { + Write-Output "$dbName already exists — skipping restore." + $Ansible.Changed = $false + return + } + + # Get logical file list from the backup + $raw = & $sqlcmd -S . -E -s "|" -W ` + -Q "RESTORE FILELISTONLY FROM DISK = N'$bakFile'" + + $files = $raw | Select-Object -Skip 2 | Where-Object { $_ -match '\|' } | ForEach-Object { + $cols = $_ -split '\|' + [PSCustomObject]@{ + LogicalName = $cols[0].Trim() + PhysicalName = $cols[1].Trim() + FileType = $cols[2].Trim() # D = data, L = log + } + } + + # Build MOVE clauses — keep original filename, redirect to correct partition + $moves = $files | ForEach-Object { + $dir = if ($_.FileType -eq 'L') { '{{ db_log_dir }}' } else { '{{ db_data_dir }}' } + $file = [System.IO.Path]::GetFileName($_.PhysicalName) + "MOVE N'$($_.LogicalName)' TO N'$dir\$file'" + } + + $sql = "RESTORE DATABASE [$dbName] FROM DISK = N'$bakFile' WITH $($moves -join ', '), REPLACE, STATS = 10" + Write-Output "Restoring $dbName ..." + & $sqlcmd -S . -E -Q $sql -t 3600 + if ($LASTEXITCODE -ne 0) { throw "RESTORE failed with exit code $LASTEXITCODE" } + loop: "{{ db_backups }}" + loop_control: + label: "{{ item.name }}" + timeout: 7200 diff --git a/configuration/ansible_mssql/roles/mssql_install/defaults/main.yml b/configuration/ansible_mssql/roles/mssql_install/defaults/main.yml new file mode 100644 index 0000000..71f19ab --- /dev/null +++ b/configuration/ansible_mssql/roles/mssql_install/defaults/main.yml @@ -0,0 +1,32 @@ +--- +mssql_instance_name: MSSQLSERVER +mssql_features: SQLENGINE,AS,IS,CONN +mssql_collation: SQL_Latin1_General_CP1_CI_AS +mssql_iso_drive: "E:\\" + +mssql_security_mode: SQL +mssql_sa_password: "" +mssql_sysadmin_accounts: + - "CWX\\srvadmin-dandric" + - "{{ inventory_hostname_short }}\\Administrator" + +mssql_sql_svc_account: "NT AUTHORITY\\NETWORK SERVICE" +mssql_sql_svc_password: "" +mssql_agt_svc_account: "NT AUTHORITY\\NETWORK SERVICE" +mssql_agt_svc_password: "" +mssql_as_svc_account: "NT AUTHORITY\\NETWORK SERVICE" +mssql_is_svc_account: "NT AUTHORITY\\NETWORK SERVICE" + +mssql_as_server_mode: "TABULAR" + +mssql_data_dir: 'F:\Data' +mssql_log_dir: 'G:\Log' +mssql_backup_dir: 'I:\Backup' +mssql_tempdb_dir: 'H:\TempDB\Data' +mssql_tempdb_log_dir: 'H:\TempDB\Log' + +mssql_max_memory_mb: 12288 +mssql_min_memory_mb: 4096 + +mssql_tcp_enabled: 1 +mssql_tcp_port: 1433 diff --git a/configuration/ansible_mssql/roles/mssql_install/tasks/main.yml b/configuration/ansible_mssql/roles/mssql_install/tasks/main.yml new file mode 100644 index 0000000..21c6ae1 --- /dev/null +++ b/configuration/ansible_mssql/roles/mssql_install/tasks/main.yml @@ -0,0 +1,132 @@ +--- +- name: Create SQL Server data directories + ansible.windows.win_file: + path: "{{ item }}" + state: directory + loop: + - "{{ mssql_data_dir }}" + - "{{ mssql_log_dir }}" + - "{{ mssql_backup_dir }}" + - "{{ mssql_tempdb_dir }}" + - "{{ mssql_tempdb_log_dir }}" + +- name: Install SQL Server 2022 + block: + - name: Run SQL Server 2022 setup + ansible.windows.win_powershell: + script: | + $ErrorActionPreference = 'Stop' + + $sapwd = '{{ mssql_sa_password | replace("'", "''") }}' + + $proc = Start-Process ` + -FilePath '{{ mssql_iso_drive }}\setup.exe' ` + -ArgumentList @( + '/Action=Install', + '/QUIET', + '/IACCEPTSQLSERVERLICENSETERMS', + '/FEATURES={{ mssql_features }}', + '/INSTANCENAME={{ mssql_instance_name }}', + '/SQLCOLLATION={{ mssql_collation }}', + '/SECURITYMODE={{ mssql_security_mode }}', + "/SAPWD=$sapwd", + '/SQLSYSADMINACCOUNTS="{{ mssql_sysadmin_accounts | join('" "') }}"', + '/SQLSVCACCOUNT="{{ mssql_sql_svc_account }}"', + '/AGTSVCACCOUNT="{{ mssql_agt_svc_account }}"', + '/AGTSVCSTARTUPTYPE=Automatic', + '/BROWSERSVCSTARTUPTYPE=Automatic', + '/TCPENABLED={{ mssql_tcp_enabled }}', + '/SQLUSERDBDIR="{{ mssql_data_dir }}"', + '/SQLUSERDBLOGDIR="{{ mssql_log_dir }}"', + '/SQLBACKUPDIR="{{ mssql_backup_dir }}"', + '/SQLTEMPDBDIR="{{ mssql_tempdb_dir }}"', + '/SQLTEMPDBLOGDIR="{{ mssql_tempdb_log_dir }}"', + '/ASSVCACCOUNT="{{ mssql_as_svc_account }}"', + '/ASSERVERMODE={{ mssql_as_server_mode }}', + '/ASSYSADMINACCOUNTS="{{ mssql_sysadmin_accounts | join('" "') }}"', + '/ISSVCACCOUNT="{{ mssql_is_svc_account }}"', + '/UPDATEENABLED=False' + ) ` + -Wait -PassThru + + if ($proc.ExitCode -notin @(0, 3010)) { + throw "SQL Server setup failed with exit code: $($proc.ExitCode)" + } + + $Ansible.Result = $proc.ExitCode + register: mssql_install_result + timeout: 3600 + + - name: Report install exit code + ansible.builtin.debug: + msg: "SQL Server installed (exit code {{ mssql_install_result.result }}{% if mssql_install_result.result == 3010 %} — reboot required{% endif %})" + + rescue: + - name: Display setup summary log + ansible.windows.win_powershell: + script: | + $log = Get-ChildItem 'C:\Program Files\Microsoft SQL Server' -Filter 'Summary*.txt' ` + -Recurse -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime | Select-Object -Last 1 + if ($log) { + Write-Output "=== $($log.FullName) ===" + Get-Content $log.FullName | Select-Object -Last 100 + } else { + Write-Output "No summary log found." + } + register: setup_log_content + + - name: Show log + ansible.builtin.debug: + msg: "{{ setup_log_content.output }}" + + - name: Fail + ansible.builtin.fail: + msg: "SQL Server installation failed — see log output above." + +- name: Ensure SQL Server service is started + ansible.windows.win_service: + name: "{{ 'MSSQLSERVER' if mssql_instance_name == 'MSSQLSERVER' else 'MSSQL$' + mssql_instance_name }}" + state: started + start_mode: auto + +- name: Ensure SQL Server Agent is started + ansible.windows.win_service: + name: "{{ 'SQLSERVERAGENT' if mssql_instance_name == 'MSSQLSERVER' else 'SQLAgent$' + mssql_instance_name }}" + state: started + start_mode: auto + +- name: Open SQL Server firewall port + community.windows.win_firewall_rule: + name: "SQL Server ({{ mssql_instance_name }}) TCP {{ mssql_tcp_port }}" + localport: "{{ mssql_tcp_port }}" + action: allow + direction: in + protocol: tcp + state: present + enabled: true + +- name: Configure SQL Server memory limits + ansible.windows.win_powershell: + script: | + $ErrorActionPreference = 'Stop' + + $sqlcmd = Get-ChildItem 'C:\Program Files\Microsoft SQL Server' -Filter 'sqlcmd.exe' ` + -Recurse -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -notlike '*\x86\*' } | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 -ExpandProperty FullName + if (-not $sqlcmd) { throw 'sqlcmd.exe not found under C:\Program Files\Microsoft SQL Server' } + + $instance = if ('{{ mssql_instance_name }}' -eq 'MSSQLSERVER') { '.' } ` + else { '.\{{ mssql_instance_name }}' } + + $queries = @( + "EXEC sp_configure 'show advanced options', 1; RECONFIGURE;", + "EXEC sp_configure 'max server memory (MB)', {{ mssql_max_memory_mb }}; RECONFIGURE;", + "EXEC sp_configure 'min server memory (MB)', {{ mssql_min_memory_mb }}; RECONFIGURE;" + ) + foreach ($q in $queries) { + & $sqlcmd -S $instance -E -Q $q | Out-Null + } + $Ansible.Result = "max={{ mssql_max_memory_mb }} MB, min={{ mssql_min_memory_mb }} MB" diff --git a/configuration/ansible_mssql/roles/mssql_prepare/defaults/main.yml b/configuration/ansible_mssql/roles/mssql_prepare/defaults/main.yml new file mode 100644 index 0000000..da99b95 --- /dev/null +++ b/configuration/ansible_mssql/roles/mssql_prepare/defaults/main.yml @@ -0,0 +1,7 @@ +--- +mssql_staging_dir: 'C:\SQLInstall' + +mssql_bootstrapper_url: "https://go.microsoft.com/fwlink/p/?linkid=2215158&clcid=0x409&culture=en-us&country=US" +mssql_bootstrapper_path: 'C:\SQLInstall\SQLServer2022-setup.exe' + +mssql_iso_path: 'C:\SQLInstall\SQLServer2022-x64-ENU.iso' diff --git a/configuration/ansible_mssql/roles/mssql_prepare/tasks/main.yml b/configuration/ansible_mssql/roles/mssql_prepare/tasks/main.yml new file mode 100644 index 0000000..5ebd990 --- /dev/null +++ b/configuration/ansible_mssql/roles/mssql_prepare/tasks/main.yml @@ -0,0 +1,63 @@ +--- +- name: Create SQL Server staging directory + ansible.windows.win_file: + path: "{{ mssql_staging_dir }}" + state: directory + +- name: Download SQL Server 2022 setup bootstrapper + ansible.windows.win_get_url: + url: "{{ mssql_bootstrapper_url }}" + dest: "{{ mssql_bootstrapper_path }}" + timeout: 120 + +- name: Download SQL Server 2022 ISO via bootstrapper + ansible.windows.win_command: >- + "{{ mssql_bootstrapper_path }}" + /Action=Download + /MEDIATYPE=ISO + /MEDIAPATH="{{ mssql_staging_dir }}" + /QUIET + args: + creates: "{{ mssql_iso_path }}" + timeout: 3600 + +- name: Find downloaded SQL Server ISO + ansible.windows.win_find: + paths: "{{ mssql_staging_dir }}" + patterns: "*.iso" + register: iso_files + +- name: Fail if no ISO found in staging directory + ansible.builtin.fail: + msg: >- + No ISO file found in {{ mssql_staging_dir }}. + The bootstrapper download may have failed. + Check {{ mssql_staging_dir }} on the target host. + when: iso_files.files | length == 0 + +- name: Mount SQL Server 2022 ISO + ansible.windows.win_powershell: + script: | + $ErrorActionPreference = 'Stop' + $imagePath = '{{ iso_files.files[0].path }}' + + $diskImage = Get-DiskImage -ImagePath $imagePath + if (-not $diskImage.Attached) { + Mount-DiskImage -ImagePath $imagePath | Out-Null + } + + $driveLetter = (Get-DiskImage -ImagePath $imagePath | Get-Volume).DriveLetter + if (-not $driveLetter) { + throw "ISO mounted but no drive letter was assigned" + } + + $Ansible.Result = "${driveLetter}:" + register: mount_result + +- name: Set SQL Server ISO drive letter fact + ansible.builtin.set_fact: + mssql_iso_drive: "{{ mount_result.result }}" + +- name: Show mounted drive letter + ansible.builtin.debug: + msg: "SQL Server 2022 ISO mounted at {{ mssql_iso_drive }} ({{ iso_files.files[0].path }})" diff --git a/provisioning/SMSSQLDW1P/backend.tfbackend b/provisioning/SMSSQLDW1P/backend.tfbackend new file mode 100644 index 0000000..617a24d --- /dev/null +++ b/provisioning/SMSSQLDW1P/backend.tfbackend @@ -0,0 +1,13 @@ +bucket = "cwxbkp1-prod" +key = "proxmox/prod/SMSSQLDW1P/terraform.tfstate" +region = "eu-central" + +endpoints = { + s3 = "https://nbg1.your-objectstorage.com" +} + +force_path_style = true +skip_credentials_validation = true +skip_metadata_api_check = true +skip_region_validation = true +skip_requesting_account_id = true diff --git a/provisioning/SMSSQLDW1P/main.tf b/provisioning/SMSSQLDW1P/main.tf new file mode 100644 index 0000000..2def32a --- /dev/null +++ b/provisioning/SMSSQLDW1P/main.tf @@ -0,0 +1,58 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + proxmox = { + source = "bpg/proxmox" + version = "~> 0.73" + } + } + + backend "s3" {} +} + +provider "proxmox" { + endpoint = var.proxmox_endpoint + insecure = var.proxmox_insecure + api_token = var.proxmox_api_token + + ssh { + username = var.proxmox_ssh_user + password = var.proxmox_ssh_password + agent = var.proxmox_ssh_agent + } +} + +module "vm" { + source = "../modules/vm" + + node_name = var.node_name + vm_id = var.vm_id + name = var.name + description = var.description + tags = var.tags + bios = "ovmf" + os_type = "win11" + + cpu_cores = var.cpu_cores + memory_dedicated = var.memory_dedicated + + clone_vm_id = var.clone_vm_id + clone_datastore_id = var.clone_datastore_id + + init_ipv4_address = var.init_ipv4_address + init_ipv4_gateway = var.init_ipv4_gateway + init_dns_servers = var.init_dns_servers + init_username = var.init_username + admin_password = var.admin_password + cloud_init_datastore = var.cloud_init_datastore + + disk_datastore = var.disk_datastore + disk_size = var.disk_size + + data_disk_size = var.data_disk_size + data_disk_datastore = var.data_disk_datastore + + network_bridge = var.network_bridge + network_vlan_id = var.network_vlan_id +} diff --git a/provisioning/SMSSQLDW1P/outputs.tf b/provisioning/SMSSQLDW1P/outputs.tf new file mode 100644 index 0000000..72a9c3f --- /dev/null +++ b/provisioning/SMSSQLDW1P/outputs.tf @@ -0,0 +1,19 @@ +output "vm_id" { + description = "The Proxmox VM ID" + value = module.vm.vm_id +} + +output "name" { + description = "The VM hostname" + value = module.vm.name +} + +output "ipv4_addresses" { + description = "IPv4 addresses reported by the QEMU agent" + value = module.vm.ipv4_addresses +} + +output "mac_addresses" { + description = "MAC addresses of the VM's network interfaces" + value = module.vm.mac_addresses +} diff --git a/provisioning/SMSSQLDW1P/terraform.tfvars.example b/provisioning/SMSSQLDW1P/terraform.tfvars.example new file mode 100644 index 0000000..d171aec --- /dev/null +++ b/provisioning/SMSSQLDW1P/terraform.tfvars.example @@ -0,0 +1,27 @@ +proxmox_endpoint = "https://proxmox.example.local:8006/api2/json" +proxmox_insecure = true +proxmox_api_token = "packer@pve!packer=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + +proxmox_ssh_user = "root" +proxmox_ssh_password = "changeme" +proxmox_ssh_agent = false + +node_name = "pve" + +vm_id = 200 +name = "SMSSQLDW1P" +init_ipv4_address = "10.0.0.100/24" +init_ipv4_gateway = "10.0.0.1" +init_dns_servers = ["10.0.0.2", "10.0.0.3"] +admin_password = "ChangeMe-StrongPassword!" + +cpu_cores = 4 +memory_dedicated = 16384 +disk_size = 80 +disk_datastore = "local-lvm" +data_disk_size = 80 +data_disk_datastore = "local-lvm" +network_bridge = "vmbr0" +cloud_init_datastore = "local-lvm" + +clone_vm_id = 1002 diff --git a/provisioning/SMSSQLDW1P/variables.tf b/provisioning/SMSSQLDW1P/variables.tf new file mode 100644 index 0000000..53f5f95 --- /dev/null +++ b/provisioning/SMSSQLDW1P/variables.tf @@ -0,0 +1,152 @@ +variable "proxmox_endpoint" { + description = "URL of the Proxmox API (e.g. https://192.168.1.10:8006/)" + type = string +} + +variable "proxmox_insecure" { + description = "Skip TLS certificate verification (set true for self-signed certs)" + type = bool + default = false +} + +variable "proxmox_api_token" { + description = "Proxmox API token in the form user@realm!tokenid=secret" + type = string + sensitive = true +} + +variable "proxmox_ssh_user" { + description = "SSH username for the Proxmox node" + type = string +} + +variable "proxmox_ssh_password" { + description = "SSH password for connecting to the Proxmox node" + type = string + sensitive = true + default = null +} + +variable "proxmox_ssh_agent" { + description = "Use SSH agent for authentication" + type = bool + default = true +} + +variable "node_name" { + description = "Proxmox cluster node name" + type = string +} + +variable "vm_id" { + description = "Unique Proxmox VM ID" + type = number +} + +variable "name" { + description = "VM hostname" + type = string +} + +variable "description" { + description = "VM description" + type = string + default = "" +} + +variable "tags" { + description = "Tags to apply to the VM" + type = list(string) + default = [] +} + +variable "cpu_cores" { + type = number + default = 2 +} + +variable "memory_dedicated" { + description = "RAM in MB" + type = number + default = 2048 +} + +variable "clone_vm_id" { + description = "VM ID of the Windows template to clone" + type = number + default = 1002 +} + +variable "clone_datastore_id" { + description = "Target datastore for the cloned disks (null = keep on source datastore)" + type = string + default = null +} + +variable "disk_datastore" { + type = string + default = "local-lvm" +} + +variable "disk_size" { + description = "Disk size in GB" + type = number + default = 60 +} + +variable "data_disk_size" { + description = "Size in GB of the data disk. Set to null to skip." + type = number + default = null +} + +variable "data_disk_datastore" { + description = "Proxmox datastore ID for the data disk." + type = string + default = "local-lvm" +} + +variable "network_bridge" { + type = string + default = "vmbr11" +} + +variable "network_vlan_id" { + type = number + default = null +} + +variable "init_ipv4_address" { + description = "IPv4 address in CIDR notation (e.g. 10.10.2.100/24) or dhcp" + type = string +} + +variable "init_ipv4_gateway" { + description = "IPv4 gateway address" + type = string + default = null +} + +variable "init_dns_servers" { + description = "List of DNS servers for initialization" + type = list(string) + default = [] +} + +variable "init_username" { + description = "Username for the initial account" + type = string + default = "Administrator" +} + +variable "admin_password" { + description = "Password for the initial account" + type = string + sensitive = true +} + +variable "cloud_init_datastore" { + description = "Datastore used to store the cloud-init drive" + type = string + default = "local" +} diff --git a/provisioning/modules/vm/main.tf b/provisioning/modules/vm/main.tf new file mode 100644 index 0000000..0620ebb --- /dev/null +++ b/provisioning/modules/vm/main.tf @@ -0,0 +1,103 @@ +resource "proxmox_virtual_environment_vm" "this" { + node_name = var.node_name + vm_id = var.vm_id + name = var.name + description = var.description + tags = var.tags + on_boot = var.on_boot + bios = var.bios + machine = var.machine + + dynamic "clone" { + for_each = var.clone_vm_id != null ? [1] : [] + content { + vm_id = var.clone_vm_id + full = var.clone_full + datastore_id = var.clone_datastore_id + } + } + + operating_system { + type = var.os_type + } + + cpu { + cores = var.cpu_cores + sockets = var.cpu_sockets + type = var.cpu_type + numa = true + } + + memory { + dedicated = var.memory_dedicated + } + + disk { + datastore_id = var.disk_datastore + interface = var.disk_interface + size = var.disk_size + file_id = var.clone_vm_id == null ? var.disk_file_id : null + discard = "on" + file_format = "raw" + } + + dynamic "disk" { + for_each = var.data_disk_size != null ? [1] : [] + content { + datastore_id = var.data_disk_datastore + interface = var.data_disk_interface + size = var.data_disk_size + discard = "on" + file_format = "raw" + } + } + + dynamic "efi_disk" { + for_each = var.bios == "ovmf" ? [1] : [] + content { + datastore_id = var.disk_datastore + file_format = "raw" + type = "4m" + } + } + + network_device { + bridge = var.network_bridge + model = "virtio" + vlan_id = var.network_vlan_id + } + + agent { + enabled = true + trim = true + } + + initialization { + datastore_id = var.cloud_init_datastore + + dynamic "dns" { + for_each = length(var.init_dns_servers) > 0 ? [1] : [] + content { + servers = var.init_dns_servers + } + } + + ip_config { + ipv4 { + address = var.init_ipv4_address + gateway = var.init_ipv4_gateway + } + } + + user_account { + username = var.init_username + password = var.admin_password + } + } + + lifecycle { + ignore_changes = [ + disk[0].file_id, + ] + } +} diff --git a/provisioning/modules/vm/outputs.tf b/provisioning/modules/vm/outputs.tf new file mode 100644 index 0000000..f85040d --- /dev/null +++ b/provisioning/modules/vm/outputs.tf @@ -0,0 +1,19 @@ +output "vm_id" { + description = "The Proxmox VM ID" + value = proxmox_virtual_environment_vm.this.vm_id +} + +output "name" { + description = "The VM hostname" + value = proxmox_virtual_environment_vm.this.name +} + +output "ipv4_addresses" { + description = "All IPv4 addresses reported by the QEMU agent" + value = proxmox_virtual_environment_vm.this.ipv4_addresses +} + +output "mac_addresses" { + description = "MAC addresses of the network interfaces" + value = proxmox_virtual_environment_vm.this.mac_addresses +} diff --git a/provisioning/modules/vm/variables.tf b/provisioning/modules/vm/variables.tf new file mode 100644 index 0000000..af0db4b --- /dev/null +++ b/provisioning/modules/vm/variables.tf @@ -0,0 +1,187 @@ +variable "node_name" { + description = "The name of the Proxmox node to provision the VM on" + type = string +} + +variable "vm_id" { + description = "The unique ID for the VM (100-999999999)" + type = number +} + +variable "name" { + description = "The VM name (must be a valid DNS name)" + type = string +} + +variable "description" { + description = "A description for the VM" + type = string + default = "" +} + +variable "tags" { + description = "List of tags to apply to the VM" + type = list(string) + default = [] +} + +variable "on_boot" { + description = "Whether to start the VM automatically on host boot" + type = bool + default = true +} + +variable "os_type" { + description = "OS type hint for Proxmox (l26 = Linux 2.6+, win10, etc.)" + type = string + default = "l26" +} + +variable "bios" { + description = "BIOS type: seabios or ovmf (UEFI)" + type = string + default = "seabios" +} + +variable "machine" { + description = "Machine type: pc or q35" + type = string + default = "q35" +} + +# CPU +variable "cpu_cores" { + description = "Number of CPU cores per socket" + type = number + default = 2 +} + +variable "cpu_sockets" { + description = "Number of CPU sockets" + type = number + default = 2 +} + +variable "cpu_type" { + description = "CPU emulation type (host for best performance, qemu64 for compatibility)" + type = string + default = "host" +} + +# Memory +variable "memory_dedicated" { + description = "Dedicated RAM in MB" + type = number + default = 2048 +} + +# Clone +variable "clone_vm_id" { + description = "VM ID of the template to clone. When set, a clone block is used instead of disk_file_id." + type = number + default = null +} + +variable "clone_full" { + description = "Perform a full clone (true) or linked clone (false)" + type = bool + default = true +} + +variable "clone_datastore_id" { + description = "Target datastore for the cloned disks (null = keep on source datastore)" + type = string + default = null +} + +# Disk +variable "disk_datastore" { + description = "Proxmox datastore ID for the VM disk" + type = string + default = "local-lvm" +} + +variable "disk_interface" { + description = "Disk interface (scsi0, virtio0, sata0)" + type = string + default = "scsi0" +} + +variable "disk_size" { + description = "Disk size in GB" + type = number + default = 20 +} + +variable "disk_file_id" { + description = "File ID of the base disk image to import (e.g. local:iso/debian-12-cloud.img). Ignored when clone_vm_id is set." + type = string + default = null +} + +variable "data_disk_size" { + description = "Size in GB of an optional second data disk. Set to null to skip." + type = number + default = null +} + +variable "data_disk_datastore" { + description = "Proxmox datastore ID for the data disk." + type = string + default = "local-lvm" +} + +variable "data_disk_interface" { + description = "Disk interface for the data disk (must differ from the OS disk)." + type = string + default = "scsi1" +} + +# Network +variable "network_bridge" { + description = "The Linux bridge to attach the network device to" + type = string + default = "vmbr0" +} + +variable "network_vlan_id" { + description = "Optional VLAN tag for the network device (null = no VLAN)" + type = number + default = null +} + +# Initialization (provider-native) +variable "init_ipv4_address" { + description = "IPv4 address in CIDR notation (e.g. 10.10.2.100/24) or dhcp" + type = string +} + +variable "init_ipv4_gateway" { + description = "IPv4 gateway address" + type = string + default = null +} + +variable "init_dns_servers" { + description = "List of DNS servers for initialization" + type = list(string) + default = [] +} + +variable "init_username" { + description = "Username for the initial account" + type = string + default = "Administrator" +} + +variable "admin_password" { + description = "Password for the initial account" + type = string + sensitive = true +} + +variable "cloud_init_datastore" { + description = "Datastore used to store the cloud-init drive" + type = string + default = "local" +} diff --git a/provisioning/modules/vm/versions.tf b/provisioning/modules/vm/versions.tf new file mode 100644 index 0000000..6af39d4 --- /dev/null +++ b/provisioning/modules/vm/versions.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + proxmox = { + source = "bpg/proxmox" + version = "~> 0.73" + } + } +}