blob: bd24425c68b60bce6f02294d56b9abcad41e0d98 [file] [log] [blame]
import argparse
import itertools
import json
import logging
import os
import re
import shutil
import subprocess
import sys
import time
from abc import abstractmethod
from fabric import Connection
from patchwork.transfers import rsync
from deploy.endpoint_fab import start_deploy
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
logging.basicConfig(level=logging.INFO, format='%(levelname)s-%(message)s')
INITIAL_LOCATION = os.path.dirname(os.path.abspath(__file__))
class TerraformOutputBase:
@property
@abstractmethod
def output_path(self):
pass
@abstractmethod
def write(self, obj):
pass
@abstractmethod
def extract(self):
pass
class LocalStorageOutputProcessor(TerraformOutputBase):
output_path = None
def __init__(self, path):
self.output_path = path
def write(self, obj):
"""Write json string to local file
:param obj: json string
"""
existed_data = {}
if os.path.isfile(self.output_path):
with open(self.output_path, 'r') as fp:
output = fp.read()
if len(output):
existed_data = json.loads(output)
existed_data.update(obj)
with open(self.output_path, 'w') as fp:
json.dump(existed_data, fp)
pass
def extract(self):
"""Extract data from local file
:return: dict
"""
if os.path.isfile(self.output_path):
with open(self.output_path, 'r') as fp:
output = fp.read()
if len(output):
return json.loads(output)
def extract_args(cli_args):
args = []
for key, value in cli_args.items():
if not value:
continue
if type(value) == list:
quoted_list = ['"{}"'.format(item) for item in value]
joined_values = ', '.join(quoted_list)
value = '[{}]'.format(joined_values)
args.append((key, value))
return args
def get_var_args_string(cli_args):
"""Convert dict of cli argument into string
Args:
cli_args: dict of cli arguments
Returns:
str: string of joined key=values
"""
args = extract_args(cli_args)
args = ["-var '{0}={1}'".format(key, value) for key, value in args]
return ' '.join(args)
def get_args_string(cli_args):
"""Convert dict of cli argument into string
Args:
cli_args: dict of cli arguments
Returns:
str: string of joined key=values
"""
args = extract_args(cli_args)
args = ["{0} {1}".format(key, value) for key, value in args]
return ' '.join(args)
class ParamsBuilder:
def __init__(self):
self.__params = []
def add(self, arg_type, name, desc, **kwargs):
default_group = ['all_args']
if isinstance(kwargs.get('group'), str):
default_group.append(kwargs.get('group'))
if isinstance(kwargs.get('group'), (list, tuple)):
default_group.extend(kwargs.get('group'))
parameter = {
'group': default_group,
'name': name,
'props': {
'help': desc,
'type': arg_type,
'default': kwargs.get('default'),
'choices': kwargs.get('choices'),
'nargs': kwargs.get('nargs'),
'action': kwargs.get('action'),
'required': kwargs.get('required'),
}
}
self.__params.append(parameter)
return self
def add_str(self, name, desc, **kwargs):
return self.add(str, name, desc, **kwargs)
def add_bool(self, name, desc, **kwargs):
return self.add(self.str2bool, name, desc, **kwargs)
def add_int(self, name, desc, **kwargs):
return self.add(int, name, desc, **kwargs)
@staticmethod
def str2bool(v):
if isinstance(v, bool):
return v
if v.lower() in ('yes', 'true', 't', 'y', '1'):
return True
elif v.lower() in ('no', 'false', 'f', 'n', '0'):
return False
else:
raise argparse.ArgumentTypeError('Boolean value expected.')
def build(self):
return self.__params
class Console:
@staticmethod
def execute_to_command_line(command):
""" Execute cli command
Args:
command: str cli command
Returns:
str: command result
"""
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True)
while True:
nextline = process.stdout.readline()
print(nextline)
if nextline == '' and process.poll() is not None:
break
if 'error' in nextline.lower():
sys.exit(0)
@staticmethod
def execute(command):
""" Execute cli command
Args:
command: str cli command
Returns:
str: command result
"""
return os.popen(command).read()
@staticmethod
def ssh(ip, name, pkey):
attempt = 0
while attempt < 12:
logging.info('connection attempt {}'.format(attempt))
connection = Connection(
host=ip,
user=name,
connect_kwargs={'key_filename': pkey,
'allow_agent': False,
'look_for_keys': False,
})
try:
connection.run('ls')
return connection
except Exception as ex:
logging.error(ex)
attempt += 1
time.sleep(10)
class TerraformProviderError(Exception):
"""
Raises errors while terraform provision
"""
pass
class TerraformProvider:
def __init__(self, no_color=False):
self.no_color = '-no-color' if no_color else ''
def initialize(self):
"""Initialize terraform
Returns:
bool: init successful
Raises:
TerraformProviderError: if initialization was not succeed
"""
logging.info('terraform init')
terraform_success_init = 'Terraform has been successfully initialized!'
command = 'terraform init {}'.format(self.no_color)
terraform_init_result = Console.execute(command)
logging.info(terraform_init_result)
if terraform_success_init not in terraform_init_result:
raise TerraformProviderError(terraform_init_result)
def validate(self):
"""Validate terraform
Returns:
bool: validation successful
Raises:
TerraformProviderError: if validation status was not succeed
"""
logging.info('terraform validate')
terraform_success_validate = 'Success!'
terraform_validate_result = Console.execute(
'terraform validate {}'.format(self.no_color))
logging.info(terraform_validate_result)
if terraform_success_validate not in terraform_validate_result:
raise TerraformProviderError(terraform_validate_result)
def apply(self, tf_params, cli_args):
"""Run terraform
Args:
tf_params: dict of terraform parameters
cli_args: dict of parameters
Returns:
None
"""
logging.info('terraform apply')
args_str = get_var_args_string(cli_args)
params_str = get_args_string(tf_params)
command = ('terraform apply -auto-approve {} {} {}'
.format(self.no_color, params_str, args_str))
logging.info(command)
Console.execute_to_command_line(command)
def destroy(self, tf_params, cli_args, keep_state_file=False):
"""Destroy terraform
Args:
tf_params: dict of terraform parameters
cli_args: dict of parameters
keep_state_file: Boolean
Returns:
None
"""
logging.info('terraform destroy')
args_str = get_var_args_string(cli_args)
params_str = get_args_string(tf_params)
command = ('terraform destroy -auto-approve {} {} {}'
.format(self.no_color, params_str, args_str))
logging.info(command)
Console.execute_to_command_line(command)
if not keep_state_file:
state_file = tf_params['-state']
state_file_backup = tf_params['-state'] + '.backup'
if os.path.isfile(state_file):
os.remove(state_file)
if os.path.isfile(state_file_backup):
os.remove(state_file_backup)
@staticmethod
def output(tf_params, *args):
"""Get terraform output
Args:
tf_params: dict of terraform parameters
*args: list of str parameters
Returns:
str: terraform output result
"""
params = get_args_string(tf_params)
return Console.execute('terraform output {} {}'
.format(params, ' '.join(args)))
class AbstractDeployBuilder:
def __init__(self):
args = self.parse_args()
self.service_args = args.get('service')
self.no_color = self.service_args.get('no_color')
state_dir = self.service_args.get('state')
if not state_dir:
self.output_dir = None
self.tf_output = os.path.join(INITIAL_LOCATION, 'output.json')
self.tf_params = {}
else:
if os.path.isdir(state_dir) and os.access(state_dir, os.W_OK):
service_name = (args.get(self.terraform_args_group_name)
.get('service_base_name'))
self.output_dir = (os.path.join(state_dir, service_name))
self.tf_output = os.path.join(self.output_dir, 'output.json')
self.tf_params = {
'-state': os.path.join(
self.output_dir, '{}.tfstate'.format(self.name))
}
else:
sys.stdout.write('path doesn\'t exist')
sys.exit(1)
if self.use_tf_output_file:
self.fill_sys_argv_from_file()
self.terraform_args = self.parse_args().get(
self.terraform_args_group_name)
@property
@abstractmethod
def terraform_location(self):
""" get Terraform location
Returns:
str: TF script location
"""
raise NotImplementedError
@property
@abstractmethod
def name(self):
""" get Terraform name
Returns:
str: TF name
"""
raise NotImplementedError
@property
@abstractmethod
def terraform_args_group_name(self):
""" get Terraform location
Returns:
str: TF script location
"""
raise NotImplementedError
@property
@abstractmethod
def cli_args(self):
"""Get cli arguments
Returns:
dict: dictionary of client arguments
with name as key and props as value
"""
raise NotImplementedError
@abstractmethod
def deploy(self):
"""Post terraform execution
Returns:
None
"""
raise NotImplementedError
@property
def use_tf_output_file(self):
return False
def apply(self):
"""Apply terraform"""
terraform = TerraformProvider(self.no_color)
terraform.apply(self.tf_params, self.terraform_args)
def destroy(self):
"""Destory terraform"""
terraform = TerraformProvider(self.no_color)
terraform.destroy(self.tf_params, self.terraform_args)
def store_output_to_file(self):
"""Extract terraform output and store to file"""
terraform = TerraformProvider(self.no_color)
output = terraform.output(self.tf_params, '-json')
output = {key: value.get('value')
for key, value in json.loads(output).items()}
output_writer = LocalStorageOutputProcessor(self.tf_output)
output_writer.write(output)
def update_extracted_file_data(self, obj):
"""
:param obj:
:return:
Override method if you need to modify extracted from file data
"""
pass
def fill_sys_argv_from_file(self):
"""Extract data from file and fill sys args"""
output_processor = LocalStorageOutputProcessor(self.tf_output)
output = output_processor.extract()
if output:
self.update_extracted_file_data(output)
for key, value in output.items():
key = '--' + key
if key not in sys.argv:
sys.argv.extend([key, value])
else:
try:
index = sys.argv.index(key)
sys.argv[index + 1] = value
except:
pass
def parse_args(self):
"""Get dict of arguments
Returns:
dict: CLI arguments
"""
parsers = {}
args = []
for arg in self.cli_args:
group = arg.get('group')
if isinstance(group, (list, tuple)):
for item in group:
args.append(dict(arg.copy(), **{'group': item}))
else:
args.append(arg)
cli_args = sorted(args, key=lambda x: x.get('group'))
args_groups = itertools.groupby(cli_args, lambda x: x.get('group'))
for group, args in args_groups:
parser = argparse.ArgumentParser()
for arg in args:
parser.add_argument(arg.get('name'), **arg.get('props'))
parsers[group] = parser
return {
group: vars(parser.parse_known_args()[0])
for group, parser in parsers.items()
}
def validate_params(self):
params = self.parse_args()[self.terraform_args_group_name]
if len(params.get('service_base_name')) > 20:
sys.stderr.write('service_base_name length should be less then 20')
sys.exit(1)
if not re.match("^[a-z0-9\-]+$", params.get('service_base_name')):
sys.stderr.write('service_base_name should contain only lowercase '
'alphanumetic characters and hyphens')
sys.exit(1)
def provision(self):
"""Execute terraform script
Returns:
None
Raises:
TerraformProviderError: if init or validate fails
"""
self.validate_params()
tf_location = self.terraform_location
terraform = TerraformProvider(self.no_color)
os.chdir(tf_location)
try:
terraform.initialize()
terraform.validate()
except TerraformProviderError as ex:
raise Exception('Error while provisioning {}'.format(ex))
class AWSK8sSourceBuilder(AbstractDeployBuilder):
def __init__(self):
super(AWSK8sSourceBuilder, self).__init__()
self._args = self.parse_args()
self._ip = None
self._user_name = self.args.get(self.terraform_args_group_name).get(
'os_user')
self._pkey_path = self.args.get('service').get('pkey')
@property
def name(self):
return 'ssn-k8s'
@property
def args(self):
return self._args
@property
def ip(self):
return self._ip
@ip.setter
def ip(self, ip):
self._ip = ip
@property
def user_name(self):
return self._user_name
@property
def pkey_path(self):
return self._pkey_path
@property
def terraform_location(self):
tf_dir = os.path.abspath(os.path.join(os.getcwd(), os.path.pardir))
return os.path.join(tf_dir, 'aws/ssn-k8s/main')
@property
def terraform_args_group_name(self):
return 'k8s'
def validate_params(self):
super(AWSK8sSourceBuilder, self).validate_params()
params = self.parse_args()['all_args']
if params.get('ssn_k8s_masters_count', 1) < 1:
sys.stderr.write('ssn_k8s_masters_count should be greater then 0')
sys.exit(1)
if params.get('ssn_k8s_workers_count', 3) < 3:
sys.stderr.write('ssn_k8s_masters_count should be minimum 3')
sys.exit(1)
# Temporary condition for Jenkins job
if 'endpoint_id' in params and len(params.get('endpoint_id')) > 12:
sys.stderr.write('endpoint_id length should be less then 12')
sys.exit(1)
@property
def cli_args(self):
params = ParamsBuilder()
(params
.add_bool('--no_color', 'no color console_output', group='service',
default=False)
.add_str('--state', 'State file path', group='service')
.add_str('--access_key_id', 'AWS Access Key ID', required=True,
group='k8s')
.add_str('--allowed_cidrs',
'CIDR to allow acces to SSN K8S cluster.',
default=["0.0.0.0/0"], action='append', group='k8s')
.add_str('--ami', 'ID of EC2 AMI.', required=True, group='k8s')
.add_str('--env_os', 'OS type.', default='debian',
choices=['debian', 'redhat'], group=('k8s'))
.add_str('--key_name', 'Name of EC2 Key pair.', required=True,
group='k8s')
.add_str('--os_user', 'Name of DLab service user.',
default='dlab-user', group='k8s')
.add_str('--pkey', 'path to key', required=True, group='service')
.add_str('--region', 'Name of AWS region.', default='us-west-2',
group=('k8s'))
.add_str('--secret_access_key', 'AWS Secret Access Key',
required=True,
group='k8s')
.add_str('--service_base_name',
'Any infrastructure value (should be unique if '
'multiple SSN\'s have been deployed before).',
default='k8s', group=('k8s', 'helm_charts'))
.add_int('--ssn_k8s_masters_count', 'Count of K8S masters.',
default=3,
group='k8s')
.add_int('--ssn_k8s_workers_count', 'Count of K8S workers', default=2,
group=('k8s', 'helm_charts'))
.add_str('--ssn_k8s_masters_shape', 'Shape for SSN K8S masters.',
default='t2.medium', group=('k8s'))
.add_str('--ssn_k8s_workers_shape', 'Shape for SSN K8S workers.',
default='t2.medium', group='k8s')
.add_int('--ssn_root_volume_size', 'Size of root volume in GB.',
default=30, group='k8s')
.add_str('--subnet_cidr_a',
'CIDR for Subnet creation in zone a. Conflicts with subnet_id_a.',
default='172.31.0.0/24', group='k8s')
.add_str('--subnet_cidr_b',
'CIDR for Subnet creation in zone b. Conflicts with subnet_id_b.',
default='172.31.1.0/24', group='k8s')
.add_str('--subnet_cidr_c',
'CIDR for Subnet creation in zone c. Conflicts with subnet_id_c.',
default='172.31.2.0/24', group='k8s')
.add_str('--subnet_id_a',
'ID of AWS Subnet in zone a if you already have subnet created.',
group='k8s')
.add_str('--subnet_id_b',
'ID of AWS Subnet in zone b if you already have subnet created.',
group='k8s')
.add_str('--subnet_id_c',
'ID of AWS Subnet in zone c if you already have subnet created.',
group='k8s')
.add_str('--vpc_cidr', 'CIDR for VPC creation. Conflicts with vpc_id',
default='172.31.0.0/16', group='k8s')
.add_str('--vpc_id', 'ID of AWS VPC if you already have VPC created.',
group='k8s')
.add_str('--zone', 'Name of AWS zone', default='a',
group=('k8s'))
.add_str('--ldap_host', 'ldap host', required=True,
group='helm_charts')
.add_str('--ldap_dn', 'ldap dn', required=True,
group='helm_charts')
.add_str('--ldap_user', 'ldap user', required=True,
group='helm_charts')
.add_str('--ldap_bind_creds', 'ldap bind creds', required=True,
group='helm_charts')
.add_str('--ldap_users_group', 'ldap users group', required=True,
group='helm_charts')
.add_str('--tag_resource_id', 'Tag resource ID.',
default='user:tag', group=('k8s', 'helm_charts'))
.add_str('--additional_tag', 'Additional tag.',
default='product:dlab', group='k8s')
.add_str('--billing_bucket', 'Billing bucket name',
group='helm_charts')
.add_str('--billing_bucket_path',
'The path to billing reports directory in S3 bucket',
default='',
group='helm_charts')
.add_str('--billing_aws_job_enabled',
'Billing format. Available options: true (aws), false(epam)',
default='false',
group='helm_charts')
.add_str('--billing_aws_account_id',
'The ID of Amazon account', default='',
group='helm_charts')
.add_str('--billing_dlab_id',
'Column name in report file that contains dlab id tag',
default='resource_tags_user_user_tag',
group='helm_charts')
.add_str('--billing_usage_date',
'Column name in report file that contains usage date tag',
default='line_item_usage_start_date',
group='helm_charts')
.add_str('--billing_product',
'Column name in report file that contains product name tag',
default='product_product_name',
group='helm_charts')
.add_str('--billing_usage_type',
'Column name in report file that contains usage type tag',
default='line_item_usage_type',
group='helm_charts')
.add_str('--billing_usage',
'Column name in report file that contains usage tag',
default='line_item_usage_amount',
group='helm_charts')
.add_str('--billing_cost',
'Column name in report file that contains cost tag',
default='line_item_blended_cost',
group='helm_charts')
.add_str('--billing_resource_id',
'Column name in report file that contains dlab resource id tag',
default='line_item_resource_id',
group='helm_charts')
.add_str('--billing_tags',
'Column name in report file that contains tags',
default='line_item_operation,line_item_line_item_description',
group='helm_charts')
.add_str('--billing_tag', 'Billing tag', default='dlab',
group='helm_charts')
.add_bool('--custom_certs_enabled', 'Enable custom certificates',
default=False, group=('service', 'helm_charts'))
.add_str('--custom_cert_path', 'custom_cert_path', default='', group=('service', 'helm_charts'))
.add_str('--custom_key_path', 'custom_key_path', default='', group=('service', 'helm_charts'))
.add_str('--custom_certs_host', 'custom certs host', default='', group='helm_charts')
# Tmp for jenkins job
.add_str('--endpoint_id', 'Endpoint Id',
default='user:tag', group=())
)
return params.build()
def check_k8s_cluster_status(self):
""" Check for kubernetes status
Returns:
None
Raises:
TerraformProviderError: if master or kubeDNS is not running
"""
start_time = time.time()
while True:
with Console.ssh(self.ip, self.user_name, self.pkey_path) as c:
k8c_info_status = c.run(
'kubectl cluster-info | '
'sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g"') \
.stdout
kubernetes_success_status = 'Kubernetes master is running'
kubernetes_dns_success_status = 'KubeDNS is running'
kubernetes_succeed = kubernetes_success_status in k8c_info_status
kube_dns_succeed = kubernetes_dns_success_status in k8c_info_status
if kubernetes_succeed and kube_dns_succeed:
break
if (time.time() - start_time) >= 600:
raise TimeoutError
time.sleep(60)
def check_tiller_status(self):
""" Check tiller status
Returns:
None
Raises:
TerraformProviderError: if tiller is not running
"""
start_time = time.time()
with Console.ssh(self.ip, self.user_name, self.pkey_path) as c:
while True:
tiller_status = c.run(
"kubectl get pods --all-namespaces "
"| grep tiller | awk '{print $4}'").stdout
tiller_success_status = 'Running'
if tiller_success_status in tiller_status:
break
if (time.time() - start_time) >= 1200:
raise TimeoutError
time.sleep(60)
def select_master_ip(self):
terraform = TerraformProvider(self.no_color)
output = terraform.output(self.tf_params,
'-json ssn_k8s_masters_ip_addresses')
ips = json.loads(output)
if not ips:
raise TerraformProviderError('no ips')
self.ip = ips[0]
def copy_terraform_to_remote(self):
logging.info('transfer terraform dir to remote')
tf_dir = os.path.abspath(
os.path.join(os.getcwd(), os.path.pardir, os.path.pardir))
source = os.path.join(tf_dir, 'ssn-helm-charts')
remote_dir = '/home/{}/terraform/'.format(self.user_name)
with Console.ssh(self.ip, self.user_name, self.pkey_path) as conn:
conn.run('mkdir -p {}'.format(remote_dir))
rsync(conn, source, remote_dir, strict_host_keys=False)
def copy_cert(self):
logging.info('transfer certificates to remote')
cert_path = self.service_args.get('custom_cert_path')
key_path = self.service_args.get('custom_key_path')
remote_dir = '/tmp/' # .format(self.user_name)
with Console.ssh(self.ip, self.user_name, self.pkey_path) as conn:
conn.run('mkdir -p {}'.format(remote_dir))
rsync(conn, cert_path, remote_dir, strict_host_keys=False)
rsync(conn, key_path, remote_dir, strict_host_keys=False)
def run_remote_terraform(self):
logging.info('apply helm charts')
args = self.parse_args()
# dns_name = json.loads(TerraformProvider(self.no_color)
# .output(self.tf_params,
# '-json ssn_k8s_alb_dns_name'))
nlb_dns_name = json.loads(TerraformProvider(self.no_color)
.output(self.tf_params,
'-json ssn_k8s_nlb_dns_name'))
logging.info('apply ssn-helm-charts')
terraform_args = args.get('helm_charts')
args_str = get_var_args_string(terraform_args)
with Console.ssh(self.ip, self.user_name, self.pkey_path) as conn:
with conn.cd('terraform/ssn-helm-charts/main'):
init = conn.run('terraform init').stdout.lower()
validate = conn.run('terraform validate').stdout.lower()
if 'success' not in init or 'success' not in validate:
raise TerraformProviderError
command = ('terraform apply -auto-approve {} '
'-var \'ssn_k8s_nlb_dns_name={}\''
.format(args_str, nlb_dns_name))
logging.info(command)
conn.run(command)
output = ' '.join(conn.run('terraform output -json')
.stdout.split())
self.fill_args_from_dict(json.loads(output))
def output_terraform_result(self):
# dns_name = json.loads(
# TerraformProvider(self.no_color).output(self.tf_params,
# '-json nginx_load_balancer_hostname'))
ssn_k8s_sg_id = json.loads(
TerraformProvider(self.no_color).output(self.tf_params,
'-json ssn_k8s_sg_id'))
ssn_subnet = json.loads(
TerraformProvider(self.no_color).output(self.tf_params,
'-json ssn_subnet_id'))
ssn_vpc_id = json.loads(
TerraformProvider(self.no_color).output(self.tf_params,
'-json ssn_vpc_id'))
logging.info("""
DLab SSN K8S cluster has been deployed successfully!
Summary:
VPC ID: {}
Subnet ID: {}
SG IDs: {}
""".format(ssn_vpc_id, ssn_subnet, ssn_k8s_sg_id))
def fill_args_from_dict(self, output):
for key, value in output.items():
value = value.get('value')
sys.argv.extend(['--' + key, value])
def fill_remote_terraform_output(self):
with Console.ssh(self.ip, self.user_name, self.pkey_path) as conn:
with conn.cd('terraform/ssn-helm-charts/main'):
output = ' '.join(conn.run('terraform output -json')
.stdout.split())
self.fill_args_from_dict(json.loads(output))
output_processor = LocalStorageOutputProcessor(self.tf_output)
output = {key: value.get('value')
for key, value in json.loads(output).items()}
output_processor.write(output)
@staticmethod
def add_ip_to_known_hosts(ip):
attempt = 0
while attempt < 10:
if len(Console.execute('ssh-keygen -H -F {}'.format(ip))) == 0:
Console.execute(
'ssh-keyscan {} >> ~/.ssh/known_hosts'.format(ip))
attempt += 1
else:
break
def destroy_remote_terraform(self):
logging.info('destroy helm charts')
with Console.ssh(self.ip, self.user_name, self.pkey_path) as conn:
with conn.cd('terraform/ssn-helm-charts/main'):
init = conn.run('terraform init').stdout.lower()
validate = conn.run('terraform validate').stdout.lower()
if 'success' not in init or 'success' not in validate:
raise TerraformProviderError
command = 'terraform destroy -auto-approve'
logging.info(command)
conn.run(command)
def deploy(self):
logging.info('deploy')
output = ' '.join(
TerraformProvider(self.no_color).output(self.tf_params,
'-json').split())
self.fill_args_from_dict(json.loads(output))
self.select_master_ip()
self.add_ip_to_known_hosts(self.ip)
self.check_k8s_cluster_status()
self.check_tiller_status()
self.copy_terraform_to_remote()
if self.service_args.get('custom_certs_enabled'):
self.copy_cert()
self.run_remote_terraform()
self.fill_remote_terraform_output()
self.output_terraform_result()
def destroy(self):
self.select_master_ip()
try:
self.destroy_remote_terraform()
except:
print("Error with destroying helm charts.")
super(AWSK8sSourceBuilder, self).destroy()
if self.output_dir is not None:
shutil.rmtree(self.output_dir)
elif os.path.isfile(os.path.join(INITIAL_LOCATION, 'output.json')):
os.remove(os.path.join(INITIAL_LOCATION, 'output.json'))
class AWSEndpointBuilder(AbstractDeployBuilder):
def update_extracted_file_data(self, obj):
if 'ssn_vpc_id' in obj:
obj['vpc_id'] = obj['ssn_vpc_id']
if 'ssn_subnet_id' in obj:
obj['subnet_id'] = obj['ssn_subnet_id']
@property
def name(self):
return 'endpoint'
@property
def use_tf_output_file(self):
return True
@property
def terraform_location(self):
tf_dir = os.path.abspath(os.path.join(os.getcwd(), os.path.pardir))
return os.path.join(tf_dir, 'aws/endpoint/main')
@property
def terraform_args_group_name(self):
return 'endpoint'
def validate_params(self):
super(AWSEndpointBuilder, self).validate_params()
params = self.parse_args()[self.terraform_args_group_name]
if len(params.get('endpoint_id')) > 12:
sys.stderr.write('endpoint_id length should be less then 12')
sys.exit(1)
@property
def cli_args(self):
params = ParamsBuilder()
(params
.add_bool('--no_color', 'no color console_output', group='service',
default=False)
.add_str('--state', 'State file path', group='service')
.add_str('--secret_access_key', 'AWS Secret Access Key',
required=True,
group='endpoint')
.add_str('--access_key_id', 'AWS Access Key ID', required=True,
group='endpoint')
.add_str('--pkey', 'path to key', required=True, group='service')
.add_str('--service_base_name',
'Any infrastructure value (should be unique if multiple '
'SSN\'s have been deployed before). Should be same as on ssn',
group='endpoint')
.add_str('--vpc_id', 'ID of AWS VPC if you already have VPC created.',
group='endpoint')
.add_str('--vpc_cidr',
'CIDR for VPC creation. Conflicts with vpc_id.',
default='172.31.0.0/16', group='endpoint')
.add_str('--subnet_id',
'ID of Subnet if you already have subnet created.',
group='endpoint')
.add_str('--ssn_k8s_sg_id', 'ID of SSN SG.', group='endpoint')
.add_str('--subnet_cidr',
'CIDR for Subnet creation. Conflicts with subnet_id.',
default='172.31.0.0/24', group='endpoint')
.add_str('--ami', 'ID of AMI.', group='endpoint')
.add_str('--key_name', 'Name of EC2 Key pair.', required=True,
group='endpoint')
.add_str('--endpoint_id', 'Endpoint id.', required=True,
group='endpoint')
.add_str('--region', 'Name of AWS region.', default='us-west-2',
group='endpoint')
.add_str('--zone', 'Name of AWS zone.', default='a', group='endpoint')
.add_str('--network_type',
'Type of created network (if network is not existed and '
'require creation) for endpoint',
default='public', group='endpoint')
.add_str('--endpoint_instance_shape', 'Instance shape of Endpoint.',
default='t2.medium', group='endpoint')
.add_int('--endpoint_volume_size', 'Size of root volume in GB.',
default=30, group='endpoint')
.add_str('--product', 'Product name.', default='dlab',
group='endpoint')
.add_str('--additional_tag', 'Additional tag.',
default='product:dlab', group='endpoint')
.add_str('--ldap_host', 'ldap host', required=True,
group='endpoint')
.add_str('--ldap_dn', 'ldap dn', required=True,
group='endpoint')
.add_str('--ldap_user', 'ldap user', required=True,
group='endpoint')
.add_str('--ldap_bind_creds', 'ldap bind creds', required=True,
group='endpoint')
.add_str('--ldap_users_group', 'ldap users group', required=True,
group='endpoint')
)
return params.build()
def deploy(self):
self.fill_sys_argv_from_file()
new_dir = os.path.abspath(
os.path.join(os.getcwd(), '../../../bin/deploy'))
os.chdir(new_dir)
start_deploy()
class GCPK8sSourceBuilder(AbstractDeployBuilder):
# def update_extracted_file_data(self, obj):
# if 'ssn_vpc_id' in obj:
# obj['vpc_id'] = obj['ssn_vpc_id']
@property
def name(self):
return 'k8s'
@property
def use_tf_output_file(self):
return True
@property
def terraform_location(self):
tf_dir = os.path.abspath(os.path.join(os.getcwd(), os.path.pardir))
return os.path.join(tf_dir, 'gcp/ssn-gke/main')
@property
def terraform_args_group_name(self):
return 'k8s'
def validate_params(self):
super(GCPK8sSourceBuilder, self).validate_params()
# params = self.parse_args()[self.terraform_args_group_name]
# if len(params.get('endpoint_id')) > 12:
# sys.stderr.write('endpoint_id length should be less then 12')
# sys.exit(1)
@property
def cli_args(self):
params = ParamsBuilder()
(params
.add_bool('--no_color', 'no color console_output', group='service',
default=False)
.add_str('--state', 'State file path', group='service')
.add_str('--namespace', 'Name of namespace', group='k8s')
.add_str('--credentials_file_path', 'Path to creds file', group='k8s', required=True)
.add_str('--project_id', 'Project ID', group='k8s', required=True)
.add_str('--region', 'Region name', group='k8s', required=True)
.add_str('--zone', 'Zone name', group='k8s', required=True)
.add_str('--vpc_name', 'VPC name', group='k8s')
.add_str('--subnet_name', 'Subnet name', group='k8s')
.add_str('--service_base_name', 'Service base name', group='k8s', required=True)
.add_str('--subnet_cidr', 'Subnet CIDR', group='k8s')
.add_str('--additional_tag', 'Additional tag', group='k8s')
.add_str('--ssn_k8s_workers_count', 'Number of workers per zone', group='k8s')
.add_str('--gke_cluster_version', 'GKE version', group='k8s')
.add_str('--ssn_k8s_workers_shape', 'Workers shape', group='k8s')
.add_str('--service_account_iam_roles', 'Array of roles', group='k8s')
.add_str('--ssn_k8s_alb_dns_name', 'DNS name', group='k8s')
.add_str('--keycloak_user', 'Keycloak user name', group='k8s')
.add_str('--mysql_user', 'MySQL user name', group='k8s')
.add_str('--mysql_db_name', 'MySQL database name', group='k8s')
.add_str('--ldap_usernameAttr', 'LDAP username attr', group='k8s', default='uid')
.add_str('--ldap_rdnAttr', 'LDAP rdn attr', group='k8s', default='uid')
.add_str('--ldap_uuidAttr', 'LDAP uuid attr', group='k8s', default='uid')
.add_str('--ldap_users_group', 'LDAP users group', group='k8s', default='ou=People')
.add_str('--ldap_dn', 'LDAP DN', group='k8s', default='dc=example,dc=com')
.add_str('--ldap_user', 'LDAP user', group='k8s', default='cn=admin')
.add_str('--ldap_bind_creds', 'LDAP user password', group='k8s', required=True)
.add_str('--ldap_host', 'LDAP host', group='k8s', required=True)
.add_str('--mongo_db_username', 'Mongo user name', group='k8s')
.add_str('--mongo_dbname', 'Mongo database name', group='k8s')
.add_str('--mongo_image_tag', 'Mongo image tag', group='k8s')
.add_str('--mongo_service_port', 'Mongo service port', group='k8s')
.add_str('--mongo_node_port', 'Mongo node port', group='k8s')
.add_str('--mongo_service_name', 'Mongo service name', group='k8s')
.add_str('--env_os', 'Environment Operating system', group='k8s', default='debian')
.add_str('--big_query_dataset', 'Big query dataset name for billing', group='k8s', default='test')
.add_str('--custom_certs_enabled', 'If custom certs enabled', group='k8s')
.add_str('--custom_cert_path', 'Custom cert path', group='k8s')
.add_str('--custom_key_path', 'Custom key path', group='k8s')
.add_str('--custom_certs_host', 'Custom cert host ', group='k8s')
.add_str('--mysql_disk_size', 'MySQL disk size', group='k8s')
.add_str('--domain', 'Domain name', group='k8s', required=True)
)
return params.build()
def apply(self):
terraform = TerraformProvider(self.no_color)
gke_params = self.tf_params.copy()
helm_charts_params = self.tf_params.copy()
gke_params['-target'] = 'module.gke_cluster'
helm_charts_params['-target'] = 'module.helm_charts'
terraform.apply(gke_params, self.terraform_args)
terraform.apply(helm_charts_params, self.terraform_args)
def deploy(self):
pass
def destroy(self):
terraform = TerraformProvider(self.no_color)
gke_params = self.tf_params.copy()
helm_charts_params = self.tf_params.copy()
gke_params['-target'] = 'module.gke_cluster'
helm_charts_params['-target'] = 'module.helm_charts'
terraform.destroy(helm_charts_params, self.terraform_args, True)
time.sleep(60)
terraform.destroy(gke_params, self.terraform_args)
class GCPEndpointBuilder(AbstractDeployBuilder):
def update_extracted_file_data(self, obj):
if 'ssn_vpc_id' in obj:
obj['vpc_id'] = obj['ssn_vpc_id']
@property
def name(self):
return 'endpoint'
@property
def use_tf_output_file(self):
return True
@property
def terraform_location(self):
tf_dir = os.path.abspath(os.path.join(os.getcwd(), os.path.pardir))
return os.path.join(tf_dir, 'gcp/endpoint/main')
@property
def terraform_args_group_name(self):
return 'endpoint'
def validate_params(self):
super(GCPEndpointBuilder, self).validate_params()
params = self.parse_args()[self.terraform_args_group_name]
if len(params.get('endpoint_id')) > 12:
sys.stderr.write('endpoint_id length should be less then 12')
sys.exit(1)
@property
def cli_args(self):
params = ParamsBuilder()
(params
.add_bool('--no_color', 'no color console_output', group='service',
default=False)
.add_str('--state', 'State file path', group='service')
.add_str('--gcp_project_id', 'GCP project ID', required=True, group='endpoint')
.add_str('--creds_file', 'Path to crdes file', required=True, group='endpoint')
.add_str('--pkey', 'path to key', required=True, group='service')
.add_str('--service_base_name', 'Service base name', group='endpoint')
.add_str('--vpc_id', 'ID of VPC if you already have VPC created.', group='endpoint')
.add_str('--subnet_cidr', 'CIDR for Subnet creation. Conflicts with vpc_id.', default='172.31.0.0/24',
group='endpoint')
.add_str('--ssn_subnet', 'ID of AWS Subnet if you already have subnet created.', group='endpoint')
.add_str('--subnet_id', 'ID of subnet', group='endpoint')
.add_str('--ami', 'ID of EC2 AMI.', group='endpoint')
.add_str('--path_to_pub_key', 'Path to public key', required=True, group='endpoint')
.add_str('--endpoint_id', 'Endpoint id.', required=True, group='endpoint')
.add_str('--region', 'Name of region.', group='endpoint')
.add_str('--zone', 'Name of zone.', group='endpoint')
.add_str('--endpoint_shape', 'Instance shape of Endpoint.', group='endpoint')
.add_str('--endpoint_volume_size', 'Endpoint disk size', group='endpoint')
.add_str('--additional_tag', 'Additional tag.', default='product:dlab', group='endpoint')
.add_str('--ldap_host', 'ldap host', required=True, group='endpoint')
.add_str('--ldap_dn', 'ldap dn', required=True, group='endpoint')
.add_str('--ldap_user', 'ldap user', required=True, group='endpoint')
.add_str('--ldap_bind_creds', 'ldap bind creds', required=True, group='endpoint')
.add_str('--ldap_users_group', 'ldap users group', required=True, group='endpoint')
.add_str('--firewall_ing_cidr_range', 'Ingress range', group='endpoint')
.add_str('--firewall_eg_cidr_range', 'Egress range', group='endpoint')
.add_str('--endpoint_policies', 'Endpoint policies list', group='endpoint')
.add_str('--endpoint_roles', 'Endpoint roles list', group='endpoint')
.add_str('--bucket_region', 'Bucket region', group='endpoint')
)
return params.build()
def deploy(self):
self.fill_sys_argv_from_file()
new_dir = os.path.abspath(
os.path.join(os.getcwd(), '../../../bin/deploy'))
os.chdir(new_dir)
start_deploy()
class AzureEndpointBuilder(AbstractDeployBuilder):
def update_extracted_file_data(self, obj):
if 'ssn_vpc_id' in obj:
obj['vpc_id'] = obj['ssn_vpc_id']
@property
def name(self):
return 'endpoint'
@property
def use_tf_output_file(self):
return True
@property
def terraform_location(self):
tf_dir = os.path.abspath(os.path.join(os.getcwd(), os.path.pardir))
return os.path.join(tf_dir, 'azure/endpoint/main')
@property
def terraform_args_group_name(self):
return 'endpoint'
def validate_params(self):
super(AzureEndpointBuilder, self).validate_params()
params = self.parse_args()[self.terraform_args_group_name]
if len(params.get('endpoint_id')) > 12:
sys.stderr.write('endpoint_id length should be less then 12')
sys.exit(1)
@property
def cli_args(self):
params = ParamsBuilder()
(params
.add_bool('--no_color', 'no color console_output', group='service',
default=False)
.add_str('--state', 'State file path', group='service')
.add_str('--auth_file_path', 'Path to crdes file', required=True, group='endpoint')
.add_str('--pkey', 'path to key', required=True, group='service')
.add_str('--service_base_name', 'Service base name', group='endpoint')
.add_str('--resource_group_name', 'Resource group name', group='endpoint')
.add_str('--vpc_id', 'ID of VPC if you already have VPC created.', group='endpoint')
.add_str('--vpc_cidr', 'CIDR for VPC creation. Conflicts with vpc_id.', default='172.31.0.0/16',
group='endpoint')
.add_str('--subnet_cidr', 'CIDR for Subnet creation. Conflicts with vpc_id.', default='172.31.0.0/24',
group='endpoint')
.add_str('--ssn_subnet', 'ID of AWS Subnet if you already have subnet created.', group='endpoint')
.add_str('--subnet_id', 'ID of subnet', group='endpoint')
.add_str('--ami', 'ID of EC2 AMI.', group='endpoint')
.add_str('--key_path', 'Path to public key', required=True, group='endpoint')
.add_str('--endpoint_id', 'Endpoint id.', required=True, group='endpoint')
.add_str('--region', 'Name of region.', group='endpoint')
.add_str('--endpoint_shape', 'Instance shape of Endpoint.', default='Standard_DS2_v2', group='endpoint')
.add_str('--endpoint_volume_size', 'Endpoint disk size', default='30', group='endpoint')
.add_str('--additional_tag', 'Additional tag.', default='product:dlab', group='endpoint')
)
return params.build()
def deploy(self):
self.fill_sys_argv_from_file()
new_dir = os.path.abspath(
os.path.join(os.getcwd(), '../../../bin/deploy'))
os.chdir(new_dir)
start_deploy()
class DeployDirector:
def build(self, action, builder):
""" Do build action
Args:
builder: AbstractDeployBuilder
Returns:
None
"""
try:
builder.provision()
if action == 'deploy':
builder.apply()
builder.store_output_to_file()
builder.deploy()
if action == 'destroy':
builder.destroy()
except Exception as ex:
print(ex)
def deploy():
actions = {'deploy', 'destroy'}
sources_targets = {
'aws': ['k8s', 'endpoint'],
'gcp': ['k8s', 'endpoint'],
'azure': ['endpoint']
}
no_args_error = ('usage: ./dlab {} {} {}\n'.format(
actions,
set(sources_targets.keys()),
set(itertools.chain(*sources_targets.values()))))
no_source_error = (
lambda x: ('usage: ./dlab {} {} {}\n'.format(
x,
set(sources_targets.keys()),
set(itertools.chain(*sources_targets.values())))))
no_target_error = (
lambda x, y: ('usage: ./dlab {} {} {}\n'.format(
x, y, set(itertools.chain(*sources_targets.values())))))
if len(sys.argv) == 1 or sys.argv[1] not in actions:
sys.stderr.write(no_args_error)
exit(1)
if len(sys.argv) == 2 or sys.argv[2] not in sources_targets:
sys.stderr.write(no_source_error(sys.argv[1]))
exit(1)
if len(sys.argv) == 3 or sys.argv[3] not in sources_targets[sys.argv[2]]:
sys.stderr.write(no_target_error(sys.argv[1], sys.argv[2]))
module, action, source, target = sys.argv[:4]
builders_dict = {
'aws': {
'k8s': AWSK8sSourceBuilder,
'endpoint': AWSEndpointBuilder
},
'gcp': {
'k8s': GCPK8sSourceBuilder,
'endpoint': GCPEndpointBuilder
},
'azure': {
'endpoint': AzureEndpointBuilder
}
}
builder = builders_dict[source][target]()
deploy_director = DeployDirector()
deploy_director.build(action, builder)
if __name__ == '__main__':
deploy()