First PoC on provisioning instance end to end on openstack
Some checks failed
/ dev-shell (push) Successful in 19s
/ check (push) Failing after 18s
/ terraform-providers (push) Successful in 30s
/ rust-packages (push) Successful in 39s

This commit is contained in:
Kaare Hoff Skovgaard 2025-07-08 16:08:37 +02:00
parent 1e8460c2ec
commit 1945038c90
Signed by: khs
GPG key ID: C7D890804F01E9F0
24 changed files with 479 additions and 44 deletions

178
flake.lock generated
View file

@ -83,6 +83,28 @@
"type": "github" "type": "github"
} }
}, },
"disko_2": {
"inputs": {
"nixpkgs": [
"nixos-anywhere",
"nixpkgs"
]
},
"locked": {
"lastModified": 1748225455,
"narHash": "sha256-AzlJCKaM4wbEyEpV3I/PUq5mHnib2ryEy32c+qfj6xk=",
"owner": "nix-community",
"repo": "disko",
"rev": "a894f2811e1ee8d10c50560551e50d6ab3c392ba",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "master",
"repo": "disko",
"type": "github"
}
},
"flake-base": { "flake-base": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@ -122,6 +144,27 @@
} }
}, },
"flake-parts": { "flake-parts": {
"inputs": {
"nixpkgs-lib": [
"nixos-anywhere",
"nixpkgs"
]
},
"locked": {
"lastModified": 1743550720,
"narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "c621e8422220273271f52058f618c94e405bb0f5",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_2": {
"inputs": { "inputs": {
"nixpkgs-lib": [ "nixpkgs-lib": [
"terranix", "terranix",
@ -209,6 +252,116 @@
"type": "github" "type": "github"
} }
}, },
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1751810233,
"narHash": "sha256-kllkNbIqQi3VplgTMeGzuh1t8Gk8TauvkTRt93Km+tQ=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "9b0873b46c9f9e4b7aa01eb634952c206af53068",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-25.05",
"repo": "home-manager",
"type": "github"
}
},
"nix-vm-test": {
"inputs": {
"nixpkgs": [
"nixos-anywhere",
"nixpkgs"
]
},
"locked": {
"lastModified": 1748765518,
"narHash": "sha256-vftOR+7zwnMWl5UpG32GL1VBeNGTDZZT0hv+2uNuBGw=",
"owner": "Mic92",
"repo": "nix-vm-test",
"rev": "d6642fbaf42fc98883d84bab66cd0ec720d9dd0c",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "nix-vm-test",
"type": "github"
}
},
"nixos-anywhere": {
"inputs": {
"disko": "disko_2",
"flake-parts": "flake-parts",
"nix-vm-test": "nix-vm-test",
"nixos-images": "nixos-images",
"nixos-stable": "nixos-stable",
"nixpkgs": [
"nixpkgs"
],
"treefmt-nix": "treefmt-nix_2"
},
"locked": {
"lastModified": 1749105224,
"narHash": "sha256-hVTCvMnwywxQ6rGgO7ytBiSpVuLOHNgm3w3vE8UNaQY=",
"owner": "nix-community",
"repo": "nixos-anywhere",
"rev": "55fc48f9677822393cd9585b6742e1194accf365",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "1.11.0",
"repo": "nixos-anywhere",
"type": "github"
}
},
"nixos-images": {
"inputs": {
"nixos-stable": [
"nixos-anywhere",
"nixos-stable"
],
"nixos-unstable": [
"nixos-anywhere",
"nixpkgs"
]
},
"locked": {
"lastModified": 1748481078,
"narHash": "sha256-jwKRF2EDzlv0VBF8pImPFT7DAJma7stDun25utHtwBw=",
"owner": "nix-community",
"repo": "nixos-images",
"rev": "191a461dc38313ff41bd3df4b82e49f74a56560d",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixos-images",
"type": "github"
}
},
"nixos-stable": {
"locked": {
"lastModified": 1748437600,
"narHash": "sha256-hYKMs3ilp09anGO7xzfGs3JqEgUqFMnZ8GMAqI6/k04=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7282cb574e0607e65224d33be8241eae7cfe0979",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1751582995, "lastModified": 1751582995,
@ -246,6 +399,8 @@
"crane": "crane", "crane": "crane",
"disko": "disko", "disko": "disko",
"flake-base": "flake-base", "flake-base": "flake-base",
"home-manager": "home-manager",
"nixos-anywhere": "nixos-anywhere",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay", "rust-overlay": "rust-overlay",
"terranix": "terranix", "terranix": "terranix",
@ -327,7 +482,7 @@
}, },
"terranix": { "terranix": {
"inputs": { "inputs": {
"flake-parts": "flake-parts", "flake-parts": "flake-parts_2",
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
], ],
@ -427,6 +582,27 @@
"repo": "treefmt-nix", "repo": "treefmt-nix",
"type": "github" "type": "github"
} }
},
"treefmt-nix_2": {
"inputs": {
"nixpkgs": [
"nixos-anywhere",
"nixpkgs"
]
},
"locked": {
"lastModified": 1748243702,
"narHash": "sha256-9YzfeN8CB6SzNPyPm2XjRRqSixDopTapaRsnTpXUEY8=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "1f3f7b784643d488ba4bf315638b2b0a4c5fb007",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View file

@ -15,6 +15,10 @@
url = "github:terranix/terranix"; url = "github:terranix/terranix";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
home-manager = {
url = "github:nix-community/home-manager/release-25.05";
inputs.nixpkgs.follows = "nixpkgs";
};
terranix-hcloud = { terranix-hcloud = {
url = "github:terranix/terranix-hcloud"; url = "github:terranix/terranix-hcloud";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
@ -30,6 +34,12 @@
nixpkgs.follows = "nixpkgs"; nixpkgs.follows = "nixpkgs";
}; };
}; };
nixos-anywhere = {
url = "github:nix-community/nixos-anywhere/1.11.0";
inputs = {
nixpkgs.follows = "nixpkgs";
};
};
}; };
outputs = outputs =

View file

@ -0,0 +1,4 @@
{
snowfallorg.user.name = "khs";
home.stateVersion = "25.05";
}

View file

@ -0,0 +1,53 @@
{ ... }:
{
disko-root-lvm-bios =
{
diskName,
device,
mbrSize ? "1M",
bootPartName ? "mbr",
rootPartName ? "primary",
volumeGroupName ? "mainpool",
rootLvName ? "root",
}:
{
devices.disk = {
"${diskName}" = {
inherit device;
type = "disk";
content = {
type = "gpt";
partitions = {
"${bootPartName}" = {
size = mbrSize;
type = "EF02";
};
"${rootPartName}" = {
size = "100%";
content = {
type = "lvm_pv";
vg = volumeGroupName;
};
};
};
};
};
};
devices.lvm_vg = {
"${volumeGroupName}" = {
type = "lvm_vg";
lvs = {
"${rootLvName}" = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
mountOptions = [ "defaults" ];
};
};
};
};
};
};
}

View file

@ -1,6 +1,6 @@
{ ... }: { ... }:
{ {
disko-root-lvm = disko-root-lvm-uefi =
{ {
diskName, diskName,
device, device,

View file

@ -24,7 +24,7 @@ in
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
disko = lib.khscodes.disko-root-lvm { disko = lib.khscodes.disko-root-lvm-uefi {
device = "/dev/sda"; device = "/dev/sda";
diskName = cfg.diskName; diskName = cfg.diskName;
}; };

View file

@ -184,11 +184,18 @@ in
khscodes.openstack.compute_instance.compute = { khscodes.openstack.compute_instance.compute = {
inherit tags; inherit tags;
name = fqdn; name = fqdn;
initial_image = "Ubuntu-22.04"; initial_image = "debian-12";
flavor = cfg.flavor; flavor = cfg.flavor;
ssh_public_key = cfg.ssh_key; ssh_public_key = cfg.ssh_key;
firewall_rules = firewallRules; firewall_rules = firewallRules;
}; };
khscodes.unifi.enable = true;
khscodes.unifi.static_route.compute = {
name = fqdn;
network = config.khscodes.openstack.output.compute_instance.compute.ipv6_cidr;
distance = 1;
next_hop = config.khscodes.openstack.output.compute_instance.compute.ipv6_external_gateway;
};
khscodes.cloudflare = { khscodes.cloudflare = {
enable = true; enable = true;
dns = { dns = {
@ -230,15 +237,18 @@ in
} }
]; ];
khscodes.provisioning.pre = { khscodes.provisioning = {
modules = modules; pre = {
secretsSource = cfg.secretsSource; modules = modules;
endpoints = [ secretsSource = cfg.secretsSource;
"aws" endpoints = [
"cloudflare" "aws"
"openstack" "cloudflare"
"unifi" "openstack"
]; "unifi"
];
};
preImageUsername = "debian";
}; };
} }
); );

View file

@ -16,11 +16,12 @@ in
}; };
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
disko = lib.khscodes.disko-root-lvm { disko = lib.khscodes.disko-root-lvm-bios {
device = "/dev/sda"; device = "/dev/sda";
diskName = cfg.diskName; diskName = cfg.diskName;
}; };
khscodes.systemd-boot.enable = lib.mkDefault true; boot.loader.grub.efiSupport = false;
boot.loader.timeout = 1;
khscodes.qemu-guest.enable = true; khscodes.qemu-guest.enable = true;
}; };
} }

View file

@ -44,6 +44,11 @@ in
type = lib.types.nullOr lib.types.path; type = lib.types.nullOr lib.types.path;
description = "The generated config for the pre provisioning, if any was specified"; description = "The generated config for the pre provisioning, if any was specified";
}; };
preImageUsername = lib.mkOption {
type = lib.types.str;
description = "The username for the image being deployed before being swapped for NixOS";
default = "root";
};
postConfig = lib.mkOption { postConfig = lib.mkOption {
type = lib.types.nullOr lib.types.path; type = lib.types.nullOr lib.types.path;
description = "The generated config for the post provisioning, if any was specified"; description = "The generated config for the post provisioning, if any was specified";

View file

@ -0,0 +1,20 @@
{ config, lib, ... }:
let
cfg = config.khscodes.services.openssh;
in
{
options.khscodes.services.openssh = {
enable = lib.mkEnableOption "Enables openssh service for the instance";
};
config = lib.mkIf cfg.enable {
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "no";
KbdInteractiveAuthentication = false;
};
};
};
}

View file

@ -1,11 +0,0 @@
{ config, lib, ... }:
let
cfg = config.khscodes.sshd;
in
{
options.khscodes.sshd.enable = lib.mkEnableOption "Enables sshd for the instance";
config = lib.mkIf cfg.enable {
services.sshd.enable = true;
};
}

View file

@ -21,6 +21,10 @@ let
type = lib.types.str; type = lib.types.str;
description = "The IPv6 external gateway for the network. This is useful to eg. create static routes in Unifi"; description = "The IPv6 external gateway for the network. This is useful to eg. create static routes in Unifi";
}; };
ipv6_cidr = lib.mkOption {
type = lib.types.str;
description = "IPv6 cidr";
};
}; };
}; };
in in
@ -44,6 +48,7 @@ in
ipv4_address = "\${ openstack_networking_floatingip_v2.${sanitizedName}.address }"; ipv4_address = "\${ openstack_networking_floatingip_v2.${sanitizedName}.address }";
ipv6_address = "\${ data.openstack_networking_port_v2.${sanitizedName}.all_fixed_ips[1] }"; ipv6_address = "\${ data.openstack_networking_port_v2.${sanitizedName}.all_fixed_ips[1] }";
ipv6_external_gateway = "\${ [for ip in openstack_networking_router_v2.${sanitizedName}.external_fixed_ip : ip.ip_address if replace(ip.ip_address, \":\", \"\") != ip.ip_address][0] }"; ipv6_external_gateway = "\${ [for ip in openstack_networking_router_v2.${sanitizedName}.external_fixed_ip : ip.ip_address if replace(ip.ip_address, \":\", \"\") != ip.ip_address][0] }";
ipv6_cidr = "\${ openstack_networking_subnet_v2.${sanitizedName}_ip6.cidr }";
} }
) )
) cfg.compute_instance; ) cfg.compute_instance;

View file

@ -5,19 +5,65 @@ let
modules = [ modules = [
./output.nix ./output.nix
]; ];
unifiStaticRouteModule = khscodesLib.mkSubmodule {
description = "Unifi static route";
options = {
network = lib.mkOption {
type = lib.types.str;
description = "The network to make a static route for";
};
name = lib.mkOption {
type = lib.types.str;
description = "Human friendly name of the static route";
};
distance = lib.mkOption {
type = lib.types.int;
description = "The distance of the hop";
};
next_hop = lib.mkOption {
type = lib.types.str;
description = "The router that can route the network";
};
};
};
in in
{ {
options.khscodes.unifi = { options.khscodes.unifi = {
enable = lib.mkEnableOption "Enables the unifi provider"; enable = lib.mkEnableOption "Enables the unifi provider";
bucket = { static_route = lib.mkOption {
key = lib.mkOption { type = lib.types.attrsOf unifiStaticRouteModule;
type = lib.types.str; description = "Static routes";
description = "key for the bucket to use";
};
}; };
}; };
imports = lib.lists.map (m: import m { inherit khscodesLib inputs; }) modules; imports = lib.lists.map (m: import m { inherit khscodesLib inputs; }) modules;
config = lib.mkIf cfg.enable { }; config = lib.mkIf cfg.enable {
terraform.required_providers.unifi = {
source = "paultyng/unifi";
version = "= 0.42.0-prerelease";
};
provider.unifi = {
allow_insecure = true;
};
resource.unifi_static_route = lib.mapAttrs' (
name: value:
let
sanitizedName = khscodesLib.sanitize-terraform-name name;
in
{
name = sanitizedName;
value = {
inherit (value)
network
name
distance
next_hop
;
type = "nexthop-route";
};
}
) cfg.static_route;
};
} }

View file

@ -17,8 +17,8 @@
TF_VAR_hcloud_api_token = "Terraform API Token"; TF_VAR_hcloud_api_token = "Terraform API Token";
}; };
"Ubiquiti" = { "Ubiquiti" = {
TF_VAR_unifi_username = "Terraform username"; UNIFI_USERNAME = "Terraform username";
TF_VAR_unifi_password = "Terraform password"; UNIFI_PASSWORD = "Terraform password";
TF_VAR_unifi_url = "Terraform URL"; UNIFI_API = "Terraform URL";
}; };
} }

View file

@ -0,0 +1,9 @@
{ pkgs, ... }:
pkgs.writeShellApplication {
name = "create-instance";
runtimeInputs = [ pkgs.khscodes.pre-provisioning ];
text = ''
instance="''${1:-}"
pre-provisioning "$instance" apply
'';
}

View file

@ -0,0 +1,9 @@
{ pkgs, ... }:
pkgs.writeShellApplication {
name = "destroy-instance";
runtimeInputs = [ pkgs.khscodes.pre-provisioning ];
text = ''
instance="''${1:-}"
pre-provisioning "$instance" destroy
'';
}

View file

@ -0,0 +1,27 @@
{
inputs,
pkgs,
}:
pkgs.writeShellApplication {
name = "nixos-install";
runtimeInputs = [
pkgs.nix
pkgs.nixos-anywhere
];
# TODO: Use secret source and required secrets to set up the correct env variables
text = ''
hostname="$1"
# Build the configuration to ensure it doesn't fail when trying to install it on the host
nix build --no-link '${inputs.self}#nixosConfigurations."'"$hostname"'".config.system.build.toplevel'
# Allow overriding the host to connec tto, this is useful when testing and the DNS entries are stale with older IPs.
host="''${2:-$1}"
baseAttr='${inputs.self}#nixosConfigurations."'"$hostname"'".config.khscodes.provisioning'
config="$(nix build --no-link --print-out-paths "''${baseAttr}.preConfig")"
username="$(nix eval --raw "''${baseAttr}.preImageUsername")"
if [[ "$config" == "null" ]]; then
echo "No preprovisioning needed"
exit 0
fi
nixos-anywhere --flake "${inputs.self}#$hostname" --target-host "$username@$host"
'';
}

View file

@ -5,7 +5,7 @@ pkgs.terraform-providers.mkProvider {
owner = "paultyng"; owner = "paultyng";
repo = "terraform-provider-unifi"; repo = "terraform-provider-unifi";
rev = "ff9c041b3dc4dc6cf6db4c824a43ed6d3ae408d1"; rev = "ff9c041b3dc4dc6cf6db4c824a43ed6d3ae408d1";
version = "v0.42.0-prerelease"; version = "0.42.0-prerelease";
spdx = "MPL-2.0"; spdx = "MPL-2.0";
vendorHash = null; vendorHash = null;
} }

View file

@ -0,0 +1,10 @@
{ inputs, pkgs, ... }:
pkgs.writeShellApplication {
name = "update-instance";
runtimeInputs = [ pkgs.nixos-rebuild ];
text = ''
instance="''${1:-}"
connect_host="''${2:-$1}"
nixos-rebuild switch --flake "${inputs.self}#$instance" --target-host "$connect_host" --build-host "localhost"
'';
}

View file

@ -0,0 +1,54 @@
{ pkgs, ... }:
let
image = pkgs.fetchurl {
url = "https://cdimage.debian.org/images/cloud/bookworm/20250703-2162/debian-12-genericcloud-amd64-20250703-2162.qcow2";
hash = "sha256-hfcP4INSfyP90f9DWDqMLXsZa7teXMhePwK6Vyk+dG4=";
};
script = pkgs.writeShellApplication {
name = "upload-openstack-base-debian-image-wrapped";
runtimeInputs = [
pkgs.openstackclient
];
text = ''
export OS_AUTH_URL="''${TF_VAR_openstack_auth_url:-}"
# With the addition of Keystone we have standardized on the term **project**
# as the entity that owns the resources.
# export OS_PROJECT_ID=4840ee3bdd0e4285bc46586b8e14aafb
export OS_PROJECT_NAME="khs"
export OS_USER_DOMAIN_NAME="Default"
export OS_PROJECT_DOMAIN_ID="default"
# In addition to the owning entity (tenant), OpenStack stores the entity
# performing the action as the **user**.
export OS_USERNAME="''${TF_VAR_openstack_username:-}"
export OS_PASSWORD="''${TF_VAR_openstack_password:-}"
# If your configuration has multiple regions, we set that information here.
# OS_REGION_NAME is optional and only valid in certain environments.
export OS_REGION_NAME="''${TF_VAR_openstack_region:-}"
export OS_INTERFACE="''${TF_VAR_endpoint_type:-}"
export OS_IDENTITY_API_VERSION=3
openstack image create \
--container-format bare \
--disk-format qcow2 \
--property hw_disk_bus=scsi \
--property hw_scsi_model=virtio-scsi \
--property os_type=linux \
--property os_distro=debian \
--property os_admin_user=debian \
--property os_version='12.0.0' \
--file ${image} \
debian-12
'';
};
in
pkgs.writeShellApplication {
name = "upload-openstack-base-debian-image";
runtimeInputs = [
pkgs.khscodes.openbao-helper
script
];
text = ''
openbao-helper wrap-program -e openstack -- upload-openstack-base-debian-image-wrapped
'';
}

View file

@ -2,6 +2,6 @@
{ {
config.khscodes = { config.khscodes = {
hetzner.enable = true; hetzner.enable = true;
sshd.enable = true; services.openssh.enable = true;
}; };
} }

View file

@ -2,6 +2,6 @@
{ {
config.khscodes = { config.khscodes = {
openstack.enable = true; openstack.enable = true;
sshd.enable = true; services.openssh.enable = true;
}; };
} }

View file

@ -9,6 +9,13 @@
flavor = "m.medium"; flavor = "m.medium";
secretsSource = "vault"; secretsSource = "vault";
}; };
snowfallorg.users.khs.admin = true;
users.users.khs = {
initialPassword = "test";
openssh.authorizedKeys.keys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCqY0FHnWFKfLG2yfgr4qka5sR9CK+EMAhzlHUkaQyWHTKD+G0/vC/fNPyL1VV3Dxc/ajxGuPzVE+mBMoyxazL3EtuCDOVvHJ5CR+MUSEckg/DDwcGHqy6rC8BvVVpTAVL04ByQdwFnpE1qNSBaQLkxaFVdtriGKkgMkc7+UNeYX/bv7yn+APqfP1a3xr6wdkSSdO8x4N2jsSygOIMx10hLyCV4Ueu7Kp8Ww4rGY8j5o7lKJhbgfItBfSOuQHdppHVF/GKYRhdnK6Y2fZVYbhq4KipUtclbZ6O/VYd8/sOO98+LMm7cOX+K35PQjUpYgcoNy5+Sw3CNS/NHn4JvOtTaUEYP7fK6c9LhMULOO3T7Cm6TMdiFjUKHkyG+s2Mu/LXJJoilw571zwuh6chkeitW8+Ht7k0aPV96kNEvTdoXwLhBifVEaChlAsLAzSUjUq+YYCiXVk0VIXCZQWKj8LoVNTmaqDksWwbcT64fw/FpVC0N18WHbKcFUEIW/O4spJMa30CQwf9FeqpoWoaF1oRClCSDPvX0AauCu0JcmRinz1/JmlXljnXWbSfm20/V+WyvktlI0wTD0cdpNuSasT9vS77YfJ8nutcWWZKSkCj4R4uHeCNpDTX5YXzapy7FxpM9ANCXLIvoGX7Yafba2Po+er7SSsUIY1AsnBBr8ZoDVw=="
];
};
khscodes.fqdn = "test.kaareskovgaard.net"; khscodes.fqdn = "test.kaareskovgaard.net";
system.stateVersion = "25.05"; system.stateVersion = "25.05";
} }

View file

@ -281,9 +281,9 @@ struct UnifiData {
impl UnifiData { impl UnifiData {
pub fn read_from_env() -> anyhow::Result<Self> { pub fn read_from_env() -> anyhow::Result<Self> {
let username = common::env::read_env("TF_VAR_unifi_username")?; let username = common::env::read_env("UNIFI_USERNAME")?;
let password = common::env::read_env("TF_VAR_unifi_password")?; let password = common::env::read_env("UNIFI_PASSWORD")?;
let url = common::env::read_env("TF_VAR_unifi_url")?; let url = common::env::read_env("UNIFI_API")?;
Ok(Self { Ok(Self {
username, username,
password, password,
@ -298,9 +298,9 @@ impl UnifiData {
pub fn into_env_data(self) -> Vec<(&'static str, String)> { pub fn into_env_data(self) -> Vec<(&'static str, String)> {
vec![ vec![
("TF_VAR_unifi_username", self.username), ("UNIFI_USERNAME", self.username),
("TF_VAR_unifi_password", self.password), ("UNIFI_PASSWORD", self.password),
("TF_VAR_unifi_url", self.url), ("UNIFI_API", self.url),
] ]
} }
} }