Newer
Older
Varac
committed
"""Contains code for managing the files related to an OpenAppStack cluster."""
import logging
import os
import yaml

Maarten de Waard
committed
import greenhost_cloud

Maarten de Waard
committed
from openappstack import ansible
CLUSTER_PATH = os.path.join(os.getcwd(), 'clusters')
log = logging.getLogger(__name__) # pylint: disable=invalid-name
"""Current possible RAM/VCPUs combinations:
512 MiB / 1 CPU core
1 GiB / 1 CPU core
2 GiB / 1 CPU core
3 GiB / 2 CPU cores
4 GiB / 2 CPU cores
6 GiB / 3 CPU cores
8 GiB / 4 CPU cores
12 GiB / 6 CPU cores
16 GiB / 8 CPU cores
"""
"""Greenhost region where VPS will be started with create_droplet"""
DEFAULT_REGION = 'ams1'
"""Default disk size"""
"""Default "image" (operating system): 19 = Debian buster-x64 """
DEFAULT_IMAGE = 19
Varac
committed
class Cluster:
"""
Helper class for cluster-related paths, files, etc.
:param str cluster_name: Identifier of the cluster. A folder in
CLUSTER_PATH will be creaeted with this name.
:param bool load_data: If this is true, `load_data` function is called at
the end of the constructor.
"""
def __init__(self, cluster_name, load_data=False):
self.name = cluster_name
self.cluster_dir = os.path.join(CLUSTER_PATH, cluster_name)
self.ip_address = None
self.hostname = None
self.domain = None
# Set this to False if the data needs to be (re)loaded from file
if load_data:
self.load_data()
# Can be used to use a custom disk image.
self.disk_image_id = DEFAULT_IMAGE
self.docker_mirror_endpoint = None
self.docker_mirror_username = None
self.docker_mirror_password = None
Loads cluster data from inventory.yml and files
Set self.data_loaded to False if this function should re-read data
if not self.data_loaded:
with open(self.inventory_file, 'r') as stream:
inventory = yaml.safe_load(stream)
# Work with the master node from the inventory
self.hostname = inventory['all']['children']['master']['hosts']
self.domain = \
inventory['all']['hosts'][self.hostname]['domain']
inventory['all']['hosts'][self.hostname]['ansible_host']
log.debug("""Read data from inventory:
ip address: %s
domain: %s
hostname: %s""", self.ip_address, self.domain, self.hostname)
log.debug('Not loading cluster data from file. Set '
'Cluster.data_loaded to False if you want a reload.')
Varac
committed
def create_droplet(self, ssh_key_id=0, hostname=None):
"""
Uses the Cosmos API to create a droplet with OAS default spec
:param int ssh_key_id: SSH key ID in Greenhost Cosmos.

Maarten de Waard
committed
:param str hostname: hostname of the droplet created at GH. Defaults to
the cluster name
if hostname is None:
# If hostname is not set use cluster name for it.

Maarten de Waard
committed
hostname = self.name
if hostname == "master":
ram = 12288
else:
ram = DEFAULT_MEMORY_SIZE_MB

Maarten de Waard
committed
droplet = greenhost_cloud.create_droplet(
name=hostname,
droplet_id = droplet['droplet']['id']
log.info('Created droplet id: %s', droplet_id)

Maarten de Waard
committed
greenhost_cloud.wait_for_state(droplet_id, 'running')
self.set_info_by_droplet_id(droplet_id)
def set_info_by_droplet_id(self, droplet_id):
"""
Sets info about the cluster based on the Greenhost VPS id
:param int droplet_id: Droplet ID at Greenhost
"""

Maarten de Waard
committed
droplet = greenhost_cloud.get_droplet(droplet_id)
self.ip_address = droplet['networks']['v4'][0]['ip_address']
self.hostname = droplet['name']
def set_info_by_hostname(self, hostname):
"""
Sets info based on hostname, assuming that the hostname can be found
with the Cosmos API
"""
hostname = r"^{}$".format(hostname)

Maarten de Waard
committed
droplets = greenhost_cloud.get_droplets_by_name(hostname)
if droplets == []:
log.error("Droplet with hostname %s not found", hostname)
sys.exit(3)
self.ip_address = droplets[0]['networks']['v4'][0]['ip_address']
self.hostname = droplets[0]['name']
def set_info_by_ip_and_hostname(self, ip_address, hostname):
"""
Sets info based on hostname and IP address provided by the user. No API
needed
"""
self.ip_address = ip_address
self.hostname = hostname
def write_cluster_files(self):
"""Creates an inventory.yml and dotenv file for the cluster"""
ansible.create_inventory(self)
dotenv_file = """CLUSTER_NAME={name}
CLUSTER_DIR={cluster_dir}
IP_ADDRESS={ip_address}
HOSTNAME={hostname}
KUBECONFIG={cluster_dir}/kube_config_cluster.yml
"""
with open(self.dotenv_file, 'w') as stream:
stream.write(dotenv_file.format(
name=self.name,
cluster_dir=self.cluster_dir,
ip_address=self.ip_address,
hostname=self.hostname,
domain=self.domain,
))
log.info("Created %s", self.dotenv_file)
# Set self.data_loaded to True because the data in the class now
# reflects the data in the file.
self.data_loaded = True
"""Make sure the cluster's file directory exists"""
os.makedirs(self.cluster_dir, exist_ok=True)
@property
def inventory_file(self):
"""Path to the ansible inventory.yml for this cluster"""
return os.path.join(self.cluster_dir, 'inventory.yml')
@property
def dotenv_file(self):
"""Path to the .cluster.env file with relevant environment variables"""
return os.path.join(self.cluster_dir, '.cluster.env')
all_secrets = {
'flux-system': {
'oas-kube-prometheus-stack-variables': ['grafana_admin_password'],
'oas-nextcloud-variables': [
'nextcloud_mariadb_password',
'nextcloud_mariadb_root_password',
'nextcloud_password',
'onlyoffice_jwt_secret',
'onlyoffice_postgresql_password',
'onlyoffice_rabbitmq_password'],
'oas-rocketchat-variables': [
'userbackend_admin_username',
'userbackend_admin_password',
'userbackend_postgres_password',
'wordpress_admin_password',
'wordpress_mariadb_password',
},
'oas': {
'oas-alertmanager-basic-auth': ['pass'],
'oas-prometheus-basic-auth': ['pass']
for namespace, sec in all_secrets.items():
for app, app_secrets in sec.items():
for app_secret in app_secrets:
secret = self.get_password_from_kubernetes(
app,
app_secret,
namespace)
print(f'{app}: {app_secret}={secret}')
def get_password_from_kubernetes(self, secret, key, namespace):
"""
Reads a password from the Kubernetes cluster. Always returns a string,
but returns "password not found" if no password was found.
:param string secret: The name of the secret in the cluster
:param string key: The key inside the secret that contains the base64
encoded password
:param string namespace: The namespace the secret is in
"""
kubeconfig = os.path.join(self.cluster_dir, 'kube_config_cluster.yml')
config.load_kube_config(config_file=kubeconfig)
api = client.CoreV1Api()
try:
secret_data = api.read_namespaced_secret(secret, namespace)
print(f"Secret {secret} not found in namespace '{namespace}'")
return "password not found"
try:
password = secret_data.data[key]
except KeyError:
print(f"Could not get password from secret '{secret}' in namespace"
return "password not found"
return base64.b64decode(password).decode('utf-8')

Maarten de Waard
committed
def print_info(self, args):
"""Writes information about the cluster. Useful for debugging.
:param argparse.Namespace args: If the --ip-address argument is given,
only prints the machine's IP address.
"""
if args.ip_address:
print(self.ip_address)
else:
info_string = """
Cluster "{name}":
- IP address: {ip_address}
- Hostname: {hostname}
- Domain: {domain}
Configuration:
- Inventory file: {inventory_file}
Kubectl:
To use kubectl with this cluster, copy-paste this in your terminal:
export KUBECONFIG={cluster_dir}/kube_config_cluster.yml"""

Maarten de Waard
committed
print(info_string.format(
name=self.name,
ip_address=self.ip_address,
hostname=self.hostname,
domain=self.domain,
inventory_file=self.inventory_file,