cli: add basic support for constellation create on OpenStack (#1283)

* image: support OpenStack image build / upload

* cli: add OpenStack terraform template

* config: add OpenStack as CSP

* versionsapi: add OpenStack as CSP

* cli: add OpenStack as provider for `config generate` and `create`

* disk-mapper: add basic support for boot on OpenStack

* debugd: add placeholder for OpenStack

* image: fix config file sourcing for image upload
This commit is contained in:
Malte Poll 2023-02-27 18:19:52 +01:00 committed by GitHub
parent b013a7ab32
commit b79f7d0c8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 954 additions and 79 deletions

View file

@ -0,0 +1,52 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/random" {
version = "3.4.3"
constraints = "3.4.3"
hashes = [
"h1:hV66lcagXXRwwCW3Y542bI1JgPo8z/taYKT7K+a2Z5U=",
"h1:hXUPrH8igYBhatzatkp80RCeeUJGu9lQFDyKemOlsTo=",
"h1:saZR+mhthL0OZl4SyHXZraxyaBNVMxiZzks78nWcZ2o=",
"h1:tL3katm68lX+4lAncjQA9AXL4GR/VM+RPwqYf4D2X8Q=",
"h1:xZGZf18JjMS06pFa4NErzANI98qi59SEcBsOcS2P2yQ=",
"zh:41c53ba47085d8261590990f8633c8906696fa0a3c4b384ff6a7ecbf84339752",
"zh:59d98081c4475f2ad77d881c4412c5129c56214892f490adf11c7e7a5a47de9b",
"zh:686ad1ee40b812b9e016317e7f34c0d63ef837e084dea4a1f578f64a6314ad53",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:84103eae7251384c0d995f5a257c72b0096605048f757b749b7b62107a5dccb3",
"zh:8ee974b110adb78c7cd18aae82b2729e5124d8f115d484215fd5199451053de5",
"zh:9dd4561e3c847e45de603f17fa0c01ae14cae8c4b7b4e6423c9ef3904b308dda",
"zh:bb07bb3c2c0296beba0beec629ebc6474c70732387477a65966483b5efabdbc6",
"zh:e891339e96c9e5a888727b45b2e1bb3fcbdfe0fd7c5b4396e4695459b38c8cb1",
"zh:ea4739860c24dfeaac6c100b2a2e357106a89d18751f7693f3c31ecf6a996f8d",
"zh:f0c76ac303fd0ab59146c39bc121c5d7d86f878e9a69294e29444d4c653786f8",
"zh:f143a9a5af42b38fed328a161279906759ff39ac428ebcfe55606e05e1518b93",
]
}
provider "registry.terraform.io/terraform-provider-openstack/openstack" {
version = "1.48.0"
constraints = "1.48.0"
hashes = [
"h1:0Oy4KDG/+l4tKeB+kYglsrFnghsAVJr60YTAbLkhfWc=",
"h1:1OTZZtFI/HIdp052eDSmjZ29d/4YP0emMkH2VzQs/P0=",
"h1:Q1+17/v0+xpNDUvEqVOX9UqYTwb1suAdH73ObnIVepc=",
"h1:mTlCzugRpavDX3IG2zAs6ZainqpTUpNnQKbQuR523NA=",
"h1:qjf/qyH9oKOMujQk59bNxV8yLRbUhmihxMRrKOeA8qI=",
"zh:1fe237fa1153e05879fd26857416a1d029a3f108e32e83c4931dd874c777aa6a",
"zh:2c4587b4c810d569aafd69e287ecc2ee910e6c16cfc784e49861e0a8066b8655",
"zh:3f1a42fce3c925afeeaa96efae0bc9be95acfc80ba147a8123d03038d429df6b",
"zh:430511b62dc2fdafa070e9bd88e5e1fc39b3d667151aa9bf8e21b2c2c5421281",
"zh:4452279f6f23d3f2c5969deebf24ae2c38af8e02d52ee589b658c52b321835e5",
"zh:5525d1ca817f28ec9f0f648ea38b94fd0741130eaed2260bbd734efd03aecfb8",
"zh:675001e8cec8d0d4f006ce01b0608b7c5a378b4e56c6a27fbf5562f04371de70",
"zh:6c0f4da6da81da562e16af6fbb36035c0797de2a0384d0ef7c9a8b4676f8eca9",
"zh:79db708664ecbcf9d1a6d20e6a294716bff21a2641a8f58bfce60f3d11b944ef",
"zh:7bfc5ee6765694779fbfc00954fe04795035e85dfefd916dc6601717116b7005",
"zh:899a17c1547aa1bf732a55c903f3df25c8a0c107c16e0753677aecb8ed32130c",
"zh:9e02fb5267dc415a763ef55a24f3890f7e63de8d61e05e220d90a5a4a4b891ed",
"zh:a224e6e677e92cd31d0806a2d11c9bb17d032eaa0086e2aa8136ae0e9ce2fa83",
"zh:b3905869f6fea27ffd144eb8221ea67aeca63e23c06af43a221e55634faef3e2",
]
}

View file

@ -0,0 +1,245 @@
terraform {
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "1.48.0"
}
random = {
source = "hashicorp/random"
version = "3.4.3"
}
}
}
provider "openstack" {
cloud = var.cloud
}
locals {
uid = random_id.uid.hex
name = "${var.name}-${local.uid}"
initSecretHash = random_password.initSecret.bcrypt_hash
ports_node_range_start = "30000"
ports_node_range_end = "32767"
ports_kubernetes = "6443"
ports_bootstrapper = "9000"
ports_konnectivity = "8132"
ports_verify = "30081"
ports_recovery = "9999"
ports_debugd = "4000"
cidr_vpc_subnet_nodes = "192.168.178.0/24"
tags = ["constellation-uid-${local.uid}"]
}
resource "random_id" "uid" {
byte_length = 4
}
resource "random_password" "initSecret" {
length = 32
special = true
override_special = "_%@"
}
resource "openstack_images_image_v2" "constellation_os_image" {
name = local.name
image_source_url = var.image_url
web_download = var.direct_download
container_format = "bare"
disk_format = "raw"
visibility = "private"
properties = {
hw_firmware_type = "uefi"
os_type = "linux"
}
}
data "openstack_networking_network_v2" "floating_ip_pool" {
network_id = var.floating_ip_pool_id
}
resource "openstack_networking_network_v2" "vpc_network" {
name = local.name
description = "Constellation VPC network"
tags = local.tags
}
resource "openstack_networking_subnet_v2" "vpc_subnetwork" {
name = local.name
description = "Constellation VPC subnetwork"
network_id = openstack_networking_network_v2.vpc_network.id
cidr = local.cidr_vpc_subnet_nodes
dns_nameservers = [
"1.1.1.1",
"8.8.8.8",
"9.9.9.9",
]
tags = local.tags
}
resource "openstack_networking_router_v2" "vpc_router" {
name = local.name
external_network_id = data.openstack_networking_network_v2.floating_ip_pool.network_id
}
resource "openstack_networking_router_interface_v2" "vpc_router_interface" {
router_id = openstack_networking_router_v2.vpc_router.id
subnet_id = openstack_networking_subnet_v2.vpc_subnetwork.id
}
resource "openstack_compute_secgroup_v2" "vpc_secgroup" {
name = local.name
description = "Constellation VPC security group"
rule {
from_port = -1
to_port = -1
ip_protocol = "icmp"
self = true
}
rule {
from_port = local.ports_node_range_start
to_port = local.ports_node_range_end
ip_protocol = "tcp"
cidr = "0.0.0.0/0"
}
dynamic "rule" {
for_each = flatten([
local.ports_kubernetes,
local.ports_bootstrapper,
local.ports_konnectivity,
local.ports_verify,
local.ports_recovery,
var.debug ? [local.ports_debugd] : [],
])
content {
from_port = rule.value
to_port = rule.value
ip_protocol = "tcp"
cidr = "0.0.0.0/0"
}
}
}
module "instance_group_control_plane" {
source = "./modules/instance_group"
name = local.name
role = "ControlPlane"
instance_count = var.control_plane_count
image_id = openstack_images_image_v2.constellation_os_image.image_id
flavor_id = var.flavor_id
security_groups = [
openstack_compute_secgroup_v2.vpc_secgroup.id,
]
tags = local.tags
disk_size = var.state_disk_size
availability_zone = var.availability_zone
network_id = openstack_networking_network_v2.vpc_network.id
init_secret_hash = local.initSecretHash
}
module "instance_group_worker" {
source = "./modules/instance_group"
name = local.name
role = "Worker"
instance_count = var.worker_count
image_id = openstack_images_image_v2.constellation_os_image.image_id
flavor_id = var.flavor_id
tags = local.tags
security_groups = [
openstack_compute_secgroup_v2.vpc_secgroup.id,
]
disk_size = var.state_disk_size
availability_zone = var.availability_zone
network_id = openstack_networking_network_v2.vpc_network.id
init_secret_hash = local.initSecretHash
}
resource "openstack_networking_floatingip_v2" "public_ip" {
pool = data.openstack_networking_network_v2.floating_ip_pool.name
description = "Public ip for first control plane node"
tags = local.tags
}
resource "openstack_compute_floatingip_associate_v2" "public_ip_associate" {
floating_ip = openstack_networking_floatingip_v2.public_ip.address
instance_id = module.instance_group_control_plane.instance_ids.0
depends_on = [
openstack_networking_router_v2.vpc_router,
openstack_networking_router_interface_v2.vpc_router_interface,
]
}
# TODO: get LoadBalancer API enabled in the test environment
# resource "openstack_lb_loadbalancer_v2" "loadbalancer" {
# name = local.name
# description = "Constellation load balancer"
# vip_subnet_id = openstack_networking_subnet_v2.vpc_subnetwork.id
# }
# resource "openstack_networking_floatingip_v2" "loadbalancer_ip" {
# pool = data.openstack_networking_network_v2.floating_ip_pool.name
# description = "Loadbalancer ip for ${local.name}"
# tags = local.tags
# }
# module "loadbalancer_kube" {
# source = "./modules/loadbalancer"
# name = "${local.name}-kube"
# member_ips = module.instance_group_control_plane.ips.value
# loadbalancer_id = openstack_lb_loadbalancer_v2.loadbalancer.id
# subnet_id = openstack_networking_subnet_v2.vpc_subnetwork.id
# port = local.ports_kubernetes
# }
# module "loadbalancer_boot" {
# source = "./modules/loadbalancer"
# name = "${local.name}-boot"
# member_ips = module.instance_group_control_plane.ips
# loadbalancer_id = openstack_lb_loadbalancer_v2.loadbalancer.id
# subnet_id = openstack_networking_subnet_v2.vpc_subnetwork.id
# port = local.ports_bootstrapper
# }
# module "loadbalancer_verify" {
# source = "./modules/loadbalancer"
# name = "${local.name}-verify"
# member_ips = module.instance_group_control_plane.ips
# loadbalancer_id = openstack_lb_loadbalancer_v2.loadbalancer.id
# subnet_id = openstack_networking_subnet_v2.vpc_subnetwork.id
# port = local.ports_verify
# }
# module "loadbalancer_konnectivity" {
# source = "./modules/loadbalancer"
# name = "${local.name}-konnectivity"
# member_ips = module.instance_group_control_plane.ips
# loadbalancer_id = openstack_lb_loadbalancer_v2.loadbalancer.id
# subnet_id = openstack_networking_subnet_v2.vpc_subnetwork.id
# port = local.ports_konnectivity
# }
# module "loadbalancer_recovery" {
# source = "./modules/loadbalancer"
# name = "${local.name}-recovery"
# member_ips = module.instance_group_control_plane.ips
# loadbalancer_id = openstack_lb_loadbalancer_v2.loadbalancer.id
# subnet_id = openstack_networking_subnet_v2.vpc_subnetwork.id
# port = local.ports_recovery
# }
# module "loadbalancer_debugd" {
# count = var.debug ? 1 : 0 // only deploy debugd in debug mode
# source = "./modules/loadbalancer"
# name = "${local.name}-debugd"
# member_ips = module.instance_group_control_plane.ips
# loadbalancer_id = openstack_lb_loadbalancer_v2.loadbalancer.id
# subnet_id = openstack_networking_subnet_v2.vpc_subnetwork.id
# port = local.ports_debugd
# }

View file

@ -0,0 +1,54 @@
terraform {
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "1.48.0"
}
}
}
locals {
role_dashed = var.role == "ControlPlane" ? "control-plane" : "worker"
name = "${var.name}-${local.role_dashed}"
tags = distinct(sort(concat(var.tags, ["constellation-role-${local.role_dashed}"])))
}
# TODO: get this API enabled in the test environment
# resource "openstack_compute_servergroup_v2" "instance_group" {
# name = local.name
# policies = ["soft-anti-affinity"]
# }
resource "openstack_compute_instance_v2" "instance_group_member" {
name = "${local.name}-${count.index}"
count = var.instance_count
image_id = var.image_id
flavor_id = var.flavor_id
security_groups = var.security_groups
tags = local.tags
# TODO: get this API enabled in the test environment
# scheduler_hints {
# group = openstack_compute_servergroup_v2.instance_group.id
# }
network {
uuid = var.network_id
}
block_device {
uuid = var.image_id
source_type = "image"
destination_type = "local"
boot_index = 0
delete_on_termination = true
}
block_device {
source_type = "blank"
destination_type = "volume"
volume_size = var.disk_size
boot_index = 1
delete_on_termination = true
}
metadata = {
constellation-init-secret-hash = var.init_secret_hash
}
availability_zone_hints = var.availability_zone
}

View file

@ -0,0 +1,11 @@
output "instance_group" {
value = local.name
}
output "ips" {
value = openstack_compute_instance_v2.instance_group_member.*.access_ip_v4
}
output "instance_ids" {
value = openstack_compute_instance_v2.instance_group_member.*.id
}

View file

@ -0,0 +1,58 @@
variable "name" {
type = string
description = "Base name of the instance group."
}
variable "role" {
type = string
description = "The role of the instance group."
validation {
condition = contains(["ControlPlane", "Worker"], var.role)
error_message = "The role has to be 'ControlPlane' or 'Worker'."
}
}
variable "instance_count" {
type = number
description = "Number of instances in the instance group."
}
variable "image_id" {
type = string
description = "Image ID for the nodes."
}
variable "flavor_id" {
type = string
description = "Flavor ID (machine type) to use for the nodes."
}
variable "security_groups" {
type = list(string)
description = "Security groups to place the nodes in."
}
variable "tags" {
type = list(string)
description = "Tags to attach to each node."
}
variable "disk_size" {
type = number
description = "Disk size for the nodes, in GiB."
}
variable "availability_zone" {
type = string
description = "The availability zone to deploy the nodes in."
}
variable "network_id" {
type = string
description = "Network ID to attach each node to."
}
variable "init_secret_hash" {
type = string
description = "Hash of the init secret."
}

View file

@ -0,0 +1,40 @@
terraform {
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "1.48.0"
}
}
}
resource "openstack_lb_listener_v2" "listener" {
name = var.name
protocol = "TCP"
protocol_port = var.port
loadbalancer_id = var.loadbalancer_id
}
resource "openstack_lb_pool_v2" "pool" {
name = var.name
protocol = "TCP"
lb_method = "ROUND_ROBIN"
listener_id = openstack_lb_listener_v2.listener.id
}
resource "openstack_lb_member_v2" "member" {
count = length(var.member_ips)
name = format("${var.name}-member-%02d", count.index + 1)
address = var.member_ips[count.index]
protocol_port = var.port
pool_id = openstack_lb_pool_v2.pool.id
subnet_id = var.subnet_id
}
resource "openstack_lb_monitor_v2" "k8s_api" {
name = var.name
pool_id = openstack_lb_pool_v2.pool.id
type = "TCP"
delay = 2
timeout = 2
max_retries = 2
}

View file

@ -0,0 +1,25 @@
variable "name" {
type = string
description = "Base name of the load balancer rule."
}
variable "member_ips" {
type = list(string)
description = "The IP addresses of the members of the load balancer pool."
default = []
}
variable "loadbalancer_id" {
type = string
description = "The ID of the load balancer."
}
variable "subnet_id" {
type = string
description = "The ID of the members subnet."
}
variable "port" {
type = number
description = "The port on which to listen for incoming traffic."
}

View file

@ -0,0 +1,12 @@
output "ip" {
value = openstack_networking_floatingip_v2.public_ip.address
}
output "uid" {
value = local.uid
}
output "initSecret" {
value = random_password.initSecret.result
sensitive = true
}

View file

@ -0,0 +1,58 @@
variable "cloud" {
type = string
default = null
description = "The cloud to use within the OpenStack \"clouds.yaml\" file. Optional. If not set, environment variables are used."
}
variable "name" {
type = string
default = "constell"
description = "Base name of the cluster."
}
variable "control_plane_count" {
type = number
description = "The number of control plane nodes to deploy."
}
variable "worker_count" {
type = number
description = "The number of worker nodes to deploy."
}
variable "state_disk_size" {
type = number
default = 30
description = "The size of the state disk in GB."
}
variable "availability_zone" {
type = string
description = "The availability zone to deploy the nodes in."
}
variable "image_url" {
type = string
description = "The image to use for cluster nodes."
}
variable "direct_download" {
type = bool
description = "If enabled, downloads OS image directly from source URL to OpenStack. Otherwise, downloads image to local machine and uploads to OpenStack."
}
variable "flavor_id" {
type = string
description = "The flavor (machine type) to use for cluster nodes."
}
variable "floating_ip_pool_id" {
type = string
description = "The pool (network name) to use for floating IPs."
}
variable "debug" {
type = bool
default = false
description = "Enable debug mode. This opens up a debugd port that can be used to deploy a custom bootstrapper."
}

View file

@ -216,6 +216,46 @@ func (v *AzureIAMVariables) String() string {
return b.String()
}
// OpenStackClusterVariables is user configuration for creating a cluster with Terraform on OpenStack.
type OpenStackClusterVariables struct {
// CommonVariables contains common variables.
CommonVariables
// Cloud is the (optional) name of the OpenStack cloud to use when reading the "clouds.yaml" configuration file. If empty, environment variables are used.
Cloud string
// AvailabilityZone is the OpenStack availability zone to use.
AvailabilityZone string
// Flavor is the ID of the OpenStack flavor (machine type) to use.
FlavorID string
// FloatingIPPoolID is the ID of the OpenStack floating IP pool to use for public IPs.
FloatingIPPoolID string
// ImageURL is the URL of the OpenStack image to use.
ImageURL string
// DirectDownload decides whether to download the image directly from the URL to OpenStack or to upload it from the local machine.
DirectDownload bool
// Debug is true if debug mode is enabled.
Debug bool
}
// String returns a string representation of the variables, formatted as Terraform variables.
func (v *OpenStackClusterVariables) String() string {
b := &strings.Builder{}
b.WriteString(v.CommonVariables.String())
if v.Cloud != "" {
writeLinef(b, "cloud = %q", v.Cloud)
}
writeLinef(b, "availability_zone = %q", v.AvailabilityZone)
writeLinef(b, "flavor_id = %q", v.FlavorID)
writeLinef(b, "floating_ip_pool_id = %q", v.FloatingIPPoolID)
writeLinef(b, "image_url = %q", v.ImageURL)
writeLinef(b, "direct_download = %t", v.DirectDownload)
writeLinef(b, "debug = %t", v.Debug)
return b.String()
}
// TODO: Add support for OpenStack IAM variables.
// QEMUVariables is user configuration for creating a QEMU cluster with Terraform.
type QEMUVariables struct {
// CommonVariables contains common variables.