sysadmin-tools/scripts/openstack-server

601 lines
19 KiB
Text
Raw Normal View History

2024-02-21 13:43:44 +01:00
#!/usr/bin/env python3
import argparse
import json
import os
2024-02-21 13:58:42 +01:00
import re
2024-02-21 13:43:44 +01:00
import shlex
import shutil
import subprocess
import sys
import time
from os.path import isfile
from os.path import join as path_join
import argcomplete
# These are helper functions
# To determine if openstack thinks that a given object exists already
def exists_in_openstack(output_error: tuple) -> bool:
output = output_error[0]
2024-11-06 15:43:01 +01:00
# error = output_error[1]
2024-02-21 13:43:44 +01:00
# FIXME: This is a dumb way to check that an object exists, we should check the error in addition to this
try:
json.loads(output)
except json.decoder.JSONDecodeError:
return False
return True
def get_dns_records(addresses: dict, fqdn: str) -> str:
def get_tabs(string: str) -> str:
num_tabs: int = int(len(string.expandtabs()) / len(f"\t".expandtabs()))
tabs = str()
for _ in range(0, num_tabs):
2024-11-06 15:43:01 +01:00
tabs += "\t"
2024-02-21 13:43:44 +01:00
return tabs
v6_record = str()
v4_record = str()
first = True
tabs = get_tabs(f"{fqdn}.\t")
for address in addresses:
2024-11-06 15:43:01 +01:00
if address.find(".") != -1:
2024-02-21 13:43:44 +01:00
if first:
v4_record = f"{fqdn}.\tA\t{address}\n"
first = False
else:
v4_record += f"{tabs}A\t{address}\n"
else:
v6_record += f"{tabs}AAAA\t{address}\n"
record = v4_record + v6_record
return record.expandtabs()
# This function is responsible for collecting all neccessary information from the user, and provide feedback if something is missing
def get_options() -> dict:
parser = argparse.ArgumentParser()
2024-11-06 15:43:01 +01:00
subparsers = parser.add_subparsers(title="subcommands", dest="command")
2024-02-21 13:43:44 +01:00
# Used with the command "create"
create_parser = subparsers.add_parser("create")
create_parser.add_argument(
"--flavor",
help="Set flavor for machine",
default="b2.c2r8",
)
create_parser.add_argument(
"--image",
help="Set image for machine",
default="debian-12",
)
create_parser.add_argument(
"--network",
help="Set network for machine",
default="public",
)
create_parser.add_argument(
"--sgroup",
2024-02-22 08:31:50 +01:00
help="Set server group for machine",
2024-02-21 13:43:44 +01:00
default="",
)
create_parser.add_argument(
"--sgroup-policy",
2024-11-06 15:43:01 +01:00
dest="sgroup_policy",
2024-02-21 13:43:44 +01:00
help="Set security group policy for machine",
2024-11-06 15:43:01 +01:00
choices=["affinity", "anti-affinity", "soft-affinity", "soft-anti-affinity"],
2024-02-21 13:43:44 +01:00
default="anti-affinity",
)
create_parser.add_argument(
"--volume-size",
2024-11-06 15:43:01 +01:00
dest="volume_size",
2024-02-21 13:43:44 +01:00
help="Set volume size in GB for machine",
default="100",
)
create_parser.add_argument(
"-k",
"--key",
2024-11-06 15:43:01 +01:00
help="SSH key to use, either the path to a public ssh key OR the name of an existing key in openstack",
2024-02-21 13:43:44 +01:00
required=True,
)
create_parser.add_argument(
2024-11-06 15:43:01 +01:00
"--fqdn", help="fqdn of server, MUST follow name standard", required=True
)
2024-02-21 13:43:44 +01:00
create_parser.add_argument(
2024-11-06 15:43:01 +01:00
"--format",
choices=["bind", "json", "shell", "table", "value", "yaml"],
2024-02-21 13:43:44 +01:00
default="json",
)
2024-02-21 13:58:42 +01:00
create_parser.add_argument(
"--namestandard",
help="Name of name standard",
2024-11-06 15:43:01 +01:00
default="default",
dest="namestandard",
)
create_parser.add_argument(
"--with-tsocks",
help="Run with tsocks",
action=argparse.BooleanOptionalAction,
default=False,
dest="with_tsocks",
2024-02-21 13:58:42 +01:00
)
2024-02-21 13:43:44 +01:00
delete_parser = subparsers.add_parser("delete")
delete_parser.add_argument(
"--fqdn",
help="fqdn of server, MUST follow name standard",
required=True,
)
delete_parser.add_argument(
"--preserve-port",
action=argparse.BooleanOptionalAction,
2024-11-06 15:43:01 +01:00
dest="preserve_port",
2024-02-21 13:43:44 +01:00
help="Keep the configured port in openstack",
default=False,
)
delete_parser.add_argument(
"--preserve-cosmos-config",
action=argparse.BooleanOptionalAction,
2024-11-06 15:43:01 +01:00
dest="preserve_cosmos_overlay",
2024-02-21 13:43:44 +01:00
help="Keep the overlay for this server around",
default=False,
)
delete_parser.add_argument(
"--preserve-disk",
action=argparse.BooleanOptionalAction,
2024-11-06 15:43:01 +01:00
dest="preserve_disk",
2024-02-21 13:43:44 +01:00
help="Keep the configured disk in openstack",
default=False,
)
2024-02-21 13:58:42 +01:00
delete_parser.add_argument(
"--namestandard",
help="Name of name standard",
2024-11-06 15:43:01 +01:00
default="default",
dest="namestandard",
)
delete_parser.add_argument(
"--with-tsocks",
help="Run with tsocks",
action=argparse.BooleanOptionalAction,
default=False,
dest="with_tsocks",
2024-02-21 13:58:42 +01:00
)
2024-02-21 13:43:44 +01:00
show_parser = subparsers.add_parser("show")
show_parser.add_argument(
2024-11-06 15:43:01 +01:00
"--format",
choices=["bind", "json", "shell", "table", "value", "yaml"],
default="json",
dest="format",
2024-02-21 13:43:44 +01:00
)
show_parser.add_argument(
"--network",
help="Show addresses for this network for machine",
default="public",
)
show_parser.add_argument(
"--fqdn",
help="fqdn of server, MUST follow name standard",
required=True,
)
2024-02-21 13:58:42 +01:00
show_parser.add_argument(
"--namestandard",
help="Name of name standard",
2024-11-06 15:43:01 +01:00
default="default",
dest="namestandard",
)
show_parser.add_argument(
"--with-tsocks",
help="Run with tsocks",
action=argparse.BooleanOptionalAction,
default=False,
dest="with_tsocks",
2024-02-21 13:58:42 +01:00
)
2024-02-21 13:43:44 +01:00
argcomplete.autocomplete(parser)
args = parser.parse_args()
# This is an opinionated name standard that we follow, and WILL rely on for various automation tasks
fqdn = args.fqdn
2024-11-06 15:43:01 +01:00
domain, environment, function, hostname, instance, location, number, service = (
parse_fqdn(fqdn, args.namestandard)
)
2024-02-21 13:43:44 +01:00
# We will allways put a server in i server group, you can override the default with the --sgroup switch
2024-11-06 15:43:01 +01:00
if ("sgroup" not in args) or (args.sgroup == ""):
2024-02-21 13:43:44 +01:00
# if it is not set, we will construct a security group name based on other information
2024-11-06 15:43:01 +01:00
sgroup = fqdn.replace(".", "-").replace(number, "") + "sgroup"
2024-02-21 13:43:44 +01:00
else:
sgroup = args.sgroup
# These options MUST match what is used in main function, or there WILL be dragons
options: dict = {
2024-11-06 15:43:01 +01:00
"command": args.command,
"domain": domain,
"environment": environment,
"fqdn": fqdn,
"function": function,
"hostname": hostname,
"instance": instance,
"location": location,
"namestandard": args.namestandard,
"number": number,
"service": service,
"sgroup": sgroup,
"with_tsocks": args.with_tsocks,
2024-02-21 13:43:44 +01:00
}
2024-11-06 15:43:01 +01:00
if args.command == "create":
options["format"] = args.format
options["flavor"] = args.flavor
options["image"] = args.image
options["key"] = args.key
options["network"] = args.network
options["sgroup_policy"] = args.sgroup_policy
options["volume_size"] = args.volume_size
if args.command == "delete":
options["preserve_port"] = args.preserve_port
options["preserve_cosmos_overlay"] = args.preserve_cosmos_overlay
options["preserve_disk"] = args.preserve_disk
if args.command == "show":
options["format"] = args.format
2024-02-21 13:43:44 +01:00
return options
2024-11-06 15:43:01 +01:00
2024-02-21 13:58:42 +01:00
def parse_fqdn(fqdn: str, namestandard: str) -> tuple:
2024-11-06 15:43:01 +01:00
if namestandard == "drive":
2024-02-21 13:58:42 +01:00
return parse_drive_fqdn(fqdn)
else:
return parse_default_fqdn(fqdn)
2024-02-21 13:43:44 +01:00
2024-11-06 15:43:01 +01:00
2024-02-21 13:58:42 +01:00
def parse_default_fqdn(fqdn: str) -> tuple:
2024-11-06 15:43:01 +01:00
domain = ".".join([x for x in fqdn.split(".")[:1]])
hostname = fqdn.split(".")[0]
instance, location, environment, function, number = hostname.split("-")
service = fqdn.split(".")[1]
2024-02-21 13:43:44 +01:00
return domain, environment, function, hostname, instance, location, number, service
2024-02-21 13:58:42 +01:00
def parse_drive_fqdn(fqdn: str) -> tuple:
2024-11-06 15:43:01 +01:00
type_regex = r"-*[1-9]*\..*"
server_type = re.sub(type_regex, "", fqdn)
2024-02-21 13:58:42 +01:00
2024-11-06 15:43:01 +01:00
env_regex = r".*(pilot|test).*"
environment = re.sub(env_regex, r"\1", fqdn)
domain = f"drive.{environment}.sunet.se"
customer = "common"
2024-02-21 13:58:42 +01:00
2024-11-06 15:43:01 +01:00
if environment not in ["pilot", "test"]:
environment = "prod"
domain = "drive.sunet.se"
2024-02-21 13:58:42 +01:00
if server_type in ["backup", "intern-db", "node", "redis", "script"]:
2024-11-06 15:43:01 +01:00
customer = fqdn.split(".")[1]
if customer == "drive":
customer = "common"
elif server_type in ["gss", "lookup"]:
2024-02-21 13:58:42 +01:00
customer = server_type
2024-11-06 15:43:01 +01:00
elif server_type == "gssbackup":
customer = "gss"
elif server_type == "lookupbackup":
customer = "lookup"
2024-02-21 13:58:42 +01:00
else:
2024-11-06 15:43:01 +01:00
customer = "common"
hostname = fqdn.split(".")[0]
number = re.sub(r"[-a-z.]", "", hostname)
location = "sto4"
if number == "3":
location = "sto3"
if number == "1":
location = "dco"
return (
domain,
environment,
server_type,
hostname,
customer,
location,
number,
"drive",
)
2024-02-21 13:58:42 +01:00
2024-02-21 13:43:44 +01:00
2024-11-06 15:43:01 +01:00
def run_in_openstack(
outer_command: list[str], rc_file: str, with_tsocks: bool
) -> tuple:
2024-02-21 13:43:44 +01:00
# This function runs the specified command with openstack cli using tsocks.
# FIXME: We should support disabling tsocks with a commandline switch
def run_command(inner_command: list[str]) -> tuple:
2024-11-06 15:43:01 +01:00
proc = subprocess.Popen(
inner_command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
2024-02-21 13:43:44 +01:00
output_error = proc.communicate()
proc.wait()
return output_error
outer_command.insert(0, "openstack")
2024-11-06 15:43:01 +01:00
if with_tsocks:
outer_command.insert(0, "tsocks")
2024-02-21 13:43:44 +01:00
# This is where we actually use the openstack openrc file, and we source it before running the command
string_cmd = f"bash -c 'source {rc_file} &&"
for cmd in outer_command:
2024-11-06 15:43:01 +01:00
string_cmd += " " + cmd
2024-02-21 13:43:44 +01:00
string_cmd += "'"
full_command = shlex.split(string_cmd)
return run_command(full_command)
2024-11-06 15:43:01 +01:00
def setup_key(in_key: str, rc_file: str, with_tsocks: bool) -> str:
2024-02-21 13:43:44 +01:00
if type(in_key) == type(tuple()):
key = in_key[0]
else:
key = in_key
2024-11-06 15:43:01 +01:00
2024-02-21 13:43:44 +01:00
# We deal with public keys first, if the supplied option is a filename we will use that to create a new keypair in openstack if neccessary
# If the key is NOT a file on the local machine, we assume that it is a name of a pre existing key
def keypair_existence(key_name: str) -> bool:
2024-11-06 15:43:01 +01:00
show_keypair_command = ["keypair", "show", "-f", "json", key_name]
2024-02-21 13:43:44 +01:00
return exists_in_openstack(
2024-11-06 15:43:01 +01:00
run_in_openstack(show_keypair_command, rc_file, with_tsocks)
)
2024-02-21 13:43:44 +01:00
if isfile(key):
2024-11-06 15:43:01 +01:00
key_name = str(os.environ.get("USER")) + "-sshkey"
2024-02-21 13:43:44 +01:00
pub_key = key
create_keypair_command = [
2024-11-06 15:43:01 +01:00
"keypair",
"create",
"--public-key",
pub_key,
key_name,
2024-02-21 13:43:44 +01:00
]
keypair_exists = keypair_existence(key_name)
if not keypair_exists:
2024-11-06 15:43:01 +01:00
run_in_openstack(create_keypair_command, rc_file, with_tsocks)
2024-02-21 13:43:44 +01:00
else:
key_name = key
keypair_exists = keypair_existence(key_name)
if not keypair_exists:
# We cannot proceed without a public key
2024-11-06 15:43:01 +01:00
print('{"ERROR": "No keypair exists and I could not create a new one :("}')
2024-02-21 13:43:44 +01:00
sys.exit(3)
return key_name
def setup_rc_file(options: dict) -> str:
2024-11-06 15:43:01 +01:00
config_basepath = path_join(path_join(os.environ["HOME"], ".config"), "sunet")
2024-02-21 13:43:44 +01:00
rc_file = path_join(
config_basepath,
2024-11-06 15:43:01 +01:00
f"app-cred-{options['service']}-{options['location']}-{options['environment']}-openrc.sh",
2024-02-21 13:43:44 +01:00
)
if not isfile(rc_file):
print(
f"You need to download an openrc file from openstack and save it as {rc_file}"
)
sys.exit(2)
return rc_file
2024-11-06 15:43:01 +01:00
def show_server(
fqdn: str, rc_file: str, with_tsocks: bool, format: str = "json"
) -> tuple:
show_server_command = ["server", "show", "-f", format, fqdn]
return run_in_openstack(show_server_command, rc_file, with_tsocks)
2024-02-21 13:43:44 +01:00
2024-11-06 15:43:01 +01:00
def wait_for_server(
fqdn: str, rc_file: str, network: str, with_tsocks: bool, timeout=360
) -> bool:
2024-02-21 13:43:44 +01:00
then = time.time()
now = then
while then >= (now - timeout):
try:
2024-11-06 15:43:01 +01:00
output, _ = show_server(fqdn, rc_file, with_tsocks)
json.loads(output)["addresses"][network]
2024-02-21 13:43:44 +01:00
return True
except KeyError:
time.sleep(3)
now = time.time()
return False
# These are the main functions run by main
def create(options: dict) -> int:
# All of these can be set by command line arguments, but some have defaults and as such are optional.
# At this point they MUST be set after parsing args, otherwise there WILL be bugs
2024-11-06 15:43:01 +01:00
key: str = options["key"]
2024-02-21 13:43:44 +01:00
dns_record: bool = False
2024-11-06 15:43:01 +01:00
format: str = options["format"]
if format == "bind":
format = "json"
2024-02-21 13:43:44 +01:00
dns_record = True
2024-11-06 15:43:01 +01:00
flavor: str = options["flavor"]
fqdn: str = options["fqdn"]
hostname: str = options["hostname"]
image: str = options["image"]
network: str = options["network"]
rc_file: str = options["rc_file"]
sgroup: str = options["sgroup"]
sgroup_policy: str = options["sgroup_policy"]
volume_size: str = options["volume_size"]
with_tsocks: bool = options["with_tsocks"]
key_name: str = setup_key(key, rc_file, with_tsocks)
2024-02-21 13:43:44 +01:00
# FIXME: we don't support setting the portname, we just use the fqdn and append -port to it
2024-11-06 15:43:01 +01:00
port_name = options["fqdn"].replace(".", "-") + "-port"
2024-02-21 13:43:44 +01:00
# Internal helper functions
def sgroup_show(sgroup: str) -> tuple:
2024-11-06 15:43:01 +01:00
show_sgroup_command = ["server", "group", "show", "-f", "json", sgroup]
return run_in_openstack(show_sgroup_command, rc_file, with_tsocks)
2024-02-21 13:43:44 +01:00
# Check if server exists
2024-11-06 15:43:01 +01:00
server_output_error = show_server(fqdn, rc_file, with_tsocks)
2024-02-21 13:43:44 +01:00
server_exists = exists_in_openstack(server_output_error)
# We will bail out early if the server allready exists, and just print out what we know about it
if server_exists:
if dns_record:
2024-11-06 15:43:01 +01:00
addresses = json.loads(server_output_error[0])["addresses"][network]
2024-02-21 13:43:44 +01:00
print(get_dns_records(addresses, fqdn))
else:
2024-11-06 15:43:01 +01:00
if format == "json":
2024-02-21 13:43:44 +01:00
print(server_output_error[0])
else:
2024-11-06 15:43:01 +01:00
print(show_server(fqdn, rc_file, with_tsocks, format)[0])
2024-02-21 13:43:44 +01:00
return 0
# Server group section
sgroup_exists = exists_in_openstack(sgroup_show(sgroup))
if not sgroup_exists:
create_sgroup_command = [
2024-11-06 15:43:01 +01:00
"server",
"group",
"create",
"--policy",
sgroup_policy,
sgroup,
2024-02-21 13:43:44 +01:00
]
2024-11-06 15:43:01 +01:00
run_in_openstack(create_sgroup_command, rc_file, with_tsocks)
2024-02-21 13:43:44 +01:00
sgroup_output, _ = sgroup_show(sgroup)
sgroup_object = json.loads(sgroup_output)
2024-11-06 15:43:01 +01:00
sgroup_id = sgroup_object["id"]
2024-02-21 13:43:44 +01:00
# Port section
2024-11-06 15:43:01 +01:00
show_port_command = ["port", "show", "-f", "json", port_name]
2024-02-21 13:43:44 +01:00
port_exists = exists_in_openstack(
2024-11-06 15:43:01 +01:00
run_in_openstack(show_port_command, rc_file, with_tsocks)
)
2024-02-21 13:43:44 +01:00
if not port_exists:
2024-11-06 15:43:01 +01:00
create_port_command = ["port", "create", "--network", network, port_name]
run_in_openstack(create_port_command, rc_file, with_tsocks)
2024-02-21 13:43:44 +01:00
# Finaly set up is done, and we can create server
create_server_command = [
2024-11-06 15:43:01 +01:00
"server",
"create",
"--port",
port_name,
"--flavor",
flavor,
"--image",
image,
"--boot-from-volume",
volume_size,
"--key-name",
key_name,
"--hint",
f"group={sgroup_id}",
fqdn,
2024-02-21 13:43:44 +01:00
]
2024-11-06 15:43:01 +01:00
run_in_openstack(create_server_command, rc_file, with_tsocks)
2024-02-21 13:43:44 +01:00
timeout = 360
2024-11-06 15:43:01 +01:00
succeed = wait_for_server(fqdn, rc_file, network, with_tsocks, timeout=timeout)
2024-02-21 13:43:44 +01:00
if not succeed:
print(f"Wait for server: {fqdn} timed out in {timeout}")
return 4
if dns_record:
2024-11-06 15:43:01 +01:00
output, _ = show_server(fqdn, rc_file, with_tsocks)
addresses = json.loads(output)["addresses"][network]
2024-02-21 13:43:44 +01:00
print(get_dns_records(addresses, fqdn))
else:
2024-11-06 15:43:01 +01:00
print(show_server(fqdn, rc_file, with_tsocks, format))
2024-02-21 13:43:44 +01:00
return 0
def delete(options: dict) -> int:
2024-11-06 15:43:01 +01:00
fqdn = options["fqdn"]
rc_file = options["rc_file"]
preserve_port = options["preserve_port"]
preserve_cosmos_overlay = options["preserve_cosmos_overlay"]
preserve_disk = options["preserve_disk"]
with_tsocks = options["with_tsocks"]
2024-02-21 13:43:44 +01:00
# Check if server exists
2024-11-06 15:43:01 +01:00
server_output_error = show_server(fqdn, rc_file, with_tsocks)
2024-02-21 13:43:44 +01:00
server_exists = exists_in_openstack(server_output_error)
# We will bail out early if the server does not allready exists
if not server_exists:
print(f"No server called {fqdn} exists.")
return 0
2024-11-06 15:43:01 +01:00
delete_server_command = ["server", "delete", fqdn]
run_in_openstack(delete_server_command, rc_file, with_tsocks)
2024-02-21 13:43:44 +01:00
print(f"Deleted server: {fqdn}")
if not preserve_disk:
server_object = json.loads(server_output_error[0])
2024-11-06 15:43:01 +01:00
for disk in server_object["volumes_attached"]:
delete_disk_command = ["volume", "delete", disk["id"]]
run_in_openstack(delete_disk_command, rc_file, with_tsocks)
2024-02-21 13:43:44 +01:00
# Port section
if not preserve_port:
# FIXME: we don't support setting the portname, we just use the fqdn and append -port to it
2024-11-06 15:43:01 +01:00
port_name = options["fqdn"].replace(".", "-") + "-port"
show_port_command = ["port", "show", "-f", "json", port_name]
2024-02-21 13:43:44 +01:00
port_exists = exists_in_openstack(
2024-11-06 15:43:01 +01:00
run_in_openstack(show_port_command, rc_file, with_tsocks)
)
2024-02-21 13:43:44 +01:00
if port_exists:
2024-11-06 15:43:01 +01:00
delete_port_command = ["port", "delete", port_name]
run_in_openstack(delete_port_command, rc_file, with_tsocks)
2024-02-21 13:43:44 +01:00
print(f"Deleted port: {port_name}")
dir = os.path.dirname(os.path.abspath(__file__))
overlay = path_join(dir, fqdn)
if not preserve_cosmos_overlay and os.path.isdir(overlay):
shutil.rmtree(overlay)
print(f"Deleted overlay: {overlay}")
return 0
def show(options: dict) -> None:
2024-11-06 15:43:01 +01:00
if options["format"] == "bind":
2024-02-21 13:43:44 +01:00
# We assume that only public addresses go in dns
addresses = json.loads(
2024-11-06 15:43:01 +01:00
show_server(options["fqdn"], options["rc_file"], options["with_tsocks"])[0]
)["addresses"]["public"]
print(get_dns_records(addresses, options["fqdn"]))
2024-02-21 13:43:44 +01:00
else:
print(
2024-11-06 15:43:01 +01:00
show_server(
options["fqdn"],
options["rc_file"],
options["with_tsocks"],
options["format"],
)
)
2024-02-21 13:43:44 +01:00
def main() -> int:
# Make sure we have openstack app credentials matching the naming scheme <service>-<location>-<environment>
options = get_options()
2024-11-06 15:43:01 +01:00
options["rc_file"] = setup_rc_file(options)
command = options["command"]
if command == "create":
2024-02-21 13:43:44 +01:00
return create(options)
2024-11-06 15:43:01 +01:00
if command == "delete":
2024-02-21 13:43:44 +01:00
return delete(options)
2024-11-06 15:43:01 +01:00
if command == "show":
2024-02-21 13:43:44 +01:00
show(options)
return 0
if __name__ == "__main__":
sys.exit(main())