openstack-server
This commit is contained in:
parent
c116ad1631
commit
4a8c25923d
481
scripts/openstack-server
Executable file
481
scripts/openstack-server
Executable file
|
@ -0,0 +1,481 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
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]
|
||||
#error = output_error[1]
|
||||
# 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):
|
||||
tabs += '\t'
|
||||
return tabs
|
||||
|
||||
v6_record = str()
|
||||
v4_record = str()
|
||||
first = True
|
||||
tabs = get_tabs(f"{fqdn}.\t")
|
||||
for address in addresses:
|
||||
if address.find('.') != -1:
|
||||
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()
|
||||
subparsers = parser.add_subparsers(title='subcommands', dest='command')
|
||||
|
||||
# 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",
|
||||
help="Set security group for machine",
|
||||
default="",
|
||||
)
|
||||
create_parser.add_argument(
|
||||
"--sgroup-policy",
|
||||
dest='sgroup_policy',
|
||||
help="Set security group policy for machine",
|
||||
choices=[
|
||||
'affinity', 'anti-affinity', 'soft-affinity', 'soft-anti-affinity'
|
||||
],
|
||||
default="anti-affinity",
|
||||
)
|
||||
create_parser.add_argument(
|
||||
"--volume-size",
|
||||
dest='volume_size',
|
||||
help="Set volume size in GB for machine",
|
||||
default="100",
|
||||
)
|
||||
|
||||
create_parser.add_argument(
|
||||
"-k",
|
||||
"--key",
|
||||
help=
|
||||
"SSH key to use, either the path to a public ssh key OR the name of an existing key in openstack",
|
||||
required=True,
|
||||
)
|
||||
create_parser.add_argument(
|
||||
"--fqdn",
|
||||
help="fqdn of server, MUST follow name standard",
|
||||
required=True)
|
||||
create_parser.add_argument(
|
||||
'--format',
|
||||
choices=['bind', 'json', 'shell', 'table', 'value', 'yaml'],
|
||||
default="json",
|
||||
)
|
||||
|
||||
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,
|
||||
dest='preserve_port',
|
||||
help="Keep the configured port in openstack",
|
||||
default=False,
|
||||
)
|
||||
delete_parser.add_argument(
|
||||
"--preserve-cosmos-config",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
dest='preserve_cosmos_overlay',
|
||||
help="Keep the overlay for this server around",
|
||||
default=False,
|
||||
)
|
||||
delete_parser.add_argument(
|
||||
"--preserve-disk",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
dest='preserve_disk',
|
||||
help="Keep the configured disk in openstack",
|
||||
default=False,
|
||||
)
|
||||
show_parser = subparsers.add_parser("show")
|
||||
show_parser.add_argument(
|
||||
'--format',
|
||||
choices=['bind', 'json', 'shell', 'table', 'value', 'yaml'],
|
||||
default='json',
|
||||
dest='format',
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
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
|
||||
domain, environment, function, hostname, instance, location, number, service = parse_fqdn(
|
||||
fqdn)
|
||||
|
||||
# We will allways put a server in i server group, you can override the default with the --sgroup switch
|
||||
if ('sgroup' not in args) or (args.sgroup == ""):
|
||||
# if it is not set, we will construct a security group name based on other information
|
||||
sgroup = fqdn.replace('.', '-') + "-sgroup"
|
||||
else:
|
||||
sgroup = args.sgroup
|
||||
|
||||
# These options MUST match what is used in main function, or there WILL be dragons
|
||||
options: dict = {
|
||||
'command': args.command,
|
||||
'domain': domain,
|
||||
'environment': environment,
|
||||
'fqdn': fqdn,
|
||||
'function': function,
|
||||
'hostname': hostname,
|
||||
'instance': instance,
|
||||
'location': location,
|
||||
'number': number,
|
||||
'service': service,
|
||||
'sgroup': sgroup
|
||||
}
|
||||
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
|
||||
|
||||
return options
|
||||
|
||||
|
||||
def parse_fqdn(fqdn: str) -> tuple:
|
||||
domain = '.'.join([x for x in fqdn.split('.')[:1]])
|
||||
hostname = fqdn.split('.')[0]
|
||||
instance, location, environment, function, number = hostname.split('-')
|
||||
service = fqdn.split('.')[1]
|
||||
return domain, environment, function, hostname, instance, location, number, service
|
||||
|
||||
|
||||
def run_in_openstack(outer_command: list[str], rc_file: str) -> tuple:
|
||||
|
||||
# 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:
|
||||
proc = subprocess.Popen(inner_command,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True)
|
||||
output_error = proc.communicate()
|
||||
proc.wait()
|
||||
return output_error
|
||||
|
||||
outer_command.insert(0, "openstack")
|
||||
outer_command.insert(0, 'tsocks')
|
||||
|
||||
# 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:
|
||||
string_cmd += ' ' + cmd
|
||||
string_cmd += "'"
|
||||
full_command = shlex.split(string_cmd)
|
||||
return run_command(full_command)
|
||||
|
||||
|
||||
def setup_key(in_key: str, rc_file: str) -> str:
|
||||
if type(in_key) == type(tuple()):
|
||||
key = in_key[0]
|
||||
else:
|
||||
key = in_key
|
||||
# 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:
|
||||
show_keypair_command = ['keypair', 'show', '-f', 'json', key_name]
|
||||
return exists_in_openstack(
|
||||
run_in_openstack(show_keypair_command, rc_file))
|
||||
|
||||
if isfile(key):
|
||||
key_name = str(os.environ.get('USER')) + '-sshkey'
|
||||
pub_key = key
|
||||
create_keypair_command = [
|
||||
'keypair', 'create', '--public-key', pub_key, key_name
|
||||
]
|
||||
keypair_exists = keypair_existence(key_name)
|
||||
if not keypair_exists:
|
||||
run_in_openstack(create_keypair_command, rc_file)
|
||||
else:
|
||||
key_name = key
|
||||
|
||||
keypair_exists = keypair_existence(key_name)
|
||||
if not keypair_exists:
|
||||
# We cannot proceed without a public key
|
||||
print(
|
||||
'{"ERROR": "No keypair exists and I could not create a new one :("}'
|
||||
)
|
||||
sys.exit(3)
|
||||
return key_name
|
||||
|
||||
|
||||
def setup_rc_file(options: dict) -> str:
|
||||
config_basepath = path_join(path_join(os.environ["HOME"], ".config"),
|
||||
"sunet")
|
||||
rc_file = path_join(
|
||||
config_basepath,
|
||||
f"app-cred-{options['service']}-{options['location']}-{options['environment']}-openrc.sh"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
|
||||
def show_server(fqdn: str, rc_file: str, format: str = 'json') -> tuple:
|
||||
show_server_command = ['server', 'show', '-f', format, fqdn]
|
||||
return run_in_openstack(show_server_command, rc_file)
|
||||
|
||||
|
||||
def wait_for_server(fqdn: str,
|
||||
rc_file: str,
|
||||
network: str,
|
||||
timeout=360) -> bool:
|
||||
then = time.time()
|
||||
now = then
|
||||
while then >= (now - timeout):
|
||||
try:
|
||||
output, _ = show_server(fqdn, rc_file)
|
||||
json.loads(output)['addresses'][network]
|
||||
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
|
||||
key: str = options['key']
|
||||
dns_record: bool = False
|
||||
format: str = options['format']
|
||||
if format == 'bind':
|
||||
format = 'json'
|
||||
dns_record = True
|
||||
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']
|
||||
|
||||
key_name: str = setup_key(key, rc_file)
|
||||
|
||||
# FIXME: we don't support setting the portname, we just use the fqdn and append -port to it
|
||||
port_name = options['fqdn'].replace('.', '-') + '-port'
|
||||
|
||||
# Internal helper functions
|
||||
def sgroup_show(sgroup: str) -> tuple:
|
||||
show_sgroup_command = ['server', 'group', 'show', '-f', 'json', sgroup]
|
||||
return run_in_openstack(show_sgroup_command, rc_file)
|
||||
|
||||
# Check if server exists
|
||||
server_output_error = show_server(fqdn, rc_file)
|
||||
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:
|
||||
addresses = json.loads(
|
||||
server_output_error[0])['addresses'][network]
|
||||
print(get_dns_records(addresses, fqdn))
|
||||
|
||||
else:
|
||||
if format == 'json':
|
||||
print(server_output_error[0])
|
||||
else:
|
||||
print(show_server(fqdn, rc_file, format)[0])
|
||||
return 0
|
||||
|
||||
# Server group section
|
||||
sgroup_exists = exists_in_openstack(sgroup_show(sgroup))
|
||||
if not sgroup_exists:
|
||||
create_sgroup_command = [
|
||||
'server', 'group', 'create', '--policy', sgroup_policy, sgroup
|
||||
]
|
||||
run_in_openstack(create_sgroup_command, rc_file)
|
||||
sgroup_output, _ = sgroup_show(sgroup)
|
||||
sgroup_object = json.loads(sgroup_output)
|
||||
sgroup_id = sgroup_object['id']
|
||||
|
||||
# Port section
|
||||
show_port_command = ['port', 'show', '-f', 'json', port_name]
|
||||
port_exists = exists_in_openstack(
|
||||
run_in_openstack(show_port_command, rc_file))
|
||||
if not port_exists:
|
||||
create_port_command = [
|
||||
'port', 'create', '--network', network, port_name
|
||||
]
|
||||
run_in_openstack(create_port_command, rc_file)
|
||||
|
||||
# Finaly set up is done, and we can create server
|
||||
create_server_command = [
|
||||
'server', 'create', '--port', port_name, '--flavor', flavor, '--image',
|
||||
image, '--boot-from-volume', volume_size, '--key-name', key_name,
|
||||
'--hint', f'group={sgroup_id}', fqdn
|
||||
]
|
||||
run_in_openstack(create_server_command, rc_file)
|
||||
timeout = 360
|
||||
succeed = wait_for_server(fqdn, rc_file, network, timeout=timeout)
|
||||
if not succeed:
|
||||
print(f"Wait for server: {fqdn} timed out in {timeout}")
|
||||
return 4
|
||||
|
||||
if dns_record:
|
||||
output, _ = show_server(fqdn, rc_file)
|
||||
addresses = json.loads(output)['addresses'][network]
|
||||
print(get_dns_records(addresses, fqdn))
|
||||
|
||||
else:
|
||||
print(show_server(fqdn, rc_file, format))
|
||||
return 0
|
||||
|
||||
|
||||
def delete(options: dict) -> int:
|
||||
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']
|
||||
|
||||
# Check if server exists
|
||||
server_output_error = show_server(fqdn, rc_file)
|
||||
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
|
||||
|
||||
delete_server_command = ['server', 'delete', fqdn]
|
||||
run_in_openstack(delete_server_command, rc_file)
|
||||
print(f"Deleted server: {fqdn}")
|
||||
|
||||
if not preserve_disk:
|
||||
server_object = json.loads(server_output_error[0])
|
||||
for disk in server_object['volumes_attached']:
|
||||
delete_disk_command = ['volume', 'delete', disk['id']]
|
||||
run_in_openstack(delete_disk_command, rc_file)
|
||||
|
||||
# Port section
|
||||
if not preserve_port:
|
||||
# FIXME: we don't support setting the portname, we just use the fqdn and append -port to it
|
||||
port_name = options['fqdn'].replace('.', '-') + '-port'
|
||||
show_port_command = ['port', 'show', '-f', 'json', port_name]
|
||||
port_exists = exists_in_openstack(
|
||||
run_in_openstack(show_port_command, rc_file))
|
||||
if port_exists:
|
||||
delete_port_command = ['port', 'delete', port_name]
|
||||
run_in_openstack(delete_port_command, rc_file)
|
||||
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:
|
||||
if options['format'] == 'bind':
|
||||
# We assume that only public addresses go in dns
|
||||
addresses = json.loads(
|
||||
show_server(options['fqdn'],
|
||||
options['rc_file'])[0])['addresses']['public']
|
||||
print(get_dns_records(addresses, options['fqdn']))
|
||||
|
||||
else:
|
||||
print(
|
||||
show_server(options['fqdn'], options['rc_file'],
|
||||
options['format']))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
# Make sure we have openstack app credentials matching the naming scheme <service>-<location>-<environment>
|
||||
options = get_options()
|
||||
options['rc_file'] = setup_rc_file(options)
|
||||
command = options['command']
|
||||
if command == 'create':
|
||||
return create(options)
|
||||
if command == 'delete':
|
||||
return delete(options)
|
||||
if command == 'show':
|
||||
show(options)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
Loading…
Reference in a new issue