Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto-apply variables #21

Open
wants to merge 34 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
db7034f
Make more usable as remote source
GoodOldJack12 Sep 10, 2024
b86a2bf
Allow custom userdata
GoodOldJack12 Sep 10, 2024
d8042da
Make more usable as remote source part 2
GoodOldJack12 Sep 11, 2024
ca1da60
Update terraform provider
GoodOldJack12 Sep 12, 2024
f0c6457
Use raw userdata variable rather than file
GoodOldJack12 Sep 13, 2024
bfe9feb
Revert "Update terraform provider"
GoodOldJack12 Sep 13, 2024
4cb548f
Add ssh-user output
GoodOldJack12 Sep 13, 2024
7e557da
fix ssh-user output
GoodOldJack12 Sep 13, 2024
2e00759
Add volume automounting
GoodOldJack12 Sep 18, 2024
174f8f5
Add private IP output
GoodOldJack12 Sep 18, 2024
3e1bd37
Do not recreate instance when userdata is updated
GoodOldJack12 Sep 19, 2024
8aa61d2
Improve mount script
GoodOldJack12 Sep 20, 2024
1895d05
Add cron support
GoodOldJack12 Sep 24, 2024
039bf93
Add custom cloudinit options
GoodOldJack12 Sep 25, 2024
6bf82ca
Mount nfs and install nginx through ansible
GoodOldJack12 Sep 25, 2024
27c280d
Replace volume and nfs script with ansible
GoodOldJack12 Sep 27, 2024
bffd0b2
Remove userdata in root
GoodOldJack12 Oct 2, 2024
73c01cb
Merge branch 'port-forwarding' into nfs-server
GoodOldJack12 Oct 3, 2024
5a97d19
Remove ansible playbook var
GoodOldJack12 Oct 4, 2024
b6bdfe5
Remove filesystem validation
GoodOldJack12 Oct 4, 2024
3063f80
Fix cron
GoodOldJack12 Oct 7, 2024
5eb3680
Fix nginx
GoodOldJack12 Oct 15, 2024
f90a55a
Expose http on port 80 by default
GoodOldJack12 Oct 15, 2024
e44e94f
Automatically generate alt http port if required
GoodOldJack12 Oct 15, 2024
12dd14b
Work around bug when changing port-forward
GoodOldJack12 Oct 15, 2024
4e21f9b
Run ansible locally + fix some issues
GoodOldJack12 Oct 23, 2024
b02c39c
Move stuff around + cleanup
GoodOldJack12 Oct 23, 2024
58457d0
Improve connectivity check and allow script disabling
GoodOldJack12 Oct 24, 2024
7506edb
Windows fixes
GoodOldJack12 Oct 24, 2024
3608dff
Insure vsc net gets dhcp ip on ubuntu/debian
GoodOldJack12 Oct 24, 2024
f91e869
more dhcp fixes
GoodOldJack12 Oct 25, 2024
a672ebd
Better solution for hotplug interfaces
GoodOldJack12 Oct 25, 2024
72002b1
Fix non-debian issue
GoodOldJack12 Oct 25, 2024
0c4f84f
improve vsc ip script
GoodOldJack12 Oct 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion terraform/modules/multi_instance/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ module "main" {
project_name = var.project_name
access_key = var.access_key
vsc_enabled = var.public_vsc_enabled
playbook_url = var.playbook_url
userscript = var.userscript
is_windows = false
custom_secgroup_rules = var.public_secgroup_rules
}
module "private" {
count = var.private_count
Expand Down
14 changes: 12 additions & 2 deletions terraform/modules/multi_instance/variable.tf
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ variable "public_vsc_enabled" {
default = false
type = bool
}
variable "playbook_url" {
default = "https://raw.githubusercontent.com/hpcugent/openstack-templates/master/heat/playbooks/install_nginx.yaml"
variable "userscript" {
type = string
default = ""
}
variable "project_name" {
default = "default"
Expand All @@ -39,3 +40,12 @@ locals {
private_image = var.private_image == "default" ? var.public_image : var.private_image
private_flavor = var.private_flavor == "default" ? var.public_flavor : var.private_flavor
}
variable "public_secgroup_rules" {
type = map(object({
port = number
remote_ip_prefix = string
protocol = string
expose = optional(bool,false)
}))
default = {}
}
GoodOldJack12 marked this conversation as resolved.
Show resolved Hide resolved
31 changes: 31 additions & 0 deletions terraform/modules/nfs/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,34 @@ resource "openstack_sharedfilesystem_share_access_v2" "share_01_access" {
access_to = "0.0.0.0"
access_level = "rw"
}
resource "null_resource" "nfs" {
count = var.host.scripts_enabled ? 1 : 0
triggers = {
path = openstack_sharedfilesystem_share_v2.share_01.export_locations[0].path
ansible_command = local.ansible_command
ansible_env = jsonencode(local.ansible_env)
}
depends_on = [ openstack_sharedfilesystem_share_access_v2.share_01_access, openstack_compute_interface_attach_v2.ai_1 ]
provisioner "local-exec" {
environment = jsondecode(self.triggers.ansible_env)
command = <<EOF
${self.triggers.ansible_command} ${path.module}/mount_nfs.yaml -e "mount=true nfs_path=${self.triggers.path}"
EOF
}
provisioner "local-exec" {
environment = jsondecode(self.triggers.ansible_env)
when = destroy
on_failure = continue
command = <<EOF
${self.triggers.ansible_command} ${path.module}/mount_nfs.yaml -e "mount=false nfs_path=${self.triggers.path}"
EOF
}
}
locals {
ansible_env={
ANSIBLE_REMOTE_PORT = var.host.port
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we still have a remote ansible? or is this always used locally?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is for local ansible to know what port to connect to

ANSIBLE_REMOTE_USER = var.host.user
ANSIBLE_HOST_KEY_CHECKING = false
}
ansible_command="timeout 2m ansible-playbook -c ssh -i ${var.host.ip},"
}
12 changes: 12 additions & 0 deletions terraform/modules/nfs/mount_nfs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
- name: Configure NFS mount
hosts: all
tasks:
- name: NFS
ansible.posix.mount:
path: /mnt/nfs
src: "{{ nfs_path }}"
fstype: nfs
opts: defaults,_netdev
state: "{{ 'mounted' if mount == 'true' else 'absent' }}"
become: true
timeout: 120
8 changes: 8 additions & 0 deletions terraform/modules/nfs/variable.tf
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,11 @@ variable "vm_name" {
variable "user_name" {

}
variable "host" {
type = object({
ip = string
port = string
user = string
scripts_enabled = bool
})
}
29 changes: 29 additions & 0 deletions terraform/modules/single_instance/ansible.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
locals {
ansible_env={
ANSIBLE_REMOTE_PORT = local.ports.ssh
ANSIBLE_REMOTE_USER = local.ssh_user
ANSIBLE_HOST_KEY_CHECKING = false
}
ansible_command="timeout 4m ansible-playbook -c ssh -i ${data.openstack_networking_floatingip_v2.public.address},"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you use the public IP instead of the local _vm private one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because otherwise users using terraform on their local laptop wont be able to connect to the vm

}
resource "null_resource" "testconnection" {
count = local.scripts_enabled ? 1 : 0
depends_on = [ openstack_compute_instance_v2.instance_01 ]
triggers = {
user = local.ssh_user
port = local.ports.ssh
ip = data.openstack_networking_floatingip_v2.public.address
}
provisioner "remote-exec" {
connection {
user = self.triggers.user
host = self.triggers.ip
port = self.triggers.port
timeout = "5m"
}
inline = [ "until [ \"$(cloud-init status)\" = 'status: done' ] ;do sleep 5;done" ]
}
}
locals {
scripts_enabled = var.is_windows ? false : var.scripts_enabled
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add an extra new line at the end of the files, the same for the rest

32 changes: 32 additions & 0 deletions terraform/modules/single_instance/cloudinit.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
data "cloudinit_config" "main" {
gzip = false
base64_encode = false
part {
filename = "cloud-config.yaml"
content_type = "text/cloud-config"
content = file("${local.scripts_dir}/cloud-config.yaml")
}
part {
filename = "userscript.sh"
content_type = "text/x-shellscript"
content = var.userscript
}
part {
filename = "install_common.sh"
content_type = "text/x-shellscript"
content = <<-EOF
#!/bin/bash
if [[ -r '/etc/debian_version' ]];then
apt-get update && apt-get install -y nfs-common cron
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I won't install nfs if the user/project does not want to use manila service

fi
EOF
}
dynamic "part" {
for_each = var.cloudinit
content {
filename = part.key
content_type = part.value.content_type
content = part.value.content
}
}
}
71 changes: 71 additions & 0 deletions terraform/modules/single_instance/cron.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
variable "crontabs" {
type = map(object({
cron_time = string
cron_user = optional(string,"root")
script = string
}))
default = {}
validation {
condition = !( length(var.crontabs) > 0 && var.is_windows )
error_message = "Can't add crons to windows!"
}
}

resource "null_resource" "cron" {
for_each = {
for k,v in local.crons : k => v
if local.scripts_enabled
}
depends_on = [ null_resource.testconnection ]
triggers = {
user = local.ssh_user
port = local.ports.ssh
ip = data.openstack_networking_floatingip_v2.public.address
content = each.value.script
}
connection {
type = "ssh"
user = self.triggers.user
agent = true
host = self.triggers.ip
timeout = "5m"
port = self.triggers.port
}
provisioner "file" {
destination = "/home/${local.ssh_user}/${random_id.obscure[each.key].id}-${each.key}.sh"
content = each.value.script
}
provisioner "file" {
destination = "/home/${local.ssh_user}/${random_id.obscure[each.key].id}-${each.key}.cron"
content = <<-EOT
${each.value.cron_time} ${each.value.cron_user} /opt/vsc/cron/${each.key}.sh

EOT
}
provisioner "remote-exec" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not local-exec?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do have a Proof Of Concept for an ansible playbook. Only downside is that it doesn't really support the "time format string" like * * * * * and instead needs dedicated variables for minute/hour/day/weekday/...

inline = [
"set -e",
"sudo cp /home/${local.ssh_user}/${random_id.obscure[each.key].id}-${each.key}.sh /opt/vsc/cron/${each.key}.sh",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these files only accessible by root user?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The files are placed inside the user home directory first with default permissions, and are then immediately copied and chown-ed to root:

"sudo chown root:root /etc/cron.d/${each.key}",

There is a brief window where these files are not owned by root, but considering this user has access to sudo anyway that doesn't really make a difference. Nevertheless with an ansible solution we could place the files in the right directory immediately

"sudo chmod +x /opt/vsc/cron/${each.key}.sh",
"sudo mv /home/${local.ssh_user}/${random_id.obscure[each.key].id}-${each.key}.cron /etc/cron.d/${each.key}",
"sudo chown root:root /etc/cron.d/${each.key}",
"sudo restorecon /etc/cron.d/${each.key} || echo \"SELinux not installed?\""
]
}
provisioner "remote-exec" {
when = destroy
on_failure = continue
inline = [
"sudo rm /etc/cron.d/${each.key}",
"sudo rm /opt/vsc/cron/${each.key}.sh"
]
}
}
resource "random_id" "obscure" {
for_each = local.crons
byte_length = 8
}
locals {
crons = var.is_windows ? {} : merge(var.crontabs,local.default_crons)
default_crons = {}
}
2 changes: 1 addition & 1 deletion terraform/modules/single_instance/custom_secgroup.tf
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ resource "shell_script" "custom_portforward" {
"OS_CLOUD" = local.cloud
}
lifecycle_commands {
create = file("../scripts/generate_port.sh")
create = file("${local.scripts_dir}/generate_port.sh")
delete = <<-EOF
rm -rf "port_${var.vm_name}-${substr(random_uuid.uuid.result, 0, 4)}_${each.key}.json"
EOF
Expand Down
14 changes: 8 additions & 6 deletions terraform/modules/single_instance/data.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ locals {
any_enabled = var.nfs_enabled || var.nginx_enabled
ports = {
ssh = jsondecode(shell_script.port_ssh.output["ports"])[0]
http = var.nginx_enabled ? jsondecode(shell_script.port_http[0].output["ports"])[0] : null
http = var.nginx_enabled ? ( var.alt_http ? jsondecode(shell_script.port_http[0].output["ports"])[0] : 80 ) : null
https = var.nginx_enabled ? ( var.alt_http ? jsondecode(shell_script.port_http[0].output["ports"])[1] : 443) : null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should add this in the current docs if we do not have it yet, ports 80 and 443 are open from ugent net

}
ssh_internal_port = var.is_windows ? 3389 : 22
project_name = data.openstack_identity_project_v3.project.name
cloud = jsondecode(file("${path.cwd}/terraform.tfvars.json"))["cloud"]
cloud = jsondecode(file("${path.root}/terraform.tfvars.json"))["cloud"]
access_key = var.access_key == "default" ? data.shell_script.access_key.output["Name"] : var.access_key
disk_var = var.rootdisk_size == "default" ? data.openstack_compute_flavor_v2.flavor.disk : var.rootdisk_size
disk_size = var.is_windows ? max(local.disk_var,60) : local.disk_var
scripts_dir = "${path.module}/scripts"
}

# UUID for this "instance of the module" rather than depending on a changeable instance ID
Expand All @@ -25,7 +27,7 @@ resource "shell_script" "port_ssh" {
"OS_CLOUD" = local.cloud
}
lifecycle_commands {
create = file("../scripts/generate_port.sh")
create = file("${local.scripts_dir}/generate_port.sh")
delete = <<-EOF
rm -rf "port_${var.vm_name}-${substr(random_uuid.uuid.result, 0, 4)}_ssh.json"
EOF
Expand All @@ -37,15 +39,15 @@ resource "shell_script" "port_ssh" {
interpreter = ["/bin/bash", "-c"]
}
resource "shell_script" "port_http" {
count = var.nginx_enabled ? 1 : 0
count = var.nginx_enabled && var.alt_http ? 1 : 0
environment = {
"OS_CLOUD" = local.cloud
"IP_ID" = data.openstack_networking_floatingip_v2.public.id
"PORT_COUNT" = 1
"PORT_COUNT" = 2
"PORT_NAME" = "${var.vm_name}-${substr(random_uuid.uuid.result, 0, 4)}_http"
}
lifecycle_commands {
create = file("../scripts/generate_port.sh")
create = file("${local.scripts_dir}/generate_port.sh")
delete = <<-EOF
rm -rf "port_${var.vm_name}-${substr(random_uuid.uuid.result, 0, 4)}_http.json"
EOF
Expand Down
56 changes: 55 additions & 1 deletion terraform/modules/single_instance/http.tf
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,37 @@ resource "openstack_networking_portforwarding_v2" "http" {
internal_port_id = openstack_networking_port_v2.vm.id
internal_ip_address = openstack_networking_port_v2.vm.all_fixed_ips[0]
protocol = "tcp"
depends_on = [openstack_networking_secgroup_rule_v2.http, shell_script.port_http]
lifecycle {
precondition {
condition = var.public
error_message = ("Cant enable forward on a private instance!")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't

}
replace_triggered_by = [terraform_data.http_port.output]
}
description = "${data.openstack_identity_auth_scope_v3.scope.user_name}-${var.vm_name}-http-80"
}
resource terraform_data http_port {
# stupid work around for terraform being unable to cope with optional replaced_triggered_by
input = try(shell_script.port_http[0].output,local.ports.http)
}
resource "openstack_networking_portforwarding_v2" "https" {
count = var.nginx_enabled ? 1 : 0
floatingip_id = data.openstack_networking_floatingip_v2.public.id
external_port = local.ports.https
internal_port = 443
internal_port_id = openstack_networking_port_v2.vm.id
internal_ip_address = openstack_networking_port_v2.vm.all_fixed_ips[0]
protocol = "tcp"
depends_on = [openstack_networking_secgroup_rule_v2.http]
lifecycle {
precondition {
condition = var.public
error_message = ("Cant enable forward on a private instance!")
}
replace_triggered_by = [terraform_data.http_port.output]
}
description = "${data.openstack_identity_auth_scope_v3.scope.user_name}-${var.vm_name}-http"
description = "${data.openstack_identity_auth_scope_v3.scope.user_name}-${var.vm_name}-http-443"
}
resource "openstack_networking_secgroup_rule_v2" "http" {
count = var.nginx_enabled ? 1 : 0
Expand All @@ -26,3 +49,34 @@ resource "openstack_networking_secgroup_rule_v2" "http" {
security_group_id = openstack_networking_secgroup_v2.secgroup.id
description = "${data.openstack_identity_auth_scope_v3.scope.user_name}-${var.vm_name}-http"
}
resource "openstack_networking_secgroup_rule_v2" "https" {
count = var.nginx_enabled ? 1 : 0
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 443
port_range_max = 443
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.secgroup.id
description = "${data.openstack_identity_auth_scope_v3.scope.user_name}-${var.vm_name}-http"
}
resource "null_resource" "nginx" {
count = ( var.nginx_enabled && local.scripts_enabled ) ? 1 : 0
triggers = {
enabled = var.nginx_enabled
scripts_dir = local.scripts_dir
ansible_command = local.ansible_command
environment = jsonencode(local.ansible_env)
}
depends_on = [ null_resource.testconnection ]
provisioner "local-exec" {
environment = jsondecode(self.triggers.environment)
command = "${self.triggers.ansible_command} ${self.triggers.scripts_dir}/ansible/nginx.yaml --extra-vars install=${self.triggers.enabled}"
}
provisioner "local-exec" {
environment = jsondecode(self.triggers.environment)
when = destroy
on_failure = continue
command= "${self.triggers.ansible_command} ${self.triggers.scripts_dir}/ansible/nginx.yaml --extra-vars install=false"
}
}
Loading