diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e20f3ab..61a46a6 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,4 @@ +2.4.0: Support Using Password with Windows Agents. 2.3.5: Support EC2-Classic Security Groups. 2.3.4: Verify resource ID exists on existing resources. 2.3.3: Support connecting ENI to Security Group via relationships. diff --git a/cloudify_awssdk/ec2/decrypt.py b/cloudify_awssdk/ec2/decrypt.py new file mode 100644 index 0000000..edc2776 --- /dev/null +++ b/cloudify_awssdk/ec2/decrypt.py @@ -0,0 +1,59 @@ +# https://github.com/tomrittervg/decrypt-windows-ec2-passwd/blob/master/decrypt-windows-ec2-passwd.py + +import base64 +import binascii + + +def pkcs1_unpad(text): + # From http://kfalck.net/2011/03/07/decoding-pkcs1-padding-in-python + if len(text) > 0 and text[0] == '\x02': + # Find end of padding marked by nul + pos = text.find('\x00') + if pos > 0: + return text[pos + 1:] + return None + + +def long_to_bytes(val, endianness='big'): + # From http://stackoverflow.com/questions/8730927/ + # convert-python-long-int-to-fixed-size-byte-array + + # one (1) hex digit per four (4) bits + try: + # Python < 2.7 doesn't have bit_length =( + width = val.bit_length() + except Exception: + width = len(val.__hex__()[2:-1]) * 4 + + # unhexlify wants an even multiple of eight (8) bits, but we don't + # want more digits than we need (hence the ternary-ish 'or') + width += 8 - ((width % 8) or 8) + + # format width specifier: four (4) bits per hex digit + fmt = '%%0%dx' % (width // 4) + + # prepend zero (0) to the width, to zero-pad the output + s = binascii.unhexlify(fmt % val) + + if endianness == 'little': + # see http://stackoverflow.com/a/931095/309233 + s = s[::-1] + + return s + + +def decrypt_password(rsa_key, password): + # Undo the whatever-they-do to the ciphertext to get the integer + encryptedData = base64.b64decode(password) + ciphertext = int(binascii.hexlify(encryptedData), 16) + + # Decrypt it + plaintext = rsa_key.decrypt(ciphertext) + + # This is the annoying part. long -> byte array + decryptedData = long_to_bytes(plaintext) + # Now Unpad it + unpaddedData = pkcs1_unpad(decryptedData) + + # Done + return unpaddedData diff --git a/cloudify_awssdk/ec2/resources/instances.py b/cloudify_awssdk/ec2/resources/instances.py index d2b63d5..ac68b18 100644 --- a/cloudify_awssdk/ec2/resources/instances.py +++ b/cloudify_awssdk/ec2/resources/instances.py @@ -19,8 +19,10 @@ ''' # Common +from Crypto.PublicKey import RSA from collections import defaultdict import json +import os # Cloudify from cloudify import compute @@ -29,6 +31,8 @@ from cloudify_awssdk.common import decorators, utils from cloudify_awssdk.common.constants import EXTERNAL_RESOURCE_ID from cloudify_awssdk.ec2 import EC2Base +from cloudify_awssdk.ec2.decrypt import decrypt_password + # Boto from botocore.exceptions import ClientError, ParamValidationError @@ -46,6 +50,7 @@ GROUP_TYPE = 'cloudify.nodes.aws.ec2.SecurityGroup' NETWORK_INTERFACE_TYPE = 'cloudify.nodes.aws.ec2.Interface' SUBNET_TYPE = 'cloudify.nodes.aws.ec2.Subnet' +KEY_TYPE = 'cloudify.nodes.aws.ec2.Keypair' GROUPIDS = 'SecurityGroupIds' NETWORK_INTERFACES = 'NetworkInterfaces' SUBNET_ID = 'SubnetId' @@ -145,6 +150,17 @@ def modify_instance_attribute(self, params): self.logger.debug('Response: {0}'.format(res)) return res + def get_password(self, params): + ''' + Modify attribute of AWS EC2 Instances. + ''' + self.logger.debug( + 'Getting {0} password with parameters: {1}'.format( + self.type_name, params)) + res = self.client.get_password_data(**params) + self.logger.debug('Response: {0}'.format(res)) + return res + @decorators.aws_resource(EC2Instances, resource_type=RESOURCE_TYPE) def prepare(ctx, iface, resource_config, **_): @@ -265,6 +281,10 @@ def start(ctx, iface, resource_config, **_): ctx.instance.runtime_properties['ip'] = ip ctx.instance.runtime_properties['public_ip_address'] = pip ctx.instance.runtime_properties['private_ip_address'] = ip + if not _handle_password(iface): + raise OperationRetry( + 'Waiting for {0} ID# {1} password.'.format( + iface.type_name, iface.resource_id)) return elif ctx.operation.retry_number == 0: @@ -412,3 +432,40 @@ def _handle_userdata(existing_userdata): [existing_userdata, install_agent_userdata]) return final_userdata + + +def _handle_password(iface): + if not ctx.node.properties['use_password']: + return True + # Get agent key data. + key_data = ctx.node.properties['agent_config'].get('key') + # If no key_data yet, check to see if + # a Key pair attached via relationship. + if not key_data: + rel = utils.find_rel_by_node_type(ctx.instance, KEY_TYPE) + if rel: + key_data = \ + rel.target.instance.runtime_properties.get( + 'create_response', {}).get('KeyMaterial') + if not key_data: + raise NonRecoverableError( + 'No key_data was provided in agent config property or rel.') + if os.path.exists(key_data): + with open(key_data, 'r') as outfile: + key_data = outfile.readlines() + password_data = iface.get_password( + { + 'InstanceId': ctx.instance.runtime_properties[EXTERNAL_RESOURCE_ID] + } + ) + if not isinstance(password_data, dict): + return False + encrypted_password = password_data.get('PasswordData') + if not encrypted_password: + ctx.logger.error('password_data is {0}'.format(password_data)) + return False + key = RSA.importKey(key_data) + password = decrypt_password(key, encrypted_password) + ctx.instance.runtime_properties['password'] = \ + password + return True diff --git a/plugin.yaml b/plugin.yaml index e57c61c..7334d99 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -2,9 +2,9 @@ plugins: awssdk: executor: central_deployment_agent - source: https://github.com/cloudify-incubator/cloudify-awssdk-plugin/archive/2.3.5.zip + source: https://github.com/cloudify-incubator/cloudify-awssdk-plugin/archive/2.4.0.zip package_name: cloudify-awssdk-plugin - package_version: '2.3.5' + package_version: '2.4.0' data_types: @@ -1767,6 +1767,10 @@ node_types: Tells the deployment to use the public IP (if available) of the resource for Cloudify Agent connections default: false + use_password: + type: boolean + description: Whether to use a password for agent communication. + default: false interfaces: cloudify.interfaces.lifecycle: create: diff --git a/setup.py b/setup.py index 38bd52c..5c5f267 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ setup( name='cloudify-awssdk-plugin', - version='2.3.5', + version='2.4.0', license='LICENSE', packages=find_packages(exclude=['tests*']), description='A Cloudify plugin for AWS',