<commit_before>## ----------------------------------------------------------------------------

package AwsSum::Amazon::EC2;

use Moose;
use Moose::Util::TypeConstraints;
with qw(
    AwsSum::Service
    AwsSum::Amazon::Service
);

use Carp;
use DateTime;
use List::Util qw( reduce );
use Digest::SHA qw (hmac_sha1_base64 hmac_sha256_base64);
use XML::Simple;
use URI::Escape;
use MIME::Base64;

my $allowed = {
    # From: http://docs.amazonwebservices.com/AWSEC2/latest/DeveloperGuide/index.html?instance-types.html
    'instance-type' => {
        'm1.small'    => 1,
        'm1.large'    => 1,
        'm1.xlarge'   => 1,
        't1.micro'    => 1,
        'c1.medium'   => 1,
        'c1.xlarge'   => 1,
        'm2.xlarge'   => 1,
        'm2.2xlarge'  => 1,
        'm2.4xlarge'  => 1,
        'cc1.4xlarge' => 1,
    },
};

## ----------------------------------------------------------------------------
# setup details needed or pre-determined

# some things required from the user
enum 'SignatureMethod' => qw(HmacSHA1 HmacSHA256);
has 'signature_method'   => ( is => 'rw', isa => 'SignatureMethod', default => 'HmacSHA256' );

# constants
sub version { '2010-08-31' }

# internal helpers
has '_command' => ( is => 'rw', isa => 'HashRef' );

## ----------------------------------------------------------------------------

my $commands = {
    # In order of: http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/index.html?OperationList-query.html

    # Amazon DevPay
    # * ConfirmProductInstance
    # AMIs
    # * CreateImage
    # * DeregisterImage
    # * DescribeImageAttribute
    # * DescribeImages
    # * ModifyImageAttribute
    # Availability Zones and Regions
    # * DescribeAvailabilityZones
    # * DescribeRegions
    # Elastic Block Store
    # * AttachVolume
    # * CreateSnapshot
    # * CreateVolume
    # * DeleteSnapshot
    # * DeleteVolume
    # * DescribeSnapshotAttribute
    # * DescribeSnapshots
    # * DescribeVolumes
    # * DetachVolume
    # * ModifySnapshotAttribute
    # * ResetSnapshotAttribute
    # Elastic IP Addresses
    # * AllocateAddress
    # * AssociateAddress
    # * DescribeAddresses
    # * DisassociateAddress
    # * ReleaseAddress
    # General
    # * GetConsoleOutput
    # Images
    # * RegisterImage
    # * ResetImageAttribute
    # Instances
    # * DescribeInstanceAttribute
    # * DescribeInstances
    # * ModifyInstanceAttribute
    # * RebootInstances
    # * ResetInstanceAttribute
    # * RunInstances
    # * StartInstances
    # * StopInstances
    # * TerminateInstances
    # Key Pairs
    # * CreateKeyPair
    # * DeleteKeyPair
    # * DescribeKeyPairs
    # * ImportKeyPair
    # Monitoring
    # * MonitorInstances
    # * UnmonitorInstances
    # Placement Groups
    # * CreatePlacementGroup
    # * DeletePlacementGroup
    # * DescribePlacementGroups
    # Reserved Instances
    # * DescribeReservedInstances
    # * DescribeReservedInstancesOfferings
    # * PurchaseReservedInstancesOffering
    # Security Groups
    # * AuthorizeSecurityGroupIngress
    # * CreateSecurityGroup
    # * DeleteSecurityGroup
    # * DescribeSecurityGroups
    # * RevokeSecurityGroupIngress
    # Spot Instances
    # * CancelSpotInstanceRequests
    # * CreateSpotDatafeedSubscription
    # * DeleteSpotDatafeedSubscription
    # * DescribeSpotDatafeedSubscription
    # * DescribeSpotInstanceRequests
    # * DescribeSpotPriceHistory
    # * RequestSpotInstances
    # Tags
    # * CreateTags
    # * DeleteTags
    # * DescribeTags
    # Windows
    # * BundleInstance
    # * CancelBundleTask
    # * DescribeBundleTasks
    # * GetPasswordData

    # AMIs
    DescribeImages => {
        name           => 'DescribeImages',
        method         => 'describe_images',
    },

    # Availability Zones and Regions
    DescribeAvailabilityZones => {
        name           => 'DescribeAvailabilityZones',
        method         => 'describe_availability_zones',
    },
    DescribeRegions => {
        name           => 'DescribeRegions',
        method         => 'describe_regions',
    },

    # Elastic Block Store
    DescribeVolumes => {
        name           => 'DescribeVolumes',
        method         => 'describe_volumes',
    },

    # Elastic IP Addresses
    AllocateAddress => {
        name           => 'AllocateAddress',
        method         => 'allocate_address',
    },
    AssociateAddress => {
        name           => 'AssociateAddress',
        method         => 'associate_address',
    },
    DescribeAddresses => {
        name           => 'DescribeAddresses',
        method         => 'describe_addresses',
    },
    DisassociateAddress => {
        name           => 'DisassociateAddress',
        method         => 'disassociate_address',
    },
    ReleaseAddress => {
        name           => 'ReleaseAddress',
        method         => 'release_address',
    },

    # Instances
    DescribeInstanceAttribute => {
        name           => 'DescribeInstanceAttribute',
        method         => 'describe_instance_attribute',
    },
    DescribeInstances => {
        name           => 'DescribeInstances',
        method         => 'describe_instances',
    },
    ModifyInstanceAttribute => {
        name           => 'ModifyInstanceAttribute',
        method         => 'modify_instance_attribute',
    },
    RebootInstances => {
        name           => 'RebootInstances',
        method         => 'reboot_instances',
    },
    RunInstances => {
        name           => 'RunInstances',
        method         => 'run_instances',
    },
    StartInstances => {
        name           => 'StartInstances',
        method         => 'start_instances',
    },
    StopInstances => {
        name           => 'StopInstances',
        method         => 'stop_instances',
    },
    TerminateInstances => {
        name           => 'TerminateInstances',
        method         => 'terminate_instances',
    },

    # Key Pairs
    CreateKeyPair => {
        name           => 'CreateKeyPair',
        method         => 'create_key_pair',
    },
    DeleteKeyPair => {
        name           => 'DeleteKeyPair',
        method         => 'delete_key_pair',
    },
    DescribeKeyPairs => {
        name           => 'DescribeKeyPairs',
        method         => 'describe_key_pairs',
    },
    ImportKeyPair =>  {
        name           => 'ImportKeyPair',
        method         => 'import_key_pair',
    },

    # Reserved Instances
    DescribeReservedInstances => {
        name           => 'DescribeReservedInstances',
        method         => 'describe_reserved_instances',
    },
    DescribeReservedInstancesOfferings => {
        name           => 'DescribeReservedInstancesOfferings',
        method         => 'describe_reserved_instances_offerings',
    },
    PurchaseReservedInstancesOffering => {
        name           => 'PurchaseReservedInstancesOffering',
        method         => 'purchase_reserved_instances_offering',
    },

    # Security Groups
    AuthorizeSecurityGroupIngress => {
        name           => 'AuthorizeSecurityGroupIngress',
        method         => 'authorize_security_group_ingress',
    },
    CreateSecurityGroup => {
        name           => 'CreateSecurityGroup',
        method         => 'create_security_group',
    },
    DeleteSecurityGroup => {
        name           => 'DeleteSecurityGroup',
        method         => 'delete_security_group',
    },
    DescribeSecurityGroups => {
        name           => 'DescribeSecurityGroups',
        method         => 'describe_security_groups',
    },
    RevokeSecurityGroupIngress => {
        name           => 'RevokeSecurityGroupIngress',
        method         => 'revoke_security_group_ingress',
    },
};

## ----------------------------------------------------------------------------
# things to fill in to fulfill AwsSum::Service

sub commands { $commands }
sub verb { 'get' }
sub url {
    my ($self) = @_;

    # From: http://docs.amazonwebservices.com/AWSEC2/latest/DeveloperGuide/index.html?using-query-api.html
    return q{https://ec2.} . $self->region . q{.amazonaws.com/};
}
sub host {
    my ($self) = @_;
    return q{ec2.} . $self->region . q{.amazonaws.com};
}
sub code { 200 }

sub sign {
    my ($self) = @_;

    my $date = DateTime->now( time_zone => 'UTC' )->strftime("%Y-%m-%dT%H:%M:%SZ");

    # add the service params first before signing
    $self->set_param( 'Action', $self->_command->{name} );
    $self->set_param( 'Version', $self->version );
    $self->set_param( 'AWSAccessKeyId', $self->access_key_id );
    $self->set_param( 'Timestamp', $date );
    $self->set_param( 'SignatureVersion', 2 );
    $self->set_param( 'SignatureMethod', $self->signature_method );

    # See: http://docs.amazonwebservices.com/AWSEC2/2010-08-31/DeveloperGuide/index.html?using-query-api.html

    # sign the request (remember this is SignatureVersion '2')
    my $str_to_sign = '';
    $str_to_sign .= uc($self->verb) . "\n";
    $str_to_sign .= $self->host . "\n";
    $str_to_sign .= "/\n";

    my $param = $self->params();
    $str_to_sign .= join('&', map { "$_=" . uri_escape($param->{$_}) } sort keys %$param);

    # sign the $str_to_sign
    my $signature = ( $self->signature_method eq 'HmacSHA1' )
        ? hmac_sha1_base64($str_to_sign, $self->secret_access_key )
        : hmac_sha256_base64($str_to_sign, $self->secret_access_key );
    $self->set_param( 'Signature', $signature . '=' );
}

sub decode {
    my ($self) = @_;

    $self->data( XMLin( $self->res->content() ));
}

## ----------------------------------------------------------------------------
# all our lovely commands

sub describe_images {
    my ($self, $param) = @_;

    $self->set_command( 'DescribeImages' );
    $self->_amazon_add_flattened_array_to_params( 'ImageId', $param->{ImageId} );
    my $data = $self->send();

    # imagesSet, productCodes and blockDeviceMapping
    $self->_fix_hash_to_array( $data->{imagesSet} );
    $self->_fix_hash_to_array( $_->{productCodes} )
        foreach @{$data->{imagesSet}};
    $self->_fix_hash_to_array( $_->{blockDeviceMapping} )
        foreach @{$data->{imagesSet}};
    return $self->data;
}

sub describe_availability_zones {
    my ($self, $param) = @_;

    $self->set_command( 'DescribeAvailabilityZones' );
    $self->region( $param->{Region} ) if $param->{Region};
    return $self->send();
}

sub describe_regions {
    my ($self, $param) = @_;

    $self->set_command( 'DescribeRegions' );
    my $data = $self->send();

    # regionInfo
    $self->_fix_hash_to_array( $data->{regionInfo} );
    return $self->data;
}

sub describe_volumes {
    my ($self, $param) = @_;

    $self->set_command( 'DescribeVolumes' );
    $self->region( $param->{Region} ) if $param->{Region};
    $self->_amazon_add_flattened_array_to_params( 'VolumeId', $param->{VolumeId} );
    $self->_amazon_add_flattened_array_to_params( 'Filter', $param->{Filter} );
    my $data = $self->send();

    # volumeSet
    $self->_fix_hash_to_array( $data->{volumeSet} );
    foreach my $volume ( @{$data->{volumeSet}} ) {
        # attachmentSet
        $self->_fix_hash_to_array( $volume->{attachmentSet} );
    }

    return $self->data;
}

sub allocate_address {
    my ($self, $param) = @_;

    $self->set_command( 'AllocateAddress' );
    $self->region( $param->{Region} ) if $param->{Region};
    return $self->send();
}

sub associate_address {
    my ($self, $param) = @_;

    unless ( defined $param->{PublicIp} ) {
        croak "Provide a 'PublicIp' address to associate to an instance";
    }
    unless ( defined $param->{InstanceId} ) {
        croak "Provide an 'InstanceId' address to associate an address to";
    }

    $self->set_command( 'AssociateAddress' );
    $self->set_param( 'InstanceId', $param->{InstanceId} );
    $self->set_param( 'PublicIp', $param->{PublicIp} );

    return $self->send();
}

sub describe_addresses {
    my ($self, $param) = @_;

    $self->set_command( 'DescribeAddresses' );
    $self->region( $param->{Region} ) if $param->{Region};
    my $data = $self->send();

    # addressesSet
    $self->_fix_hash_to_array( $data->{addressesSet} );
    return $self->data;
}

sub disassociate_address {
    my ($self, $param) = @_;

    unless ( defined $param->{PublicIp} ) {
        croak "Provide a 'PublicIp' address to disassociate";
    }

    $self->set_command( 'DisassociateAddress' );
    $self->set_param( 'PublicIp', $param->{PublicIp} );

    return $self->send();
}

sub release_address {
    my ($self, $param) = @_;

    unless ( defined $param->{PublicIp} ) {
        croak "Provide a 'PublicIp' address to release";
    }

    $self->set_command( 'ReleaseAddress' );
    $self->region( $param->{Region} ) if $param->{Region};
    $self->set_param( 'PublicIp', $param->{PublicIp} );
    return $self->send();
}

sub describe_instance_attribute {
    my ($self, $param) = @_;

    unless ( $self->is_valid_something($param->{InstanceId}) ) {
        croak "Provide an 'InstanceId' to describe";
    }
    # ToDo: check this against a list of valid attributes (for now, just let EC2 tell us we're wrong)
    unless ( $self->is_valid_something($param->{Attribute}) ) {
        croak "Provide an 'InstanceId' to describe";
    }

    $self->set_command( 'DescribeInstanceAttribute' );
    $self->region( $param->{Region} ) if $param->{Region};
    $self->set_param( 'InstanceId', $param->{InstanceId} );
    $self->set_param( 'Attribute', $param->{Attribute} );
    return $self->send();
}

sub describe_instances {
    my ($self, $param) = @_;

    $self->set_command( 'DescribeInstances' );
    $self->region( $param->{Region} ) if $param->{Region};
    $self->_amazon_add_flattened_array_to_params( 'InstanceId', $param->{InstanceId} );
    my $data = $self->send();

    # reservationSet
    $self->_fix_hash_to_array( $data->{reservationSet} );
    foreach my $reservation ( @{$data->{reservationSet}} ) {
        # groupSet
        $self->_fix_hash_to_array( $reservation->{groupSet} );

        # instancesSet
        $self->_fix_hash_to_array( $reservation->{instancesSet} );
        foreach my $instance ( @{$reservation->{instancesSet}} ) {
            # blockDeviceMapping
            $self->_fix_hash_to_array( $instance->{blockDeviceMapping} );
        }
    }

    return $self->data;
}

sub modify_instance_attribute {
    my ($self, $param) = @_;

    unless ( $self->is_valid_something($param->{InstanceId}) ) {
        croak "Provide an 'InstanceId' to describe";
    }
    # ToDo: check this against a list of valid attributes (for now, just let EC2 tell us we're wrong)
    unless ( $self->is_valid_something($param->{Attribute}) ) {
        croak "Provide an 'InstanceId' to describe";
    }
    unless ( $self->is_valid_something($param->{Value}) ) {
        croak "Provide an 'InstanceId' to describe";
    }

    $self->set_command( 'ModifyInstanceAttribute' );
    $self->region( $param->{Region} ) if $param->{Region};
    $self->set_param( 'InstanceId', $param->{InstanceId} );
    $self->set_param( 'Attribute', $param->{Attribute} );
    $self->set_param( 'Value', $param->{Value} );
    return $self->send();
}

sub reboot_instances {
    my ($self, $param) = @_;

    $self->set_command( 'RebootInstances' );
    $self->region( $param->{Region} ) if $param->{Region};
    $self->_amazon_add_flattened_array_to_params( 'InstanceId', $param->{InstanceId} );
    my $data = $self->send();

    # instancesSet
    # $self->_fix_hash_to_array( $data->{instancesSet} );

    return $self->data;
}

sub run_instances {
    my ($self, $param) = @_;
<commit_msg>Fix up stuff about InstanceAttributes<commit_after>## ----------------------------------------------------------------------------

package AwsSum::Amazon::EC2;

use Moose;
use Moose::Util::TypeConstraints;
with qw(
    AwsSum::Service
    AwsSum::Amazon::Service
);

use Carp;
use DateTime;
use List::Util qw( reduce );
use Digest::SHA qw (hmac_sha1_base64 hmac_sha256_base64);
use XML::Simple;
use URI::Escape;
use MIME::Base64;

my $allowed = {
    # From: http://docs.amazonwebservices.com/AWSEC2/latest/DeveloperGuide/index.html?instance-types.html
    'instance-type' => {
        'm1.small'    => 1,
        'm1.large'    => 1,
        'm1.xlarge'   => 1,
        't1.micro'    => 1,
        'c1.medium'   => 1,
        'c1.xlarge'   => 1,
        'm2.xlarge'   => 1,
        'm2.2xlarge'  => 1,
        'm2.4xlarge'  => 1,
        'cc1.4xlarge' => 1,
    },
};

## ----------------------------------------------------------------------------
# setup details needed or pre-determined

# some things required from the user
enum 'SignatureMethod' => qw(HmacSHA1 HmacSHA256);
has 'signature_method'   => ( is => 'rw', isa => 'SignatureMethod', default => 'HmacSHA256' );

# constants
sub version { '2010-08-31' }

# internal helpers
has '_command' => ( is => 'rw', isa => 'HashRef' );

## ----------------------------------------------------------------------------

my $commands = {
    # In order of: http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/index.html?OperationList-query.html

    # Amazon DevPay
    # * ConfirmProductInstance
    # AMIs
    # * CreateImage
    # * DeregisterImage
    # * DescribeImageAttribute
    # * DescribeImages
    # * ModifyImageAttribute
    # Availability Zones and Regions
    # * DescribeAvailabilityZones
    # * DescribeRegions
    # Elastic Block Store
    # * AttachVolume
    # * CreateSnapshot
    # * CreateVolume
    # * DeleteSnapshot
    # * DeleteVolume
    # * DescribeSnapshotAttribute
    # * DescribeSnapshots
    # * DescribeVolumes
    # * DetachVolume
    # * ModifySnapshotAttribute
    # * ResetSnapshotAttribute
    # Elastic IP Addresses
    # * AllocateAddress
    # * AssociateAddress
    # * DescribeAddresses
    # * DisassociateAddress
    # * ReleaseAddress
    # General
    # * GetConsoleOutput
    # Images
    # * RegisterImage
    # * ResetImageAttribute
    # Instances
    # * DescribeInstanceAttribute
    # * DescribeInstances
    # * ModifyInstanceAttribute
    # * RebootInstances
    # * ResetInstanceAttribute
    # * RunInstances
    # * StartInstances
    # * StopInstances
    # * TerminateInstances
    # Key Pairs
    # * CreateKeyPair
    # * DeleteKeyPair
    # * DescribeKeyPairs
    # * ImportKeyPair
    # Monitoring
    # * MonitorInstances
    # * UnmonitorInstances
    # Placement Groups
    # * CreatePlacementGroup
    # * DeletePlacementGroup
    # * DescribePlacementGroups
    # Reserved Instances
    # * DescribeReservedInstances
    # * DescribeReservedInstancesOfferings
    # * PurchaseReservedInstancesOffering
    # Security Groups
    # * AuthorizeSecurityGroupIngress
    # * CreateSecurityGroup
    # * DeleteSecurityGroup
    # * DescribeSecurityGroups
    # * RevokeSecurityGroupIngress
    # Spot Instances
    # * CancelSpotInstanceRequests
    # * CreateSpotDatafeedSubscription
    # * DeleteSpotDatafeedSubscription
    # * DescribeSpotDatafeedSubscription
    # * DescribeSpotInstanceRequests
    # * DescribeSpotPriceHistory
    # * RequestSpotInstances
    # Tags
    # * CreateTags
    # * DeleteTags
    # * DescribeTags
    # Windows
    # * BundleInstance
    # * CancelBundleTask
    # * DescribeBundleTasks
    # * GetPasswordData

    # AMIs
    DescribeImages => {
        name           => 'DescribeImages',
        method         => 'describe_images',
    },

    # Availability Zones and Regions
    DescribeAvailabilityZones => {
        name           => 'DescribeAvailabilityZones',
        method         => 'describe_availability_zones',
    },
    DescribeRegions => {
        name           => 'DescribeRegions',
        method         => 'describe_regions',
    },

    # Elastic Block Store
    DescribeVolumes => {
        name           => 'DescribeVolumes',
        method         => 'describe_volumes',
    },

    # Elastic IP Addresses
    AllocateAddress => {
        name           => 'AllocateAddress',
        method         => 'allocate_address',
    },
    AssociateAddress => {
        name           => 'AssociateAddress',
        method         => 'associate_address',
    },
    DescribeAddresses => {
        name           => 'DescribeAddresses',
        method         => 'describe_addresses',
    },
    DisassociateAddress => {
        name           => 'DisassociateAddress',
        method         => 'disassociate_address',
    },
    ReleaseAddress => {
        name           => 'ReleaseAddress',
        method         => 'release_address',
    },

    # Instances
    DescribeInstanceAttribute => {
        name           => 'DescribeInstanceAttribute',
        method         => 'describe_instance_attribute',
    },
    DescribeInstances => {
        name           => 'DescribeInstances',
        method         => 'describe_instances',
    },
    ModifyInstanceAttribute => {
        name           => 'ModifyInstanceAttribute',
        method         => 'modify_instance_attribute',
    },
    RebootInstances => {
        name           => 'RebootInstances',
        method         => 'reboot_instances',
    },
    RunInstances => {
        name           => 'RunInstances',
        method         => 'run_instances',
    },
    StartInstances => {
        name           => 'StartInstances',
        method         => 'start_instances',
    },
    StopInstances => {
        name           => 'StopInstances',
        method         => 'stop_instances',
    },
    TerminateInstances => {
        name           => 'TerminateInstances',
        method         => 'terminate_instances',
    },

    # Key Pairs
    CreateKeyPair => {
        name           => 'CreateKeyPair',
        method         => 'create_key_pair',
    },
    DeleteKeyPair => {
        name           => 'DeleteKeyPair',
        method         => 'delete_key_pair',
    },
    DescribeKeyPairs => {
        name           => 'DescribeKeyPairs',
        method         => 'describe_key_pairs',
    },
    ImportKeyPair =>  {
        name           => 'ImportKeyPair',
        method         => 'import_key_pair',
    },

    # Reserved Instances
    DescribeReservedInstances => {
        name           => 'DescribeReservedInstances',
        method         => 'describe_reserved_instances',
    },
    DescribeReservedInstancesOfferings => {
        name           => 'DescribeReservedInstancesOfferings',
        method         => 'describe_reserved_instances_offerings',
    },
    PurchaseReservedInstancesOffering => {
        name           => 'PurchaseReservedInstancesOffering',
        method         => 'purchase_reserved_instances_offering',
    },

    # Security Groups
    AuthorizeSecurityGroupIngress => {
        name           => 'AuthorizeSecurityGroupIngress',
        method         => 'authorize_security_group_ingress',
    },
    CreateSecurityGroup => {
        name           => 'CreateSecurityGroup',
        method         => 'create_security_group',
    },
    DeleteSecurityGroup => {
        name           => 'DeleteSecurityGroup',
        method         => 'delete_security_group',
    },
    DescribeSecurityGroups => {
        name           => 'DescribeSecurityGroups',
        method         => 'describe_security_groups',
    },
    RevokeSecurityGroupIngress => {
        name           => 'RevokeSecurityGroupIngress',
        method         => 'revoke_security_group_ingress',
    },
};

## ----------------------------------------------------------------------------
# things to fill in to fulfill AwsSum::Service

sub commands { $commands }
sub verb { 'get' }
sub url {
    my ($self) = @_;

    # From: http://docs.amazonwebservices.com/AWSEC2/latest/DeveloperGuide/index.html?using-query-api.html
    return q{https://ec2.} . $self->region . q{.amazonaws.com/};
}
sub host {
    my ($self) = @_;
    return q{ec2.} . $self->region . q{.amazonaws.com};
}
sub code { 200 }

sub sign {
    my ($self) = @_;

    my $date = DateTime->now( time_zone => 'UTC' )->strftime("%Y-%m-%dT%H:%M:%SZ");

    # add the service params first before signing
    $self->set_param( 'Action', $self->_command->{name} );
    $self->set_param( 'Version', $self->version );
    $self->set_param( 'AWSAccessKeyId', $self->access_key_id );
    $self->set_param( 'Timestamp', $date );
    $self->set_param( 'SignatureVersion', 2 );
    $self->set_param( 'SignatureMethod', $self->signature_method );

    # See: http://docs.amazonwebservices.com/AWSEC2/2010-08-31/DeveloperGuide/index.html?using-query-api.html

    # sign the request (remember this is SignatureVersion '2')
    my $str_to_sign = '';
    $str_to_sign .= uc($self->verb) . "\n";
    $str_to_sign .= $self->host . "\n";
    $str_to_sign .= "/\n";

    my $param = $self->params();
    $str_to_sign .= join('&', map { "$_=" . uri_escape($param->{$_}) } sort keys %$param);

    # sign the $str_to_sign
    my $signature = ( $self->signature_method eq 'HmacSHA1' )
        ? hmac_sha1_base64($str_to_sign, $self->secret_access_key )
        : hmac_sha256_base64($str_to_sign, $self->secret_access_key );
    $self->set_param( 'Signature', $signature . '=' );
}

sub decode {
    my ($self) = @_;

    $self->data( XMLin( $self->res->content() ));
}

## ----------------------------------------------------------------------------
# all our lovely commands

sub describe_images {
    my ($self, $param) = @_;

    $self->set_command( 'DescribeImages' );
    $self->_amazon_add_flattened_array_to_params( 'ImageId', $param->{ImageId} );
    my $data = $self->send();

    # imagesSet, productCodes and blockDeviceMapping
    $self->_fix_hash_to_array( $data->{imagesSet} );
    $self->_fix_hash_to_array( $_->{productCodes} )
        foreach @{$data->{imagesSet}};
    $self->_fix_hash_to_array( $_->{blockDeviceMapping} )
        foreach @{$data->{imagesSet}};
    return $self->data;
}

sub describe_availability_zones {
    my ($self, $param) = @_;

    $self->set_command( 'DescribeAvailabilityZones' );
    $self->region( $param->{Region} ) if $param->{Region};
    return $self->send();
}

sub describe_regions {
    my ($self, $param) = @_;

    $self->set_command( 'DescribeRegions' );
    my $data = $self->send();

    # regionInfo
    $self->_fix_hash_to_array( $data->{regionInfo} );
    return $self->data;
}

sub describe_volumes {
    my ($self, $param) = @_;

    $self->set_command( 'DescribeVolumes' );
    $self->region( $param->{Region} ) if $param->{Region};
    $self->_amazon_add_flattened_array_to_params( 'VolumeId', $param->{VolumeId} );
    $self->_amazon_add_flattened_array_to_params( 'Filter', $param->{Filter} );
    my $data = $self->send();

    # volumeSet
    $self->_fix_hash_to_array( $data->{volumeSet} );
    foreach my $volume ( @{$data->{volumeSet}} ) {
        # attachmentSet
        $self->_fix_hash_to_array( $volume->{attachmentSet} );
    }

    return $self->data;
}

sub allocate_address {
    my ($self, $param) = @_;

    $self->set_command( 'AllocateAddress' );
    $self->region( $param->{Region} ) if $param->{Region};
    return $self->send();
}

sub associate_address {
    my ($self, $param) = @_;

    unless ( defined $param->{PublicIp} ) {
        croak "Provide a 'PublicIp' address to associate to an instance";
    }
    unless ( defined $param->{InstanceId} ) {
        croak "Provide an 'InstanceId' address to associate an address to";
    }

    $self->set_command( 'AssociateAddress' );
    $self->set_param( 'InstanceId', $param->{InstanceId} );
    $self->set_param( 'PublicIp', $param->{PublicIp} );

    return $self->send();
}

sub describe_addresses {
    my ($self, $param) = @_;

    $self->set_command( 'DescribeAddresses' );
    $self->region( $param->{Region} ) if $param->{Region};
    my $data = $self->send();

    # addressesSet
    $self->_fix_hash_to_array( $data->{addressesSet} );
    return $self->data;
}

sub disassociate_address {
    my ($self, $param) = @_;

    unless ( defined $param->{PublicIp} ) {
        croak "Provide a 'PublicIp' address to disassociate";
    }

    $self->set_command( 'DisassociateAddress' );
    $self->set_param( 'PublicIp', $param->{PublicIp} );

    return $self->send();
}

sub release_address {
    my ($self, $param) = @_;

    unless ( defined $param->{PublicIp} ) {
        croak "Provide a 'PublicIp' address to release";
    }

    $self->set_command( 'ReleaseAddress' );
    $self->region( $param->{Region} ) if $param->{Region};
    $self->set_param( 'PublicIp', $param->{PublicIp} );
    return $self->send();
}

sub describe_instance_attribute {
    my ($self, $param) = @_;

    unless ( $self->is_valid_something($param->{InstanceId}) ) {
        croak "Provide an 'InstanceId' to describe";
    }
    # ToDo: check this against a list of valid attributes (for now, just let EC2 tell us we're wrong)
    unless ( $self->is_valid_something($param->{Attribute}) ) {
        croak "Provide an 'Attribute' to describe";
    }

    $self->set_command( 'DescribeInstanceAttribute' );
    $self->region( $param->{Region} ) if $param->{Region};
    $self->set_param( 'InstanceId', $param->{InstanceId} );
    $self->set_param( 'Attribute', $param->{Attribute} );
    return $self->send();
}

sub describe_instances {
    my ($self, $param) = @_;

    $self->set_command( 'DescribeInstances' );
    $self->region( $param->{Region} ) if $param->{Region};
    $self->_amazon_add_flattened_array_to_params( 'InstanceId', $param->{InstanceId} );
    my $data = $self->send();

    # reservationSet
    $self->_fix_hash_to_array( $data->{reservationSet} );
    foreach my $reservation ( @{$data->{reservationSet}} ) {
        # groupSet
        $self->_fix_hash_to_array( $reservation->{groupSet} );

        # instancesSet
        $self->_fix_hash_to_array( $reservation->{instancesSet} );
        foreach my $instance ( @{$reservation->{instancesSet}} ) {
            # blockDeviceMapping
            $self->_fix_hash_to_array( $instance->{blockDeviceMapping} );
        }
    }

    return $self->data;
}

sub modify_instance_attribute {
    my ($self, $param) = @_;

    unless ( $self->is_valid_something($param->{InstanceId}) ) {
        croak "Provide an 'InstanceId' to describe";
    }
    # ToDo: check this against a list of valid attributes (for now, just let EC2 tell us we're wrong)
    unless ( $self->is_valid_something($param->{Attribute}) ) {
        croak "Provide an 'Attribute' to modify";
    }
    unless ( $self->is_valid_something($param->{Value}) ) {
        croak "Provide a 'Value' for the attribute";
    }

    $self->set_command( 'ModifyInstanceAttribute' );
    $self->region( $param->{Region} ) if $param->{Region};
    $self->set_param( 'InstanceId', $param->{InstanceId} );
    $self->set_param( 'Attribute', $param->{Attribute} );
    $self->set_param( 'Value', $param->{Value} );
    return $self->send();
}

sub reboot_instances {
    my ($self, $param) = @_;

    $self->set_command( 'RebootInstances' );
    $self->region( $param->{Region} ) if $param->{Region};
    $self->_amazon_add_flattened_array_to_params( 'InstanceId', $param->{InstanceId} );
    my $data = $self->send();

    # instancesSet
    # $self->_fix_hash_to_array( $data->{instancesSet} );

    return $self->data;
}

sub run_instances {
    my ($self, $param) = @_;

{'fraction_non_alphanumeric': 0.16569674489991687, 'fraction_numerical': 0.00505211997186161, 'mean_word_length': 2.3987176700717234, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 529, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3592051', 'n_tokens_mistral': 9732, 'n_tokens_neox': 9536, 'n_words': 1751}