in source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/helpers/preset_computer_helper.py [0:0]
def __init__(self, context: SocaContext, ad_automation_dao: ADAutomationDAO, sender_id: str, request: Dict):
"""
:param context:
:param ad_automation_dao:
:param sender_id: SenderId attribute from SQS Message
:param request: the original request payload envelope
"""
self.context = context
self.ldap_client = ActiveDirectoryClient(self.context.logger())
self.service_account_username, self.service_account_password = (
self.ldap_client.fetch_service_account_credentials()
)
self.ad_automation_dao = ad_automation_dao
self.sender_id = sender_id
self.request = request
self.logger = context.logger('preset-computer')
self.nonce: Optional[str] = None
self.instance_id: Optional[str] = None
self.ec2_instance: Optional[EC2Instance] = None
self.hostname: Optional[str] = None
# Metadata for AD joins
self.aws_account = self.context.config().get_string('cluster.aws.account_id', required=True)
self.cluster_name = self.context.config().get_string('cluster.cluster_name', required=True)
self.aws_region = self.context.config().get_string('cluster.aws.region', required=True)
# parse and validate sender_id and request
payload = Utils.get_value_as_dict('payload', request, {})
# SenderId attribute from SQS message to protect against spoofing.
if Utils.is_empty(sender_id):
raise exceptions.soca_exception(
error_code=errorcodes.AD_AUTOMATION_PRESET_COMPUTER_FAILED,
message='Unable to verify cluster node identity: SenderId is required'
)
# enforce a nonce for an additional layer of protection against spoofing and help tracing
nonce = Utils.get_value_as_string('nonce', payload)
if Utils.is_empty(nonce):
raise exceptions.soca_exception(
error_code=errorcodes.AD_AUTOMATION_PRESET_COMPUTER_FAILED,
message='nonce is required'
)
self.nonce = nonce
# when sent from an EC2 Instance with an IAM Role attached, SenderId is of below format (IAM role ID):
# AROAZKN2GIY65I74VE5YH:i-035b89c7f49714a3e
sender_id_tokens = sender_id.split(':')
if len(sender_id_tokens) != 2:
raise exceptions.soca_exception(
error_code=errorcodes.AD_AUTOMATION_PRESET_COMPUTER_FAILED,
message='Unable to verify cluster node identity: Invalid SenderId'
)
instance_id = sender_id_tokens[1]
try:
ec2_instances = self.context.aws_util().ec2_describe_instances(filters=[
{
'Name': 'instance-id',
'Values': [instance_id]
},
{
'Name': 'instance-state-name',
'Values': ['running']
}
])
except botocore.exceptions.ClientError as e:
error_code = str(e.response['Error']['Code'])
if error_code.startswith('InvalidInstanceID'):
raise exceptions.soca_exception(
error_code=errorcodes.AD_AUTOMATION_PRESET_COMPUTER_FAILED,
message=f'Unable to verify cluster node identity: Invalid InstanceId - {instance_id}'
)
else:
# for all other errors, retry
raise e
if len(ec2_instances) == 0:
raise exceptions.soca_exception(
error_code=errorcodes.AD_AUTOMATION_PRESET_COMPUTER_FAILED,
message=f'Unable to verify cluster node identity: InstanceId = {instance_id} not found'
)
self.instance_id = instance_id
self.ec2_instance = ec2_instances[0]
# for Windows instances, there is no way to fetch the hostname from describe instances API.
# request payload from windows instances will contain hostname. eg. EC2AMAZ-6S29U5P
hostname = Utils.get_value_as_string('hostname', payload)
if Utils.is_empty(hostname):
# Generate and make use of an IDEA hostname
hostname_data = f"{self.aws_region}|{self.aws_account}|{self.cluster_name}|{self.instance_id}"
hostname_prefix = self.context.config().get_string('directoryservice.ad_automation.hostname_prefix', default='IDEA-')
# todo - move to constants
# todo - change this to produce the shake output and take this many chars vs. bytes (hex)
# check the configured hostname_prefix length and how much it leaves us for generating the random portion.
avail_chars = (15 - len(hostname_prefix))
if avail_chars < 4:
raise exceptions.soca_exception(
error_code=errorcodes.AD_AUTOMATION_PRESET_COMPUTER_FAILED,
message=f'{self.log_tag}configured hostname_prefix is too large. Required at least 4 char of random data. Decrease the size of directoryservice.ad_automation.hostname_prefix: {len(hostname_prefix)}'
)
self.logger.info(f'Using hostname information {hostname_data} (configured hostname prefix: [{hostname_prefix}] / len {len(hostname_prefix)} / {avail_chars} chars available for random portion)')
# Take the last n-chars from the resulting shake256 bucket of 256
shake_value = Utils.shake_256(hostname_data, 256)[(avail_chars * -1):]
hostname = f'{hostname_prefix}{shake_value}'.upper()
self.logger.info(f'Generated IDEA hostname / AD hostname of: {hostname} / len {len(hostname)}')
self.hostname = hostname
self.logger.info(f'Using hostname for AD join: {self.hostname}')
self._shell = ShellInvoker(logger=self.logger)
# verify if adcli is installed and available on the system.
which_adcli = self._shell.invoke('command -v adcli', shell=True)
if which_adcli.returncode != 0:
raise exceptions.general_exception('unable to locate adcli on system to initialize PresetComputerHelper')
self.ADCLI = which_adcli.stdout
# Currently adcli uses the GSSAPI SASL mechanism for LDAP authentication and to establish encryption.
# This should satisfy all requirements set on the server side and LDAPS should only be used if the LDAP
# port is not accessible due to firewalls or other reasons. Note that connection to the domain controller
# with LDAPS requires the latest version of adcli (>=0.9.1), which is not available in the Amazon Linux Core x86_64 repo.
# Only uncomment the lines below to force the LDAPS connection if required:
# ldap_connection_uri = self.context.config().get_string('directoryservice.ldap_connection_uri', required=True)
# self.use_ldaps = ldap_connection_uri.startswith('ldaps:')
self.use_ldaps = False
# initialize domain controller IP addresses
self._domain_controller_ips = self.get_domain_controller_ip_addresses()