diff --git a/openappstack/__main__.py b/openappstack/__main__.py index 42284ab1d75ccad545a5ecec9dc1ef320a65065a..720549f936fab601f8dce0dd9ee30eef58ac50bf 100755 --- a/openappstack/__main__.py +++ b/openappstack/__main__.py @@ -22,8 +22,9 @@ import argparse import logging import os import sys +import greenhost_cloud from behave.__main__ import main as behave_main -from openappstack import name, cluster, cosmos, ansible +from openappstack import name, cluster, ansible ALL_TESTS = ['behave'] @@ -190,9 +191,9 @@ def main(): # pylint: disable=too-many-statements,too-many-branches,too-many-lo loglevel = logging.DEBUG if args.verbose else logging.INFO init_logging(log, loglevel) - # Setup logging for cosmos module - log_cosmos = logging.getLogger('cosmos') - init_logging(log_cosmos, loglevel) + # Setup logging for greenhost_cloud module + log_greenhost_cloud = logging.getLogger('greenhost_cloud') + init_logging(log_greenhost_cloud, loglevel) log.debug("Parsed arguments: %s", str(args)) @@ -204,7 +205,7 @@ def main(): # pylint: disable=too-many-statements,too-many-branches,too-many-lo if args.terminate_droplet: # In case none of the subparser's functions have been called, load data clus.load_data() - cosmos.terminate_droplets_by_name('^{}$'.format(clus.hostname)) + greenhost_cloud.terminate_droplets_by_name('^{}$'.format(clus.hostname)) if not hasattr(args, 'func') and not args.terminate_droplet: parser.print_help() @@ -249,9 +250,9 @@ def create(clus, args): # pylint: disable=too-many-branches if args.create_droplet: clus.create_droplet(ssh_key_id=args.ssh_key_id, hostname=args.create_hostname) if args.verbose: - cosmos.list_droplets() + greenhost_cloud.list_droplets() # Wait for ssh - cosmos.wait_for_ssh(clus.ip_address) + greenhost_cloud.wait_for_ssh(clus.ip_address) elif args.droplet_id: clus.set_info_by_droplet_id(args.droplet_id) elif args.ip_address: @@ -269,7 +270,7 @@ def create(clus, args): # pylint: disable=too-many-branches create_domain_records( args.domain, clus.ip_address, subdomain=args.subdomain) if args.verbose: - cosmos.list_domain_records(args.domain) + greenhost_cloud.list_domain_records(args.domain) def install(clus, args): @@ -340,16 +341,16 @@ def create_domain_records(domain, droplet_ip, subdomain=None): if subdomain is None: subdomain_arg = "@" - domain_record = cosmos.create_domain_record( + domain_record = greenhost_cloud.create_domain_record( domain=domain, name=subdomain_arg, - data=droplet_ip, record_type='A', update=True) + data=droplet_ip, record_type='A', update=True, ttl=60) log.info('Domain record: %s', domain_record) subdomain_arg = '*' if subdomain is not None: subdomain_arg += '.' + subdomain - domain_record = cosmos.create_domain_record( + domain_record = greenhost_cloud.create_domain_record( domain=domain, name=subdomain_arg, data=subdomain, record_type='CNAME', update=True) log.info('Domain record: %s', domain_record) diff --git a/openappstack/cluster.py b/openappstack/cluster.py index 51cf07f4f49b1fbd28d0bfae6677acd08cc9f4a6..12880f030b24284eb12b98f80b554ec410356ca9 100644 --- a/openappstack/cluster.py +++ b/openappstack/cluster.py @@ -7,7 +7,8 @@ import random import string import sys import yaml -from openappstack import ansible, cosmos +import greenhost_cloud +from openappstack import ansible CLUSTER_PATH = os.path.join(os.getcwd(), 'clusters') @@ -89,7 +90,7 @@ class Cluster: # Use random generated ID in case we're not running in # gitlab CI and there's no CI_PIPELINE_ID env var hostname = self.name - droplet = cosmos.create_droplet( + droplet = greenhost_cloud.create_droplet( name=hostname, ssh_key_id=ssh_key_id, region=DEFAULT_REGION, @@ -98,7 +99,7 @@ class Cluster: image=DEFAULT_IMAGE) droplet_id = droplet['droplet']['id'] log.info('Created droplet id: %s', droplet_id) - cosmos.wait_for_state(droplet_id, 'running') + 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): @@ -107,7 +108,7 @@ class Cluster: :param int droplet_id: Droplet ID at Greenhost """ - droplet = cosmos.get_droplet(droplet_id) + droplet = greenhost_cloud.get_droplet(droplet_id) self.ip_address = droplet['networks']['v4'][0]['ip_address'] self.hostname = droplet['name'] @@ -117,7 +118,7 @@ class Cluster: with the Cosmos API """ hostname = r"^{}$".format(hostname) - droplets = cosmos.get_droplets_by_name(hostname) + droplets = greenhost_cloud.get_droplets_by_name(hostname) if droplets == []: log.error("Droplet with hostname %s not found", hostname) sys.exit(3) diff --git a/openappstack/cosmos.py b/openappstack/cosmos.py deleted file mode 100755 index 1d2bd36cc6e5db125bc6f3bdab1903edcaf34ebf..0000000000000000000000000000000000000000 --- a/openappstack/cosmos.py +++ /dev/null @@ -1,338 +0,0 @@ -#!/usr/bin/env python3 -"""Python module with helper functions to use the cosmos API.""" - -import json -from datetime import datetime -from datetime import timedelta -import logging -import os -import re -import socket -from time import sleep - -import requests -from tabulate import tabulate -from pytz import timezone - - -# Helper functions -def request_api(resource: str, request_type: str = 'GET', - data: str = ''): - """Query the cosmos API.""" - if 'COSMOS_API_TOKEN' in os.environ: - api_token = os.environ['COSMOS_API_TOKEN'] - else: - raise ValueError('Please export the COSMOS_API_TOKEN ' - 'environment variable.') - - headers = {'Content-Type': 'application/json', - 'Authorization': 'Bearer {0}'.format(api_token)} - api_url_base = 'https://service.greenhost.net/api/v2' - api_url = '{0}/{1}'.format(api_url_base, resource) - - if request_type == 'GET': - response = requests.get(api_url, headers=headers) - elif request_type == 'DELETE': - response = requests.delete(api_url, headers=headers) - elif request_type == 'POST': - response = requests.post( - api_url, headers=headers, data=json.dumps(data)) - elif request_type == 'PUT': - response = requests.put( - api_url, headers=headers, data=json.dumps(data)) - else: - raise ValueError('Specify one of GET/DELETE/POST/PUT as request_type.') - - log.debug('Request: %s, %s, data: %s', - response.url, request_type, data) - log.debug('Response code: %s', response.status_code) - - status_code_ok = [200, 201, 202, 204] - if response.status_code in status_code_ok: - if response.content: - log.debug('Response: %s\n', response.json()) - return json.loads(response.content.decode('utf-8')) - return None - raise requests.HTTPError('WARNING: Got response code ', - response.status_code, response.text) - - -# API calls -def create_domain_record(domain: str, name: str, data: str, - record_type: str = 'A', update: bool = False): - """Create domain record. - - If 'update' is set to True, the record will be updated if it exists. - """ - log.info('Creating domain record') - - record = { - 'name': name, - 'data': data, - 'type': record_type - } - # Check if record exists - existing_record = get_domain_record_by_name(domain=domain, name=name, - record_type=record_type) - if existing_record: - if update: - log.info('Domain record exists - Updating the record.') - response = request_api( - 'domains/%s/records/%s' % (domain, existing_record['id']), - 'PUT', record) - else: - raise ValueError('Domain record exists - Doing nothing,' - 'please use "update=True" to update existing' - 'records.') - else: - log.info('Creating new record.') - response = request_api('domains/%s/records/' % domain, 'POST', record) - - return response['domain_record'] - - -def create_droplet(name: str, ssh_key_id: int, region: str = 'ams1', # pylint: disable=too-many-arguments - size: int = 2048, disk: int = 20, image: int = 18): - """Create a droplet. - - Required values: - - name (str): Name of the droplet - - ssh_key_id (int): ssh key id to add - - Optional values with their default values: - - image (str): 18 (Ubuntu 18.04 x64) - - region (str): 'ams1' (Amsterdam 1) - - size (int): 2048 (2GB RAM) - - disk (int): 20 (20GB disk space) - """ - log.info('Creating droplet') - - data = { - "name": name, - "region": region, - "size": size, - "disk": disk, - "image": image, - "ssh_keys": ssh_key_id - } - response = request_api('droplet', 'POST', data) - return response - - -def delete_domain_record(domain: str, record_id: int): - """Delete a domain record.""" - log.info('Deleting domain record %s', record_id) - response = request_api('domains/{0}/records/{1}'.format(domain, record_id), - 'DELETE') - return response - - -def delete_domain_records_by_name(domain: str, name_regex: str): - r"""Delete all domain records in a given domain matching a regex. - - Examples: - delete_domain_records_by_name('openappstack.net', '^\*.ci-') - delete_domain_records_by_name('openappstack.net', '^ci-') - - """ - all_records = get_domain_records_by_name(domain, name_regex) - for record in all_records: - delete_domain_record(domain, record['id']) - - -def delete_droplet(droplet_id: int): - """Delete a droplet. Droplet needs to be stopped first.""" - log.info('Deleting %s', droplet_id) - response = request_api('droplets/{0}'.format(droplet_id), 'DELETE') - return response - - -def get_domain_record(domain: str, droplet_id: int): - """Get details for given domain record.""" - response = request_api('domains/{0}/records/{1}'.format(domain, droplet_id)) - return response['domain_record'] - - -def get_domain_records(domain: str): - """Get domain records for given domain.""" - response = request_api('domains/{0}/records'.format(domain)) - return response['domain_records'] - - -def get_domain_record_by_name(domain: str, name: str, - record_type: str = 'A'): - """ - Get domain record for given name and type. - - Example: - get_domain_record_by_name(domain='openappstack.net', name='varac-oas') - """ - records = get_domain_records(domain=domain) - matching = None - for record in records: - if record['name'] == name and record['type'] == record_type: - matching = record - break - if not matching: - log.info('No domain record found.') - - return matching - - -def get_domain_records_by_name(domain: str, name_regex: str): - r""" - Get all information about domain records matching a regex in their names. - - Example: - get_domain_records_by_name(name_regex='^ci\d+') - """ - all_records = get_domain_records(domain) - matching = [record for record in all_records - if re.match(name_regex, record['name'])] - return matching - - -def get_droplets(): - """Get all information about all droplets.""" - response = request_api('droplets') - return response['droplets'] - - -def get_droplets_by_name(name_regex: str): - r""" - Get all information about droplets matching a regex in their names. - - Example: - get_droplets_by_name(name_regex='^ci\d+') - """ - all_droplets = get_droplets() - log.debug(all_droplets) - matching = [droplet for droplet in all_droplets - if re.match(name_regex, droplet['name'])] - return matching - - -def get_droplet(droplet_id: int): - """Get information about specified droplet.""" - response = request_api('droplets/{0}'.format(droplet_id)) - return response['droplet'] - - -def list_domain_records(domain: str): - """List domain records for given domain.""" - records = get_domain_records(domain) - - log.debug(json.dumps(records, sort_keys=True, indent=2)) - - table_records = [ - [ - record['id'], record['name'], record['type'], record['data'] - ] for record in records - ] - log.info(tabulate(table_records, - headers=['ID', 'Name', 'Type', 'Data'])) - - -def list_droplets(): - """List all droplets by their ID, Name, IP and state.""" - droplets = get_droplets() - - log.debug(json.dumps(droplets, sort_keys=True, indent=2)) - - table_droplets = [ - [ - droplet['id'], - droplet['name'], - ', '.join([x['ip_address'] for x in droplet['networks']['v4']]), - droplet['status'] - ] - for droplet in droplets] - - log.info(tabulate(table_droplets, - headers=['ID', 'Name', 'IPv4', 'Status'])) - - -def shutdown_droplet(droplet_id: int): - """Shut down specified droplet (through a power_off call).""" - log.info('Shutting down %s', droplet_id) - data = {"type": "power_off"} - response = request_api('droplets/{0}/actions'.format(droplet_id), 'POST', data) - return response - - -def status_droplet(droplet_id: int): - """Get status of specified droplet.""" - response = get_droplet(droplet_id) - return response['status'] - - -def terminate_droplet(droplet_id: int): - """Terminate a droplet by powering it down and deleting it.""" - shutdown_droplet(droplet_id) - wait_for_state(droplet_id, 'stopped') - delete_droplet(droplet_id) - - -def terminate_droplets_by_name(name_regex: str, ndays: int = 0, - domain: str = 'openappstack.net'): - r""" - Terminate droplets matching a regex and for x days older than current day. - - Droplets defined on the env variable NO_TERMINATE_DROPLETS will not be - delated - - Example how to terminate all CI instances: - terminate_old_droplets(name_regex='^ci\d+', ndays=5) - will match i.e 'ci1234' , 'ci1', with a creation time older than 5 days - """ - threshold_time = (datetime.now(tz=timezone('Europe/Stockholm')) - - timedelta(days=ndays)).\ - strftime("%Y-%m-%dT%H:%M:%S+00:00") - all_droplets = get_droplets() - - noterminate_droplets = [] - if 'NO_TERMINATE_DROPLETS' in os.environ: - noterminate_droplets = os.environ['NO_TERMINATE_DROPLETS'].split(',') - - for droplet in all_droplets: - if droplet['name'] not in noterminate_droplets: - if re.match(name_regex, droplet['name']): - if droplet['created_at'] < threshold_time: - delete_domain_records_by_name( - domain, r'^\*.'+droplet['name']) - delete_domain_records_by_name(domain, '^'+droplet['name']) - terminate_droplet(droplet['id']) - - -def wait_for_ssh(droplet_ip: str): - """Wait for ssh to be reachable on port 22.""" - log.info('Waiting for ssh to become available on ip %s', droplet_ip) - - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - while sock.connect_ex((droplet_ip, 22)) != 0: - sleep(1) - - log.info('SSH became available on ip %s', droplet_ip) - - -def wait_for_state(droplet_id: int, state): - """Wait for a droplet to reach a certain state.""" - log.info('Waiting for droplet %s to reach %s state...', droplet_id, state) - status = status_droplet(droplet_id) - log.debug(status) - - while status != state: - sleep(1) - status = status_droplet(droplet_id) - - -# When called from from ipython, setup -# logging to console -try: - __IPYTHON__ # pylint: disable=pointless-statement - log = logging.getLogger() # pylint: disable=invalid-name - log.addHandler(logging.StreamHandler()) - log.setLevel(logging.INFO) -except NameError: - log = logging.getLogger(__name__) # pylint: disable=invalid-name