├── ec2home ├── ansible.cfg ├── addons.yml ├── README.md ├── variables.tf └── main.tf ├── kriskross ├── static │ ├── aws-iam.png │ └── favicon.ico ├── templates │ └── targets.html ├── kriskross.py ├── README.md └── COPYING ├── platforms ├── roles │ ├── s3_fullaccess.json │ ├── ec2-trusted-policy.json │ ├── ec2_s3_readonly.json │ ├── ec2_s3_readonly_route53_write_rrs.json │ └── mkrole.py ├── stack │ ├── params │ └── asg.py ├── packer │ ├── basic.json │ └── essentials.yml ├── mkasg.sh └── scripts │ └── meta2dict.py ├── userdata └── gtfo-systemd.sh ├── README.md ├── troposphere_examples ├── ec2instance.py ├── README.md └── asg.py ├── mods ├── session.py └── awsprice.py └── ec2.py /ec2home/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | host_key_checking = False 3 | -------------------------------------------------------------------------------- /kriskross/static/aws-iam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMilnb/awstools/HEAD/kriskross/static/aws-iam.png -------------------------------------------------------------------------------- /kriskross/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMilnb/awstools/HEAD/kriskross/static/favicon.ico -------------------------------------------------------------------------------- /platforms/roles/s3_fullaccess.json: -------------------------------------------------------------------------------- 1 | {"Version":"2012-10-17","Statement":{"Effect":"Allow","Action":"s3:*","Resource":"*"}} 2 | -------------------------------------------------------------------------------- /ec2home/addons.yml: -------------------------------------------------------------------------------- 1 | --- 2 | add_pkgs: 3 | - alpine 4 | - netcat 5 | 6 | pip_pkgs: 7 | - ansible 8 | - awscli 9 | - boto3 10 | 11 | copy_files: 12 | ~/wrk/etc/pinerc: "{{homedir}}/.pinerc" 13 | -------------------------------------------------------------------------------- /platforms/roles/ec2-trusted-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "Service": "ec2.amazonaws.com" 8 | }, 9 | "Action": "sts:AssumeRole" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /userdata/gtfo-systemd.sh: -------------------------------------------------------------------------------- 1 | # initial idea from 2 | # http://without-systemd.org/wiki/index.php/How_to_remove_systemd_from_a_Debian_jessie/sid_installation 3 | apt-get -y install sysvinit-core sysvinit sysvinit-utils 4 | apt-get remove --purge --auto-remove systemd 5 | cat >/etc/apt/preferences.d/systemd< cf-ec2instances.json 10 | ``` 11 | 12 | * Write the parameters file: 13 | 14 | ```json 15 | [ 16 | { 17 | "ParameterKey": "InstanceType", 18 | "ParameterValue": "t2.micro", 19 | "UsePreviousValue": false 20 | }, 21 | { 22 | "ParameterKey": "AmiId", 23 | "ParameterValue": "ami-00000000", 24 | "UsePreviousValue": false 25 | }, 26 | { 27 | "ParameterKey": "KeyName", 28 | "ParameterValue": "mykey-eu-central-1", 29 | "UsePreviousValue": false 30 | }, 31 | { 32 | "ParameterKey": "SecurityGroup", 33 | "ParameterValue": "sg-00000000,sg-11111111", 34 | "UsePreviousValue": false 35 | }, 36 | { 37 | "ParameterKey": "SubnetA", 38 | "ParameterValue": "subnet-00000000", 39 | "UsePreviousValue": false 40 | }, 41 | { 42 | "ParameterKey": "InstanceName", 43 | "ParameterValue": "my_test_instance_", 44 | "UsePreviousValue": false 45 | } 46 | ] 47 | ``` 48 | 49 | Deploy the stack on `CloudFormation` 50 | 51 | ```sh 52 | $ aws cloudformation create-stack --stack-name mystack --parameters file://cf-ec2instances.params --template-body file://cf-ec2instances.json 53 | ``` 54 | -------------------------------------------------------------------------------- /troposphere_examples/asg.py: -------------------------------------------------------------------------------- 1 | from troposphere import Template, Ref, Parameter, Tags 2 | from troposphere.autoscaling import LaunchConfiguration, AutoScalingGroup, Tag 3 | 4 | t = Template() 5 | 6 | params = { 7 | 'AmiId': 'Baked AMI Id', 8 | 'InstanceName': 'Name tag of the instance', 9 | 'SecurityGroup': 'Security Group' , 10 | 'KeyName': 'SSH Key Name' , 11 | 'InstanceType': 'Instance Type', 12 | 'EnvType': 'test', 13 | 'ScaleCapacity': 'Number of api servers to run', 14 | 'SubnetA': 'ASG Subnet A' 15 | } 16 | 17 | for p in params.keys(): 18 | vars()[p] = t.add_parameter(Parameter( 19 | p, 20 | Type = "String", 21 | Description = params[p] 22 | )) 23 | 24 | LaunchConfig = t.add_resource(LaunchConfiguration( 25 | "LaunchConfiguration", 26 | ImageId = Ref(AmiId), 27 | SecurityGroups = [Ref(SecurityGroup)], 28 | KeyName = Ref(KeyName), 29 | InstanceType = Ref(InstanceType) 30 | )) 31 | 32 | t.add_resource(AutoScalingGroup( 33 | "AutoscalingGroup", 34 | Tags=[ 35 | Tag("Environment", Ref(EnvType), True), 36 | Tag("Name", Ref(InstanceName), True) 37 | ], 38 | DesiredCapacity = Ref(ScaleCapacity), 39 | LaunchConfigurationName=Ref(LaunchConfig), 40 | MinSize = Ref(ScaleCapacity), 41 | MaxSize = Ref(ScaleCapacity), 42 | VPCZoneIdentifier=[Ref(SubnetA)], 43 | )) 44 | 45 | 46 | print(t.to_json()) 47 | -------------------------------------------------------------------------------- /platforms/stack/params: -------------------------------------------------------------------------------- 1 | PARAMS="[ 2 | { 3 | \"ParameterKey\": \"AmiId\", 4 | \"ParameterValue\": \"${AMIID##*:}\", 5 | \"UsePreviousValue\": false 6 | }, 7 | { 8 | \"ParameterKey\": \"InstanceName\", 9 | \"ParameterValue\": \"${INSTANCENAME}\", 10 | \"UsePreviousValue\": false 11 | }, 12 | { 13 | \"ParameterKey\": \"IamInstanceProfile\", 14 | \"ParameterValue\": \"${INSTANCEPROFILE}\", 15 | \"UsePreviousValue\": false 16 | }, 17 | { 18 | \"ParameterKey\": \"SecurityGroups\", 19 | \"ParameterValue\": \"${SGSID}\", 20 | \"UsePreviousValue\": false 21 | }, 22 | { 23 | \"ParameterKey\": \"KeyName\", 24 | \"ParameterValue\": \"${KEYNAME}\", 25 | \"UsePreviousValue\": false 26 | }, 27 | { 28 | \"ParameterKey\": \"InstanceType\", 29 | \"ParameterValue\": \"${TYPE}\", 30 | \"UsePreviousValue\": false 31 | }, 32 | { 33 | \"ParameterKey\": \"EnvType\", 34 | \"ParameterValue\": \"${ENVTYPE}\", 35 | \"UsePreviousValue\": false 36 | }, 37 | { 38 | \"ParameterKey\": \"ScaleCapacity\", 39 | \"ParameterValue\": \"${CAPACITY}\", 40 | \"UsePreviousValue\": false 41 | }, 42 | { 43 | \"ParameterKey\": \"Subnets\", 44 | \"ParameterValue\": \"${SUBNETSID}\", 45 | \"UsePreviousValue\": false 46 | } 47 | ]" 48 | -------------------------------------------------------------------------------- /platforms/stack/asg.py: -------------------------------------------------------------------------------- 1 | from troposphere import Template, Ref, Split, Parameter, Tags 2 | from troposphere.autoscaling import LaunchConfiguration, AutoScalingGroup, Tag 3 | 4 | t = Template() 5 | 6 | params = { 7 | 'AmiId': 'Baked AMI Id', 8 | 'InstanceName': 'Name tag of the instance', 9 | 'IamInstanceProfile': 'IAM Instance profile', 10 | 'SecurityGroups': 'Security Groups' , 11 | 'KeyName': 'SSH Key Name' , 12 | 'InstanceType': 'Instance Type', 13 | 'EnvType': 'test', 14 | 'ScaleCapacity': 'Number of api servers to run', 15 | 'Subnets': 'ASG Subnets' 16 | } 17 | 18 | for p in params.keys(): 19 | vars()[p] = t.add_parameter(Parameter( 20 | p, 21 | Type = "String", 22 | Description = params[p] 23 | )) 24 | 25 | LaunchConfig = t.add_resource(LaunchConfiguration( 26 | "LaunchConfiguration", 27 | ImageId = Ref(AmiId), 28 | SecurityGroups = Split(',', Ref(SecurityGroups)), 29 | KeyName = Ref(KeyName), 30 | InstanceType = Ref(InstanceType) 31 | )) 32 | 33 | t.add_resource(AutoScalingGroup( 34 | "AutoscalingGroup", 35 | Tags=[ 36 | Tag("Environment", Ref(EnvType), True), 37 | Tag("Name", Ref(InstanceName), True) 38 | ], 39 | DesiredCapacity = Ref(ScaleCapacity), 40 | LaunchConfigurationName=Ref(LaunchConfig), 41 | MinSize = Ref(ScaleCapacity), 42 | MaxSize = Ref(ScaleCapacity), 43 | VPCZoneIdentifier=Split(',', Ref(Subnets)), 44 | )) 45 | 46 | 47 | print(t.to_json()) 48 | -------------------------------------------------------------------------------- /platforms/roles/ec2_s3_readonly_route53_write_rrs.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "s3:Get*", 8 | "s3:List*" 9 | ], 10 | "Resource": "*" 11 | }, 12 | { 13 | "Effect": "Allow", 14 | "Action": "ec2:Describe*", 15 | "Resource": "*" 16 | }, 17 | { 18 | "Effect": "Allow", 19 | "Action": "elasticloadbalancing:Describe*", 20 | "Resource": "*" 21 | }, 22 | { 23 | "Effect": "Allow", 24 | "Action": [ 25 | "cloudwatch:ListMetrics", 26 | "cloudwatch:GetMetricStatistics", 27 | "cloudwatch:Describe*" 28 | ], 29 | "Resource": "*" 30 | }, 31 | { 32 | "Effect": "Allow", 33 | "Action": "autoscaling:Describe*", 34 | "Resource": "*" 35 | }, 36 | { 37 | "Effect": "Allow", 38 | "Action": [ 39 | "route53:ListHostedZones" 40 | ], 41 | "Resource": "*" 42 | }, 43 | { 44 | "Effect": "Allow", 45 | "Action": [ 46 | "route53:GetHostedZone" 47 | ], 48 | "Resource": "arn:aws:route53:::hostedzone/HOSTEDZONEID" 49 | }, 50 | { 51 | "Effect": "Allow", 52 | "Action": [ 53 | "route53:ListResourceRecordSets" 54 | ], 55 | "Resource": "arn:aws:route53:::hostedzone/HOSTEDZONEID" 56 | }, 57 | { 58 | "Effect": "Allow", 59 | "Action": [ 60 | "route53:ChangeResourceRecordSets" 61 | ], 62 | "Resource": "arn:aws:route53:::hostedzone/HOSTEDZONEID" 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /kriskross/templates/targets.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | {% if msg != None %} 15 | 22 | {% endif %} 23 |
24 | 25 |
26 |
27 | {% for t in prefs %} 28 |
29 |
30 | 31 | 32 | 33 | 34 |
35 |
36 | {% endfor %} 37 |
38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /platforms/packer/basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "variables": { 3 | "aws_access_key": "", 4 | "aws_secret_key": "", 5 | "username": "{{user `username`}}", 6 | "githubname": "{{user `githubname`}}" 7 | }, 8 | "builders": [{ 9 | "type": "amazon-ebs", 10 | "access_key": "{{user `aws_access_key`}}", 11 | "secret_key": "{{user `aws_secret_key`}}", 12 | "region": "eu-central-1", 13 | "source_ami_filter": { 14 | "filters": { 15 | "virtualization-type": "hvm", 16 | "name": "debian-jessie-*", 17 | "architecture": "x86_64", 18 | "root-device-type": "ebs" 19 | }, 20 | "owners": [ 21 | "379101102735" 22 | ], 23 | "most_recent": true 24 | }, 25 | "instance_type": "t2.micro", 26 | "ssh_username": "admin", 27 | "ami_name": "{{user `ami_basename`}}_{{timestamp}}", 28 | "subnet_id": "{{user `subnetid`}}", 29 | "security_group_id": "{{user `sgid`}}", 30 | "associate_public_ip_address": false 31 | }], 32 | "provisioners": [{ 33 | "type": "shell", 34 | "inline": [ 35 | "echo 'Acquire::ForceIPv4 \"true\";' | sudo tee /etc/apt/apt.conf.d/99force-ipv4", 36 | "echo 'deb http://cloudfront.debian.net/debian jessie-backports main' |sudo tee /etc/apt/sources.list.d/backports.list", 37 | "sudo apt-get update", 38 | "sudo apt-get -t jessie-backports install -y ansible" 39 | ] 40 | }, { 41 | "type": "ansible-local", 42 | "playbook_file": "essentials.yml", 43 | "command": "PYTHONUNBUFFERED=1 ansible-playbook", 44 | "extra_arguments": ["--extra-vars \"myuser={{user `username`}} myghuser={{user `githubname`}}\""] 45 | }], 46 | "post-processors": [ 47 | [ 48 | { 49 | "output": "manifest.json", 50 | "strip_path": true, 51 | "type": "manifest" 52 | } 53 | ] 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /platforms/mkasg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | usage() 4 | { 5 | echo "$0 -parc" 6 | echo " -p run packer" 7 | echo " -a execute all actions" 8 | echo " -a create IAM roles" 9 | echo " -a create CloudFormation stack" 10 | exit 2 11 | } 12 | 13 | flags='' 14 | 15 | while [ $# -gt 0 ]; do 16 | case "$1" in 17 | -p) flags="${flags}p" ;; 18 | -a) flags="${flags}a" ;; 19 | -r) flags="${flags}r" ;; 20 | -c) flags="${flags}c" ;; 21 | *) usage ;; 22 | esac 23 | shift 24 | done 25 | 26 | . ./vars 27 | 28 | case ${flags} in 29 | # packer IAM 30 | *a*|*p*) 31 | 32 | SGID=$(aws ec2 describe-security-groups --filters "Name=tag:Name, Values=${PKRSG}"| jq -r '.SecurityGroups[0].GroupId') 33 | SUBNETID=$(aws ec2 describe-subnets --filters "Name=tag:Name, Values=${PKRSUBNET}"|jq -r '.Subnets[0].SubnetId') 34 | 35 | cd packer && packer build \ 36 | -var "username=${USER}" \ 37 | -var "githubname=${GHUSER}" \ 38 | -var "sgid=${SGID}" \ 39 | -var "subnetid=${SUBNETID}" \ 40 | -var "ami_basename=${AMI_BASENAME}" \ 41 | basic.json 42 | ;; 43 | # role creation 44 | *a*|*r*) 45 | cd roles 46 | # HOSTEDZONEID needs to be set in vars for RRset 47 | HOSTEDZONEID=$HOSTEDZONEID python mkrole.py \ 48 | ${ROLE} ${TRUST_POLICY} ${POLICY_DOCUMENT} 49 | ;; 50 | # CloudFormation stack creation 51 | *a*|*c*) 52 | # get latest built AMI id 53 | AMIID=$(jq -r '.last_run_uuid as $uuid | .builds[] | select(.packer_run_uuid == $uuid) | .artifact_id' packer/manifest.json) 54 | INSTANCEPROFILE=$(aws iam list-instance-profiles-for-role --role-name "${ROLE}"|jq -r '.InstanceProfiles[].Arn') 55 | 56 | cd stack 57 | 58 | # generate JSON stack 59 | python asg.py > ${STACKNAME}.json 60 | # load stack parameters 61 | . ./params 62 | PARAMFILE="${STACKNAME}-parameters.json" 63 | echo "${PARAMS}" > "${PARAMFILE}" 64 | 65 | aws cloudformation create-stack --stack-name ${STACKNAME} --template-body file://${STACKNAME}.json --parameters file://${PARAMFILE} 66 | ;; 67 | *) 68 | usage 69 | ;; 70 | esac 71 | -------------------------------------------------------------------------------- /platforms/roles/mkrole.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """This piece of code aims at simplifying IAM Roles creation 3 | """ 4 | 5 | import boto3 6 | import sys 7 | import os 8 | 9 | def usage(): 10 | print( 11 | '{0} ' 12 | .format(sys.argv[0]) 13 | ) 14 | sys.exit(2) 15 | 16 | 17 | if len(sys.argv) < 4: 18 | usage() 19 | 20 | name = sys.argv[1] 21 | trust_policy_json = sys.argv[2] 22 | policy_document_json = sys.argv[3] 23 | 24 | # load the chosen trust policy and policy document 25 | for json_file in ['trust_policy_json', 'policy_document_json']: 26 | with open(vars()[json_file], 'r') as f: 27 | vars()[json_file[:-5]] = f.read() 28 | 29 | HOSTEDZONEID = os.getenv('HOSTEDZONEID') 30 | 31 | if 'HOSTEDZONEID' in policy_document and HOSTEDZONEID: 32 | policy_document = policy_document.replace('HOSTEDZONEID', HOSTEDZONEID) 33 | 34 | # connect to IAM 35 | c = boto3.client('iam') 36 | 37 | # create the instance profile 38 | # both instance_profile and role have the same same so instance profile can be 39 | # deleted from the console. Source: 40 | # http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_manage_delete.html 41 | instance_profile = '{0}_role'.format(name) 42 | r = c.create_instance_profile( 43 | InstanceProfileName = instance_profile 44 | ) 45 | 46 | # create the role, associated with the chosen trust policy 47 | role = '{0}_role'.format(name) 48 | r = c.create_role( 49 | RoleName = role, 50 | AssumeRolePolicyDocument = trust_policy 51 | ) 52 | 53 | # associate role and instance profile 54 | r = c.add_role_to_instance_profile( 55 | InstanceProfileName = instance_profile, 56 | RoleName = role 57 | ) 58 | 59 | # attach policy to role 60 | r = c.put_role_policy( 61 | RoleName = role, 62 | PolicyName = '{0}_policy'.format(name), 63 | PolicyDocument = policy_document 64 | ) 65 | 66 | print( 67 | 'Role {0} created with Instance Profile {1}' 68 | .format(role, instance_profile) 69 | ) 70 | -------------------------------------------------------------------------------- /ec2home/main.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "${var.aws_region}" 3 | profile = "${var.aws_profile}" 4 | } 5 | 6 | data "aws_ami" "ami_type" { 7 | most_recent = true 8 | 9 | filter { 10 | name = "name" 11 | values = ["${var.ami_filter}"] 12 | } 13 | 14 | filter { 15 | name = "virtualization-type" 16 | values = ["hvm"] 17 | } 18 | 19 | owners = ["${var.ami_owner}"] 20 | } 21 | 22 | resource "aws_security_group" "ssh_sg" { 23 | name = "inbound SSH" 24 | 25 | ingress { 26 | from_port = 22 27 | to_port = 22 28 | protocol = "tcp" 29 | cidr_blocks = ["0.0.0.0/0"] 30 | } 31 | 32 | egress { 33 | from_port = 0 34 | to_port = 0 35 | protocol = "-1" 36 | cidr_blocks = ["0.0.0.0/0"] 37 | } 38 | 39 | tags { 40 | Name = "allow_ssh" 41 | } 42 | } 43 | 44 | resource "aws_instance" "ec2_instance" { 45 | ami = "${data.aws_ami.ami_type.id}" 46 | instance_type = "${var.instance_type}" 47 | subnet_id = "${var.subnet_id}" 48 | vpc_security_group_ids = ["${aws_security_group.ssh_sg.id}"] 49 | key_name = "${var.keypair}" 50 | associate_public_ip_address = "${var.associate_public_ip}" 51 | count = 1 52 | 53 | tags { 54 | created_by = "terraform" 55 | Name = "${var.instance_name}" 56 | } 57 | 58 | provisioner "remote-exec" { 59 | inline = ["sudo apt-get -y install python"] 60 | 61 | connection { 62 | type = "ssh" 63 | user = "${var.ec2admin_user}" 64 | private_key = "${file(var.ssh_private_key)}" 65 | } 66 | } 67 | 68 | provisioner "local-exec" { 69 | command = "ansible-playbook -u ${var.ec2admin_user} -i '${self.public_ip},' --private-key ${var.ssh_private_key} -T 300 ${var.playbook} --extra-vars ${var.extra_vars}" 70 | } 71 | } 72 | 73 | resource "aws_route53_record" "instance" { 74 | zone_id = "${var.zone_id}" 75 | name = "${var.fqdn}" 76 | type = "A" 77 | ttl = "300" 78 | records = ["${aws_instance.ec2_instance.public_ip}"] 79 | } 80 | -------------------------------------------------------------------------------- /platforms/scripts/meta2dict.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Converts and return an AWS EC2 metadata in a dict format. 3 | 4 | This module makes easier the task of finding and reading instances values 5 | located in the http://169.254.169.254/ `virtual web server`_. 6 | 7 | Its usage is trivial:: 8 | 9 | import meta2dict 10 | 11 | ec2meta = meta2dict.load() 12 | 13 | .. _virtual web server: 14 | http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html 15 | 16 | """ 17 | 18 | import json 19 | import requests 20 | 21 | 22 | def load(root='latest'): 23 | """Returns instance metadata in a dict format. 24 | 25 | Args: 26 | root (str): root directory. 27 | 28 | Return: 29 | dict: instance metadata. 30 | 31 | """ 32 | metaurl = 'http://169.254.169.254/{0}'.format(root) 33 | # those 3 top subdirectories are not exposed with a final '/' 34 | metadict = {'dynamic': {}, 'meta-data': {}, 'user-data': {}} 35 | 36 | for subsect in metadict: 37 | _datacrawl('{0}/{1}/'.format(metaurl, subsect), metadict[subsect]) 38 | 39 | return metadict 40 | 41 | 42 | def _datacrawl(url, d): 43 | """Recursively populates a dict with metadata. 44 | 45 | Args: 46 | url (str): URL to parse 47 | d (dict): dict to populate data with 48 | 49 | """ 50 | r = requests.get(url) 51 | if r.status_code == 404: 52 | return 53 | 54 | for l in r.text.split('\n'): 55 | if not l: # handle "instance-identity/\n" case 56 | continue 57 | newurl = '{0}{1}'.format(url, l) 58 | # a key is detected with a final '/' 59 | if l.endswith('/'): 60 | newkey = l.split('/')[-2] 61 | d[newkey] = {} 62 | _datacrawl(newurl, d[newkey]) 63 | 64 | else: 65 | r = requests.get(newurl) 66 | if r.status_code != 404: 67 | try: 68 | d[l] = json.loads(r.text) 69 | except ValueError: 70 | d[l] = r.text 71 | else: 72 | d[l] = None 73 | 74 | 75 | 76 | if __name__ == '__main__': 77 | # test the load function 78 | print(json.dumps(load())) 79 | -------------------------------------------------------------------------------- /platforms/packer/essentials.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # call me with --extra-vars "myuser=foo myghuser=foogh" 3 | 4 | - hosts: all 5 | become: true 6 | 7 | vars: 8 | vimrc: | 9 | syntax on 10 | set ts=8 11 | set noai 12 | set ruler 13 | set tw=80 14 | set t_Co=256 15 | colorscheme molokai-transparent 16 | set laststatus=2 17 | 18 | au FileType python set ts=4 expandtab 19 | au FileType javascript set ts=4 expandtab 20 | au FileType html set ts=2 expandtab 21 | au FileType yaml,json set ts=2 expandtab 22 | 23 | tmuxconf: | 24 | set-option -g prefix2 C-q 25 | set-option -g default-terminal "screen-256color" 26 | 27 | profile: | 28 | PATH=${PATH}:/usr/local/bin:/sbin:/usr/sbin:/usr/local/sbin:$HOME/bin 29 | # use vim for sudoedit and all $EDITOR 30 | EDITOR=vim 31 | VISUAL=$EDITOR 32 | export EDITOR VISUAL 33 | # allow ^Q for screen / tmux 34 | stty -ixon 35 | 36 | 37 | tasks: 38 | - name: installs essential tools 39 | apt: name={{item}} state=installed 40 | with_items: 41 | - vim-nox 42 | - sudo 43 | - tmux 44 | - build-essential 45 | - git 46 | - python 47 | - python-pip 48 | - zsh 49 | 50 | ## sudo 51 | 52 | - name: make sudo group passwordless 53 | blockinfile: 54 | dest: /etc/sudoers 55 | block: | 56 | %sudo ALL=(ALL) NOPASSWD: ALL 57 | 58 | ## create the user 59 | 60 | - name: user add 61 | user: 62 | name: "{{myuser}}" 63 | groups: sudo 64 | shell: /usr/bin/zsh 65 | 66 | ## aws 67 | 68 | - name: install AWS CLI 69 | pip: name=awscli 70 | 71 | ## shell 72 | 73 | - name: install powerline from pip 74 | pip: name=powerline-status 75 | 76 | - name: record powerline module path 77 | command: python -c "import powerline, os; print(os.path.dirname(powerline.__file__))" 78 | register: powerlinepath 79 | failed_when: powerlinepath is undefined 80 | 81 | - name: profile and powerline autoload 82 | copy: 83 | content: | 84 | {{profile}} 85 | 86 | powerline-daemon -q 87 | . {{powerlinepath.stdout_lines[0]}}/bindings/bash/powerline.sh 88 | dest: "/home/{{myuser}}/.zshrc" 89 | 90 | ## vim 91 | 92 | - name: fetch vim themes 93 | git: 94 | repo: https://github.com/hugoroy/.vim 95 | dest: "/home/{{myuser}}/.vim" 96 | 97 | - name: create basic vimrc 98 | copy: content="{{vimrc}}\nset rtp+={{powerlinepath.stdout_lines[0]}}/bindings/vim\n" dest="/home/{{myuser}}/.vimrc" 99 | 100 | ## tmux 101 | 102 | - name: powerline support for tmux 103 | copy: content="{{tmuxconf}}\nsource {{powerlinepath.stdout_lines[0]}}/bindings/tmux/powerline.conf\n" dest="/home/{{myuser}}/.tmux.conf" 104 | 105 | ## chown user 106 | 107 | - name: create .ssh directory 108 | file: 109 | path: "/home/{{myuser}}/.ssh" 110 | state: directory 111 | mode: 0700 112 | 113 | - name: fetch SSH pubkeys 114 | get_url: 115 | url: "https://github.com/{{myghuser}}.keys" 116 | dest: "/home/{{myuser}}/.ssh/authorized_keys" 117 | mode: 0600 118 | 119 | - name: chown user 120 | file: 121 | path: "/home/{{myuser}}" 122 | owner: "{{myuser}}" 123 | recurse: yes 124 | -------------------------------------------------------------------------------- /kriskross/kriskross.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Starts an AWS console from the shell based on cross account roles. 4 | 5 | See `README.md` for more details. 6 | 7 | Usage: 8 | kriskross.py [--awsaccounts= --mfa=] 9 | 10 | """ 11 | 12 | import os 13 | import sys 14 | import json 15 | import uuid 16 | import boto3 17 | import requests 18 | import webbrowser 19 | from docopt import docopt 20 | from distutils.spawn import find_executable 21 | from flask import Flask, request, render_template, url_for 22 | 23 | app = Flask(__name__) 24 | 25 | # add more! 26 | browser_private_flag = { 27 | 'chrome': '--incognito', 28 | 'firefox': '--private-window' 29 | } 30 | 31 | def loadprefs(): 32 | """load preferences file 33 | """ 34 | if 'awsaccounts' not in locals() or awsaccounts == None: 35 | awsaccounts = '{0}/.awsaccounts'.format(os.path.expanduser('~')) 36 | 37 | return json.load(open(awsaccounts)) 38 | 39 | 40 | def do_auth(prefs, target, mfatoken): 41 | """Assume role, retrieve temporary token, authenticate and launch browser 42 | """ 43 | signin_url = 'https://signin.aws.amazon.com/federation' 44 | console_url = 'https://console.aws.amazon.com/' 45 | 46 | # prepare assume role parameters 47 | params = {} 48 | params['RoleArn'] = 'arn:aws:iam::{0}:role/{1}'.format( 49 | prefs[target]['account'], prefs[target]['role'] 50 | ) 51 | # random hexadecimal 52 | params['RoleSessionName'] = uuid.uuid4().hex 53 | # ExternalId for 3rd party accounts 54 | if 'external-id' in prefs[target]: 55 | params['ExternalId'] = prefs[target]['external-id'] 56 | # MFA token 57 | if mfatoken != None and 'mfa' in prefs[target]: 58 | params['SerialNumber'] = prefs[target]['mfa'] 59 | params['TokenCode'] = mfatoken 60 | 61 | # Session duration 62 | if 'duration' in prefs[target]: 63 | params['DurationSeconds'] = prefs[target]['duration'] 64 | 65 | p = {} 66 | if 'profile' in prefs[target]: 67 | p['profile_name'] = prefs[target]['profile'] 68 | 69 | s = boto3.Session(**p) 70 | sts = s.client('sts') 71 | 72 | creds = sts.assume_role(**params) 73 | 74 | json_creds = json.dumps( 75 | { 76 | 'sessionId': creds['Credentials']['AccessKeyId'], 77 | 'sessionKey': creds['Credentials']['SecretAccessKey'], 78 | 'sessionToken': creds['Credentials']['SessionToken'] 79 | } 80 | ) 81 | 82 | params = {'Action': 'getSigninToken', 'Session': json_creds} 83 | 84 | 85 | r = requests.get(signin_url, params = params) 86 | 87 | params = { 88 | 'Action': 'login', 89 | 'Issuer': '', 90 | 'Destination': console_url, 91 | 'SigninToken': json.loads(r.text)['SigninToken'], 92 | } 93 | 94 | if 'external-id' in prefs[target]: 95 | params['ExternalId'] = prefs[target]['external-id'] 96 | 97 | uri = '{0}?{1}'.format(signin_url, requests.compat.urlencode(params)) 98 | 99 | if 'private' in prefs[target] and prefs[target]['private'] == 'yes': 100 | browserapp = os.environ.get('BROWSER') 101 | 102 | if browserapp: 103 | browserpath = find_executable(browserapp) 104 | private = '' 105 | if browserpath: 106 | for k in browser_private_flag.keys(): 107 | if k in browserapp: 108 | private = ' {0} %s'.format(browser_private_flag[k]) 109 | 110 | browser = webbrowser.get('{0}{1}'.format(browserpath, private)) 111 | browser.open(uri) 112 | return 113 | 114 | webbrowser.open(uri) 115 | 116 | 117 | @app.route('/', methods=['GET', 'POST']) 118 | def web_service(): 119 | """Minimal web service to receive MFA 120 | """ 121 | prefs = loadprefs() 122 | msg = None 123 | err = None 124 | target = None 125 | if request.method == 'POST': 126 | try: 127 | target = request.form['target'] 128 | do_auth(prefs, target, request.form['mfa']) 129 | msg = 'success' 130 | except Exception, e: 131 | msg = 'danger' 132 | err = str(e) 133 | 134 | return render_template( 135 | 'targets.html', prefs = prefs, msg = msg, err = err, target = target 136 | ) 137 | 138 | 139 | if __name__ == "__main__": 140 | args = docopt(__doc__, version = 'kriskross 0.3') 141 | 142 | target = args.get('') 143 | awsaccounts = args.get('--awsaccounts') 144 | mfatoken = args.get('--mfa') 145 | 146 | do_auth(loadprefs(), target, mfatoken) 147 | 148 | -------------------------------------------------------------------------------- /kriskross/README.md: -------------------------------------------------------------------------------- 1 |

2 | Jump! Jump! 3 |

4 | 5 | ### About 6 | 7 | `kriskross` can start an _AWS_ console **on your own account**, sub-accounts or third party accounts using cross account roles from the **shell** or a basic **web service**: no more struggling with your _AWS console login / password_!. It can read `awscli` _profile_ and has [MFA][2] support. As a minimal web server`kriskross` makes _MFA_ copy & paste from your mobile device less prone to 8 | errors. 9 | Learn about Cross-Account Access [in this very well written article][5]. 10 | 11 | _Hint: yes you **can** attach a cross account role to your own local account just by entering your own account id when creating the cross account role._ 12 | 13 | ### Configuration 14 | 15 | It uses an `~/.awsaccounts` _JSON_ file with the format: 16 | 17 | ``` 18 | { 19 | "target": { 20 | "account": "AWS account id", 21 | "role": "cross account access role name", 22 | "external-id": "optional external id for secure 3rd party access", 23 | "mfa": "optional MFA device serial number", 24 | "profile": "optional aws cli profile name for non-default direct access" 25 | }, 26 | { 27 | ... 28 | } 29 | } 30 | ``` 31 | 32 | An example `awsaccount` file would be: 33 | 34 | ``` 35 | { 36 | "myownaccount": { 37 | "account": "636487856791", 38 | "role": "MFAAdmin", 39 | "mfa": "arn:aws:iam::225011332614:mfa/MySelf" 40 | }, 41 | "childaccount": { 42 | "account": "287487895991", 43 | "role": "ChildAdmin", 44 | }, 45 | "thirdpartyaccount": { 46 | "account": "981036328202", 47 | "role": "ThirdParty", 48 | "external-id": "123456789" 49 | }, 50 | "thirdpartyaccountwithMFA": { 51 | "account": "123036328892", 52 | "role": "ThirdParty", 53 | "external-id": "123456789" 54 | "mfa": "arn:aws:iam::225011332614:mfa/MySelf" 55 | }, 56 | "myotheraccount": { 57 | "profile": "othercompany", 58 | "account": "123468236778", 59 | "role": "MFAAdmin", 60 | "mfa": "arn:aws:iam::678067632434:mfa/MySelf" 61 | } 62 | } 63 | ``` 64 | 65 | * The hash index is really a label, independent from your `~/.aws` configuration. 66 | * `account` is the account number to build the role from. 67 | * `role` is the role name. 68 | * `mfa` is the _MFA_ "serial device", actually the _ARN_ as shown in the _IAM_ console. 69 | * `external-id` is the arbitrary `id` you agreed to use with the third party. 70 | * `profile` is an _AWS_ `profile_name` you would like to pivot from. 71 | * `duration` is the role session duration, in seconds. Default duration is 3600, max session is 12 hours. 72 | * `private` might be set to `yes` if you'd like to open the console on a browser private window (only _Firefox_ or _Chrome_ by now) 73 | * This feature relies on a `BROWSER` variable containing the executable name for your favorite web browser. Either add `BROWSER=firefox; export BROWSER` (or `google-chrome`) to your shell profile file, or prepend `kriskross` execution with the variable set. 74 | 75 | A very simple, single account based file could be: 76 | 77 | ``` 78 | { 79 | "simpleaccount": { 80 | "account": "636487856791" 81 | "role": "myrole" 82 | } 83 | } 84 | ``` 85 | 86 | Assuming you created a role called `myrole` in your current account, with the same account `id` as the source account, yes, this works. This configuration relies on a `~/.aws/{credentials,config}` with a default section and the corresponding `aws_access_key_id` / `aws_secret_access_key` pair. 87 | 88 | There are many possible combinations: 89 | 90 | * Direct, own account access, by creating a cross account role with the local account id 91 | * Child account, by enabling cross account access 92 | * Third party account using an external id 93 | * Third party account using an external id and MFA 94 | * Direct account access using [awscli][1] profiles 95 | 96 | All with or without MFA (while enabling MFA is highly recommended). 97 | 98 | You may give an alternative path and name for the account properties file using the 99 | `--awsaccounts=` parameter. 100 | If the role uses a MFA device, specify it with the `--mfa` parameter along with a `mfa` key associated with the target in the `awsaccounts` file which value is the MFA device serial number. 101 | 102 | ### Usage 103 | 104 | From the command line: 105 | 106 | ``` 107 | kriskross.py [--awsaccounts= --mfa=] 108 | ``` 109 | 110 | Start as a foreground local web service ([Flask][3] default port is _5000_): 111 | 112 | ``` 113 | export FLASK_APP=kriskross.py 114 | python -m flask run --host=0.0.0.0 115 | ``` 116 | 117 | Or start it as a daemon with [gunicorn][4] (default port _8000_): 118 | 119 | ``` 120 | gunicorn -D -b 0.0.0.0 kriskross:app 121 | ``` 122 | 123 | ### History 124 | 125 | This script is vaguely derived from _AWS_: 126 | https://aws.amazon.com/blogs/security/how-to-enable-cross-account-access-to-the-aws-management-console/ 127 | and this `botocore` version: 128 | https://gist.githubusercontent.com/garnaat/10682964/raw/ef1caa152c006e33b54c0be8226f31ba35db331e/gistfile1.py 129 | 130 | [1]: https://github.com/aws/aws-cli 131 | [2]: https://aws.amazon.com/iam/details/mfa/ 132 | [3]: http://flask.pocoo.org/ 133 | [4]: http://gunicorn.org/ 134 | [5]: https://aws.amazon.com/blogs/security/how-to-enable-cross-account-access-to-the-aws-management-console/ 135 | -------------------------------------------------------------------------------- /mods/session.py: -------------------------------------------------------------------------------- 1 | 2 | '''Helper functions in order to easily manipulate AWS objects with `boto3`_ 3 | 4 | .. module:: Aws 5 | :platform: UNIX 6 | :synopsis: Ease access to boto3 and wraps many useful functions 7 | 8 | .. moduleauthor:: Emile 'iMil' Heitor 9 | 10 | .. _boto3: http://boto3.readthedocs.org/en/latest/ 11 | ''' 12 | 13 | import boto3 14 | import base64 15 | import requests 16 | 17 | class Aws: 18 | '''Aws class constructor 19 | 20 | :param str profile: Region profile, as defined in awscli configuration 21 | :param str t: Resource type, like ``ec2``, ``cloudformation``... 22 | 23 | :return: Access to resource, client and helpers 24 | ''' 25 | def __init__(self, profile, t): 26 | '''Init method 27 | ''' 28 | self.profile = profile 29 | if profile: 30 | self.session = boto3.Session(profile_name=profile) 31 | self.region = self.session._session.get_config_variable('region') 32 | if t: 33 | self.client = self.session.client(t) 34 | # some objects don't have resource (i.e. route53) 35 | try: 36 | self.resource = self.session.resource(t) 37 | except: 38 | pass 39 | 40 | def lsinstances(self, obj): 41 | '''Get all instances objects 42 | 43 | :param str obj: String form of resource type to get list from 44 | :return: All instances in object form 45 | ''' 46 | return getattr(self.resource, obj).all() 47 | 48 | def mktags(self, taglst): 49 | '''Makes a Filter-friendly tag list 50 | 51 | :param dict taglst: A dict of key / value pairs 52 | 53 | :return: Filter-friendly tag list 54 | :rtype: dict 55 | ''' 56 | tags = [] 57 | for t in taglst: 58 | tags.append({'Key': t, 'Value': taglst[t]}) 59 | return tags 60 | 61 | def tags2dict(self, tags): 62 | '''Converts a Filter tag list to a dict 63 | 64 | :param dict tags: A dict of Filter tag list 65 | 66 | :return: A simple key / value dict 67 | :rtype: dict 68 | ''' 69 | ret = {} 70 | for t in tags: 71 | ret[t['Key']] = t['Value'] 72 | return ret 73 | 74 | def get_id_from_nametag(self, res, tag): 75 | '''Returns a resource id matching a Name tag 76 | 77 | :param str obj: The resource to get the id from 78 | :param str tag: The Name tag 79 | 80 | :return: Resource id 81 | ''' 82 | for o in getattr(self.resource, res).filter( 83 | Filters=[{'Name': 'tag:Name', 'Values': [tag]}] 84 | ): 85 | return o.id 86 | 87 | return None 88 | 89 | def mkuserdata(self, b64 = False, userdata = [], name = '', netblock = ''): 90 | '''Merge userdata files and possibly convert it to ``base64`` 91 | 92 | :param boolean b64: Should we convert userdata to ``base64`` 93 | :param list userdata: A list of userdata files 94 | :param str name: Name to be passed as an argument to userdata 95 | :param str netblock: Netblock (CIDR) argument for userdata 96 | 97 | :return: Merged userdata files, possibly in ``base64`` 98 | :rtype: str 99 | ''' 100 | sh = '' 101 | for u in userdata: 102 | with open('userdata/{0}'.format(u), 'r') as f: 103 | sh = sh + f.read() 104 | sh = sh.format(self.profile, name.lower(), netblock) 105 | if b64 is False: 106 | return sh 107 | else: 108 | return base64.b64encode(sh) 109 | 110 | def gettagval(self, res, tag): 111 | '''Returns a tag value for a given resource 112 | 113 | :param str res: The resource to get the tag from 114 | :param str tag: Tag's name 115 | 116 | :return: Tag value 117 | :rtype: str 118 | ''' 119 | for t in res.tags: 120 | if t['Key'].lower() == tag.lower(): 121 | return t['Value'] 122 | return 'none' 123 | 124 | def create_tag(self, rid, k, v): 125 | '''Creates a tag entry for a given resource 126 | 127 | :param rid: Resource id 128 | :k: Tag key, will be titled (upper case first letter) 129 | :v: Tag value 130 | ''' 131 | self.resource.create_tags( 132 | Resources = [rid], 133 | Tags = self.mktags({ 134 | k.title(): v 135 | }) 136 | ) 137 | 138 | def lsinstnames(self): 139 | '''Returns a dict of instances ids and Name tag 140 | 141 | :return: Dict of ``key`` = ``id`` / ``value`` = ``Name tag`` 142 | ''' 143 | instances = self.resource.instances.all() 144 | instname = {} 145 | for i in instances: 146 | instname[i.id] = self.gettagval(i, 'Name') 147 | 148 | return instname 149 | 150 | def dmesg(self, name): 151 | '''Returns console output for a given instance ``id`` or Name tag 152 | 153 | :param str name: Instance ``id`` or Name tag 154 | 155 | :return: Console output 156 | :rtype: str 157 | ''' 158 | instances = self.lsinstnames() 159 | for i in instances: 160 | if name in i or name in instances[i]: 161 | return self.client.get_console_output(InstanceId=i)['Output'] 162 | 163 | def getamis(self, glob): 164 | '''Returns all AMI ids and creation date ordered by the latter 165 | 166 | :param str glob: An AMI name ``glob`` 167 | 168 | :return: Ordered list of AMI ids 169 | :rtype: list 170 | ''' 171 | imgs = {} 172 | for i in self.resource.images.filter( 173 | Filters = [{'Name': 'name', 'Values': [glob]}] 174 | ): 175 | imgs[i.id] = i.creation_date 176 | return sorted(imgs, key = imgs.get) 177 | 178 | def get_debian_ami(self, glob): 179 | '''Returns an AMI id matching with official debian wiki list 180 | 181 | :return: Latest AMI matching the ``glob`` 182 | :rtype: str 183 | ''' 184 | for rel in ['Wheezy', 'Jessie']: 185 | if rel.lower() in glob: 186 | break 187 | 188 | r = requests.get( 189 | 'https://wiki.debian.org/Cloud/AmazonEC2Image/{0}'.format(rel) 190 | ) 191 | for ami in self.getamis(glob): 192 | if ami in r.text: 193 | return ami 194 | 195 | def getami(self, glob): 196 | '''Returns the latest AMI matching ``glob`` 197 | 198 | :param str glob: An AMI name ``glob`` 199 | 200 | :return: Latest AMI matching the ``glob`` 201 | :rtype: str 202 | ''' 203 | return self.getamis(glob)[-1] 204 | 205 | def getinst(self, iid): 206 | '''Returns an instance resource 207 | 208 | :param str iid: Instance id 209 | 210 | :return: Instance resource 211 | ''' 212 | return self.resource.Instance(iid) 213 | 214 | def getall(self, res): 215 | '''Return all occurences for a resource 216 | 217 | :param str res: Resource name 218 | 219 | :return: List of all resources 220 | :rtype: list 221 | ''' 222 | return [i for i in getattr(self.resource, res).all()] 223 | 224 | def change_nsrecord(self, action, dnsrecord): 225 | '''Create, delete or modify a DNS record 226 | 227 | :param str action: One of ``CREATE``, ``DELETE`` or ``UPSERT`` 228 | :param dict dnsrecord: A dict describing the DNS record to change 229 | 230 | .. code-block:: python 231 | 232 | dnsrecord = { 233 | 'zone': 'foo.com', # domain name 234 | 'rectype': 'A'|'NS'|'CNAME'|'MX'|'PTR'|'SRV'|'SPF'|'AAAA', 235 | 'name': 'myhost', # host part only 236 | 'target': 'targetId'|'www.bar.com'|'10.0.0.1', 237 | 'ttl': 300, # do not add TTL for an alias target 238 | 'healthcheck': True, # optional, alias target only 239 | 'dnsname': 'foo-1.region.amazonaws.com' # alias target only 240 | } 241 | 242 | .. note:: 243 | 244 | For a ``targetId`` record, ``dnsname`` is the external DNS name AWS 245 | creates for the ELB, S3 bucket, CloudFront distribution or another 246 | route 53 resource on the same hosted zone. 247 | Also note that an ``AliasTarget`` must not have a ``TTL`` specified. 248 | 249 | Usage: 250 | 251 | .. code-block:: python 252 | 253 | obj.change_nsrecord('CREATE', dnsrecord) 254 | 255 | Documentation: 256 | 257 | * http://docs.aws.amazon.com/Route53/latest/APIReference/CreateAliasRRSAPI.html 258 | * http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-route53.html 259 | * http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-choosing-alias-non-alias.html 260 | 261 | ''' 262 | 263 | hzones = self.client.list_hosted_zones_by_name( 264 | DNSName = dnsrecord['zone'] 265 | ) 266 | zoneid = hzones['HostedZones'][0]['Id'] 267 | 268 | dnsname = '{0}.{1}.'.format(dnsrecord['name'], dnsrecord['zone']) 269 | 270 | change = { 271 | 'Action': action, 272 | 'ResourceRecordSet': { 273 | 'Name': dnsname, 274 | 'Type': dnsrecord['rectype'], 275 | } 276 | } 277 | 278 | if 'dnsname' in dnsrecord: # Alias target 279 | if not 'healthcheck' in dnsrecord: 280 | hc = False 281 | else: 282 | hc = dnsrecord['healthcheck'] 283 | change['ResourceRecordSet']['AliasTarget'] = { 284 | 'HostedZoneId': dnsrecord['target'], 285 | 'DNSName': dnsrecord['dnsname'], 286 | 'EvaluateTargetHealth': hc 287 | } 288 | change['ResourceRecordSet']['SetIdentifier'] = '{0}_{1}_{2}'.format( 289 | self.region, dnsrecord['name'], dnsrecord['zone'] 290 | ) 291 | change['ResourceRecordSet']['Region'] = self.region 292 | else: 293 | change['ResourceRecordSet']['TTL'] = dnsrecord['ttl'] 294 | change['ResourceRecordSet']['ResourceRecords'] = [ 295 | {'Value': dnsrecord['target']} 296 | ] 297 | 298 | cb = { 299 | 'Comment': '{0} / {1} / {2}'.format( 300 | action, dnsrecord['name'], dnsrecord['zone'] 301 | ), 302 | 'Changes': [change] 303 | } 304 | 305 | self.client.change_resource_record_sets( 306 | HostedZoneId = zoneid, 307 | ChangeBatch = cb 308 | ) 309 | -------------------------------------------------------------------------------- /mods/awsprice.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | '''Price and attributes retrieval module for EC2 and RDS AWS resources 4 | 5 | This module uses the HTML code within AWS website, which will most probably 6 | change from time to time, dont blindly rely on this module before checking it 7 | is still functionnal. 8 | 9 | AWS website has ``