Skip to content
This repository has been archived by the owner on Jan 2, 2019. It is now read-only.

Commit

Permalink
Add password handling (#148)
Browse files Browse the repository at this point in the history
* support skipping vpc id assignment

Documentation: http://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.ServiceResource.create_security_group

bump version

* Support password
  • Loading branch information
EarthmanT authored Jul 26, 2018
1 parent fc30333 commit ccaa33b
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
59 changes: 59 additions & 0 deletions cloudify_awssdk/ec2/decrypt.py
Original file line number Diff line number Diff line change
@@ -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
57 changes: 57 additions & 0 deletions cloudify_awssdk/ec2/resources/instances.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
'''

# Common
from Crypto.PublicKey import RSA
from collections import defaultdict
import json
import os

# Cloudify
from cloudify import compute
Expand All @@ -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

Expand All @@ -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'
Expand Down Expand Up @@ -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, **_):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
8 changes: 6 additions & 2 deletions plugin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit ccaa33b

Please sign in to comment.