Exemplo n.º 1
0
def deprovision(instance_id):
    """Destroys an RDS instance.
    """
    # The deprovision endpoint supports both sync and async requests.
    # Ideally this would be async only since the operation is actually
    # async, but in at least v208 (probably some later versions as
    # well) it seems that the cloud controller does not include the
    # accepts_incomplete param in the request.  The last_operation
    # endpoint supports async deprovisions so this should "just work"
    # with either sync or async operations.  The main difference is
    # that the dynamodb reference will be removed here instead of
    # opportunistically by the last_operation endpoint.
    incompletes = bottle.request.query.getone('accepts_incomplete')
    bottle.response.content_type = 'application/json'
    dynamodb = utils.boto3_session(**CONFIG['aws']).resource('dynamodb')
    table = dynamodb.Table(name=CONFIG['dynamodb_table'])
    record = table.get_item(Key={'instance_id': instance_id})
    if 'Item' not in record.keys():
        bottle.response.status = 410
        return json.dumps({})
    record = record.pop('Item')
    record['last_operation'] = 'destroy'
    rds = RDS(DBInstanceIdentifier=record['hostname'], **CONFIG['aws'])
    rds.destroy_instance()
    if incompletes.lower() == 'true':
        bottle.response.status = 202
        table.put_item(Item=record)
    else:
        bottle.response.status = 200
        table.delete_item(Key={'instance_id': instance_id})
    return json.dumps({})
Exemplo n.º 2
0
def create_polling(record):
    """Last operation polling logic for create action.
    """
    bottle.response.content_type = 'application/json'
    try:
        rds = RDS(name=record['hostname'], **CONFIG['aws'])
        filters = {'DBInstanceIdentifier': record['hostname']}
        details = rds.rds_conn.describe_db_instances(**filters)
        details = details['DBInstances'][0]
    except botocore.exceptions.ClientError as e:
        # This exception will be raised if nothing matches the filter.
        if e.response['Error']['Code'] == 'DBInstanceNotFound':
            bottle.response.status = 410
            return json.dumps({})
        else:
            raise
    if details['DBInstanceStatus'] == 'available':
        # If the instance is available, pull the credentials
        # blob out of the record, decrypt it, update it with
        # the new information, encrypt it, and update dynamodb,
        # and return success for instance provision.
        with utils.Crypt(iv=record['iv'],
                         key=CONFIG['encryption_key']) as c:
            creds = c.decrypt(record['credentials'])
        creds = json.loads(creds)
        creds['hostname'] = details['Endpoint']['Address']
        uri = '{0}://{1}:{2}@{3}:{4}/{5}'.format(
            details['Engine'].lower(),
            creds['username'],
            creds['password'],
            creds['hostname'],
            creds['port'],
            creds['db_name']
        )
        creds['uri'] = uri
        with utils.Crypt(iv=record['iv'],
                         key=CONFIG['encryption_key']) as c:
            creds = c.encrypt(json.dumps(creds))
        record['credentials'] = creds
        dynamodb = utils.boto3_session(**CONFIG['aws']).resource('dynamodb')
        table = dynamodb.Table(name=CONFIG['dynamodb_table'])
        table.put_item(Item=record)
        response = {'state': 'succeeded',
                    'description': 'Service Created.'}
        bottle.response.status = 200
        return json.dumps(response)
    else:
        # If the RDS instance is in a state other than available
        # then return the state as 'in progress' and the actual
        # status for the instance in the message.  This will
        # cause the cloud controller to continue polling until
        # the RDS instance is available for use.
        msg = ('RDS Instance is currently in the {0} '
               'state.'.format(details['DBInstanceStatus']))
        response = {'state': 'in progress', 'description': msg}
        bottle.response.status = 200
        return json.dumps(response)
Exemplo n.º 3
0
def unbind(instance_id, binding_id):
    """Unbind the service credentials from the application
    """
    dynamodb = utils.boto3_session(**CONFIG['aws']).resource('dynamodb')
    table = dynamodb.Table(name=CONFIG['dynamodb_table'])
    record = table.get_item(Key={'instance_id': instance_id})
    if 'Item' in record.keys():
        record = record.pop('Item')
        for index, value in enumerate(record['binding_ids']):
            if value == binding_id:
                del record['binding_ids'][index]
                table.put_item(Item=record)
    bottle.response.status = 200
    response = {}
    bottle.response.content_type = 'application/json'
    return json.dumps(response)
Exemplo n.º 4
0
def destroy_polling(record):
    """Last operation polling logic for destroy action.
    """
    rds = RDS(DBInstanceIdentifier=record['hostname'], **CONFIG['aws'])
    if record['hostname'] in rds.get_all_identifiers():
        response = {
            'state': 'in progress',
            'description': 'Destroying service.'
        }
        bottle.response.status = 200
        return json.dumps(response)
    else:
        dynamodb = utils.boto3_session(**CONFIG['aws']).resource('dynamodb')
        table = dynamodb.Table(name=CONFIG['dynamodb_table'])
        table.delete_item(Key={'instance_id': record['instance_id']})
        bottle.response.status = 410
        return json.dumps({})
Exemplo n.º 5
0
def update(instance_id):
    updateable_params = ('AllocatedStorage',)
    incompletes = bottle.request.query.getone('accepts_incomplete')
    bottle.response.content_type = 'application/json'
    if incompletes is None:
        return _abort_async_required()
    data = json.loads(bottle.request.body.read())
    for param in data['parameters'].keys():
        if param not in updateable_params:
            bottle.response.status = 400
            msg = 'Updating of {0} is not supported'.format(param)
            return json.dumps({'description': msg})
    dynamodb = utils.boto3_session(**CONFIG['aws']).resource('dynamodb')
    table = dynamodb.Table(name=CONFIG['dynamodb_table'])
    record = table.get_item(Key={'instance_id': instance_id})
    if 'Item' not in record.keys():
        bottle.response.status = 410
        return json.dumps({})
    else:
        record = record.pop('Item')
    rds = RDS(DBInstanceIdentifier=record['hostname'], **CONFIG['aws'])
    details = rds.db_instance_details()
    if data['parameters']['AllocatedStorage'] <= details['AllocatedStorage']:
        bottle.response.status = 400
        return json.dumps({
            'description': 'Decreasing AllocatedStorage is not supported.'
        })
    rds.update_instance(
        DBInstanceIdentifier=record['hostname'],
        **data['parameters']
    )
    for i in xrange(0,10):
        details = rds.db_instance_details()
        if details['DBInstanceStatus'] != 'available':
            break
        sleep(5)
    else:
        bottle.response.status = 408
        return json.dumps({})
    record['last_operation'] = 'update'
    record['parameters'] = data['parameters']
    table.put_item(Item=record)
    bottle.response.status = 202
    return json.dumps({})
Exemplo n.º 6
0
def bind(instance_id, binding_id):
    """Return credentials for the service to the cloud controller for app
    binding to the service.
    """
    dynamodb = utils.boto3_session(**CONFIG['aws']).resource('dynamodb')
    table = dynamodb.Table(name=CONFIG['dynamodb_table'])
    record = table.get_item(Key={'instance_id': instance_id})
    if 'Item' not in record.keys():
        bottle.response.status = 410
        return json.dumps({})
    record = record.pop('Item')
    if binding_id not in record['binding_ids']:
        record['binding_ids'].append(binding_id)
    table.put_item(Item=record)
    with utils.Crypt(iv=record['iv'], key=CONFIG['encryption_key']) as c:
        creds = json.loads(c.decrypt(record['credentials']))
    bottle.response.status = 201
    bottle.response.content_type = 'application/json'
    return json.dumps({'credentials': creds})
Exemplo n.º 7
0
def last_operation(instance_id):
    """Check on the state of the async provisioning operation
    """
    bottle.response.content_type = 'application/json'
    dynamodb = utils.boto3_session(**CONFIG['aws']).resource('dynamodb')
    table = dynamodb.Table(name=CONFIG['dynamodb_table'])
    record = table.get_item(Key={'instance_id': instance_id})
    if 'Item' not in record.keys():
        bottle.response.status = 410
        return json.dumps({})
    record = record.pop('Item')
    rds = RDS(name=record['hostname'], **CONFIG['aws'])
    if record['last_operation'] == 'create':
        return create_polling(record)
    elif record['last_operation'] == 'create_from_snapshot':
        return create_from_snapshot_polling(record)
    elif record['last_operation'] == 'destroy':
        return destroy_polling(record)
    elif record['last_operation'] == 'update':
        return update_polling(record)
    else:
        # If last operation is in an unknown state return 410.
        bottle.response.status = 410
        return json.dumps({})
Exemplo n.º 8
0
def provision(instance_id):
    """Provisions an RDS instance.
    """
    # TODO: Break this up into more maintainable chunks.
    if bottle.request.content_type != 'application/json':
        bottle.abort(
            415,
            'Unsupported Content-Type: expecting application/json'
        )
    incompletes = bottle.request.query.getone('accepts_incomplete')
    bottle.response.content_type = 'application/json'
    if incompletes is None:
        return _abort_async_required()
    if incompletes.lower() == 'true':
        data = json.loads(bottle.request.body.read())
        for plan in CONFIG['plan_settings']:
            if plan['id'] == data['plan_id']:
                plan_params = dict(plan)
                # Remove the id value from the params so we can just
                # pass the whole dict along to the RDS class.
                del plan_params['id']
                break
        else:
            bottle.response.status = 400
            return json.dumps({'description': 'Plan ID does not exist'})
        rds = RDS(**CONFIG['aws'])
        # Update the rds class instance with the parameters for the
        # plan as defined in the configuration.
        rds.__dict__.update(plan_params)
        # Parse and use extra parameters that have been passed in by
        # the user.
        #
        # TODO: Move allowed_params to config so operator can determine
        #       what they want to allow.
        allowed_params = ['DBName', 'AllocatedStorage']
        if CONFIG['deploy_from_snapshots'] is True:
            allowed_params.append('DBSnapshotIdentifier')
        if 'parameters' in data.keys():
            user_params = dict([
                (k, v) for (k, v) in data['parameters'].items()
                if k in allowed_params
            ])
            rds.__dict__.update(user_params)
        else:
            user_params = {}
        params_to_update = {}
        rds.DBInstanceIdentifier = '-'.join([rds.Engine.lower(), instance_id])
        rds.MasterUserPassword = utils.random_string()
        if rds.DBSnapshotIdentifier is None:
            last_operation = 'create'
            source_snapshot = 'NONE'
            step = 'NONE'
            first_char = random.choice(string.letters)
            rds.MasterUsername = ''.join([
                first_char,
                utils.random_string(15)
            ])
            rds.create_instance()
        else:
            last_operation = 'create_from_snapshot'
            source_snapshot = rds.DBSnapshotIdentifier
            step = 'deploy'
            try:
                snapshot_metadata = rds.snapshot_metadata()
            except botocore.exceptions.ClientError as e:
                if e.response['Error']['Code'] == 'DBSnapshotNotFound':
                    bottle.response.status = 400
                    return json.dumps(
                        {'description': 'Invalid snapshot identifier'}
                    )
                else:
                    raise
            if snapshot_metadata['Engine'] != rds.Engine.lower():
                bottle.response.status = 400
                return json.dumps(
                    {'description': 'Database engine in snapshot differs from '
                                    'database engine in plan settings.'}
                )
            rds.MasterUsername = snapshot_metadata['MasterUsername']
            rds.Port = snapshot_metadata['Port']
            # If the user is requesting a bigger disk than the snapshot
            # was generated from store this parameter so we can change
            # it during the modify operation after initial provisioning.
            if rds.AllocatedStorage > snapshot_metadata['AllocatedStorage']:
                params_to_update['AllocatedStorage'] = rds.AllocatedStorage
            if rds.StorageType != snapshot_metadata['StorageType']:
                params_to_update['StorageType']
            # When deploying from snapshot the security groups is always
            # set to the default security group.  The only way to change
            # it is to modify the instance after provisioning is done.
            # If the security group IDs are provided then they take
            # precedence over the named security groups.  If named
            # groups are provided they will be validated now before the
            # instance is created and stored to be applied after the
            # instance is done with initial bootstrapping.
            if rds.VpcSecurityGroupIds:
                params_to_update['VpcSecurityGroupIds'] = rds.VpcSecurityGroupIds
            else:
                group_ids = rds.validate_security_groups()
                if group_ids[0]:
                    params_to_update['VpcSecurityGroupIds'] = group_ids[1]
                else:
                    bottle.response.status = 400
                    return json.dumps(
                        {'description': 'Invalid AWS security group id'}
                    )
            rds.create_from_snapshot()
        iv = utils.Crypt.generate_iv()
        credentials = {
            'username': rds.MasterUsername,
            'password': rds.MasterUserPassword,
            'hostname': '',
            'port': rds.Port,
            'db_name': rds.DBName,
            'uri': '',
        }
        with utils.Crypt(iv=iv, key=CONFIG['encryption_key']) as c:
            creds = c.encrypt(json.dumps(credentials))
        dynamodb = utils.boto3_session(**CONFIG['aws']).resource('dynamodb')
        table = dynamodb.Table(name=CONFIG['dynamodb_table'])
        record = {
            'instance_id': instance_id,
            'iv': iv,
            'hostname': rds.DBInstanceIdentifier,
            'credentials': creds,
            'engine': rds.Engine,
            'binding_ids': [],
            'parameters': user_params,
            'last_operation': last_operation,
            'source_snapshot': source_snapshot,
            'step': step,
            'params_to_update': params_to_update
        }
        record.update(data)
        table.put_item(Item=record)
    else:
        return _abort_async_required()
    bottle.response.status = 202
    return json.dumps({"dashboard_url": ""})
Exemplo n.º 9
0
def create_from_snapshot_polling(record):
    """Last operation polling logic for create_from_snaphost action.
    """
    bottle.response.content_type = 'application/json'
    try:
        dynamodb = utils.boto3_session(**CONFIG['aws']).resource('dynamodb')
        table = dynamodb.Table(name=CONFIG['dynamodb_table'])
        rds = RDS(name=record['hostname'], **CONFIG['aws'])
        filters = {'DBInstanceIdentifier': record['hostname']}
        details = rds.rds_conn.describe_db_instances(**filters)
        details = details['DBInstances'][0]
    except botocore.exceptions.ClientError as e:
        # This exception will be raised if nothing matches the filter.
        if e.response['Error']['Code'] == 'DBInstanceNotFound':
            bottle.response.status = 410
            return json.dumps({})
        else:
            raise
    if record['step'] == 'deploy':
        if details['DBInstanceStatus'] == 'available':
            params = {'DBInstanceIdentifier': record['hostname']}
            params.update(record['params_to_update'])
            # Get the new password to pass along for modify.
            with utils.Crypt(iv=record['iv'],
                             key=CONFIG['encryption_key']) as c:
                creds = json.loads(c.decrypt(record['credentials']))
            params['MasterUserPassword'] = creds['password']
            rds.update_instance(**params)
            record['step'] = 'modify'
            table.put_item(Item=record)
        msg = ('RDS Instance is currently in the {0} '
               'state.'.format(details['DBInstanceStatus']))
        response = {'state': 'in progress', 'description': msg}
        bottle.response.status = 200
        return json.dumps(response)
    elif record['step'] == 'modify':
        if details['DBInstanceStatus'] == 'available':
            with utils.Crypt(iv=record['iv'],
                             key=CONFIG['encryption_key']) as c:
                creds = json.loads(c.decrypt(record['credentials']))
            creds['hostname'] = details['Endpoint']['Address']
            uri = '{0}://{1}:{2}@{3}:{4}/{5}'.format(
                details['Engine'].lower(),
                creds['username'],
                creds['password'],
                creds['hostname'],
                creds['port'],
                creds['db_name']
            )
            creds['uri'] = uri
            with utils.Crypt(iv=record['iv'],
                             key=CONFIG['encryption_key']) as c:
                creds = c.encrypt(json.dumps(creds))
            record['credentials'] = creds
            record['step'] = 'complete'
            table.put_item(Item=record)
        msg = ('RDS Instance is currently in the {0} '
               'state.'.format(details['DBInstanceStatus']))
        response = {'state': 'in progress', 'description': msg}
        bottle.response.status = 200
        return json.dumps(response)
    elif record['step'] == 'complete':
        response = {'state': 'succeeded', 'description': 'Service Created.'}
        bottle.response.status = 200
        return json.dumps(response)
    else:
        msg = 'The instance failed to provision'
        response = {'state': 'failed', 'description': msg}
        bottle.response.status = 200
        return json.dumps(response)