1# Copyright 2015 Google Inc. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#    http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import copy
16import dateutil.parser
17import json
18import re
19
20import boto3
21
22import packer
23import service
24import util
25import validate
26
27AMI_CACHE = {}
28
29def amazon_get_amis(region_name, aws_access_key_id, aws_secret_access_key):
30    ec2 = boto3.client('ec2', region_name=region_name, aws_access_key_id=aws_access_key_id,
31                       aws_secret_access_key=aws_secret_access_key)
32    result = ec2.describe_images(Owners=['self'])
33    images = result['Images']
34    return [(i['Name'], dateutil.parser.parse(i['CreationDate'])) for i in images]
35
36def amazon_get_ami_by_name(region_name, aws_access_key_id, aws_secret_access_key, name):
37    ec2 = boto3.client('ec2', region_name=region_name, aws_access_key_id=aws_access_key_id,
38                       aws_secret_access_key=aws_secret_access_key)
39    result = ec2.describe_images(Owners=['self'], Filters=[{'Name': 'name', 'Values': [name]}])
40    images = result['Images']
41    assert len(images) == 1, util.jsonstr(images)
42    image = images[0]
43    return image['ImageId']
44
45
46class AmazonPackerBuildArtefact(packer.PackerBuildArtefact):
47    def __init__(self, image, environment):
48        super(AmazonPackerBuildArtefact, self).__init__(image['cmds'])
49
50        self.instanceType = image['instanceType']
51        self.sourceAmi = image['sourceAmi']
52
53        self.accessKey = environment['accessKey']
54        self.secretKey = environment['secretKey']
55        self.sshUser = image['sshUser']
56        self.region = environment['region']
57
58    def builderHashCode(self):
59        builder_hash = 0;
60        builder_hash ^= packer.hash_string(self.sourceAmi)
61        builder_hash ^= packer.hash_string(self.instanceType)
62        return builder_hash
63
64    def builder(self):
65        return {
66            'ami_name': self.name(),
67
68            'type': 'amazon-ebs',
69            'access_key': self.accessKey,
70            'secret_key': self.secretKey,
71            'region': self.region,
72            'source_ami': self.sourceAmi,
73            'instance_type': self.instanceType,
74            'ssh_username': self.sshUser,
75        }
76
77    def needsBuild(self):
78        print 'Checking if AMI exists: %s/%s' % (self.region, self.name())
79        if (self.region, self.accessKey) in AMI_CACHE:
80            existing_image_names = AMI_CACHE[self.region, self.accessKey]
81        else:
82            existing_image_names = [
83                img[0]
84                for img in amazon_get_amis(self.region, self.accessKey, self.secretKey)]
85            AMI_CACHE[self.region, self.accessKey] = existing_image_names
86        return self.name() not in existing_image_names
87
88    def doBuild(self, dirpath):
89        super(AmazonPackerBuildArtefact, self).doBuild(dirpath)
90        if self.accessKey not in AMI_CACHE:
91            AMI_CACHE[self.accessKey] = []
92        AMI_CACHE[self.accessKey] += [self.name()]
93
94    def postBuild(self):
95        self.id = amazon_get_ami_by_name(self.region, self.accessKey, self.secretKey, self.name())
96
97
98class AmazonService(service.Service):
99
100    def validateEnvironment(self, root, path):
101        fields = {'kind', 'accessKey', 'secretKey', 'region'}
102        validate.obj_only(root, path, fields)
103        validate.path_val(root, path + ['region'], 'string')
104        validate.path_val(root, path + ['accessKey'], 'string')
105        validate.path_val(root, path + ['secretKey'], 'string')
106
107    def validateService(self, root, path):
108        super(AmazonService, self).validateService(root, path)
109        infra_path = path + ['infrastructure']
110        validate.path_val(root, infra_path, 'object', {})
111        inst_path = infra_path + ['aws_instance']
112        instances = validate.path_val(root, inst_path, 'object', {})
113
114        # Validate image configs
115        for inst_name, inst in instances.iteritems():
116            self.validateCmds(root, inst_path + [inst_name, 'cmds'])
117            self.validateCmds(root, inst_path + [inst_name, 'bootCmds'])
118            image = inst.get('ami')
119            if isinstance(image, dict):
120                self.validateImage(root, inst_path + [inst_name, 'ami'])
121
122    def validateImage(self, root, path):
123        super(AmazonService, self).validateImage(root, path)
124        validate.path_val(root, path + ['instanceType'], 'string', 'n1-standard-1')
125        validate.path_val(root, path + ['sourceAmi'], 'string')
126        validate.path_val(root, path + ['sshUser'], 'string')
127        validate.obj_only(root, path, {'cmds', 'instanceType', 'sourceAmi', 'sshUser'})
128
129    def compileProvider(self, environment_name, environment):
130        return {
131            'environment.%s.tf.json' % environment_name: {
132                'provider': {
133                    'aws': {
134                        'alias': environment_name,
135                        'access_key': environment['accessKey'],
136                        'secret_key': environment['secretKey'],
137                        'region' : environment['region'],
138                    },
139                },
140            },
141        }
142
143    def getBuildArtefacts(self, environment, ctx, service):
144        service = copy.deepcopy(service)
145        barts = {}  # Build artefacts.
146        infra = service['infrastructure']
147
148        instances = infra.get('aws_instance', {})
149
150        # Process AMI configs
151        for inst_name, inst in instances.iteritems():
152            ami = inst['ami']
153            if isinstance(ami, dict):
154                bart = AmazonPackerBuildArtefact(ami, environment)
155                barts[bart.name()] = bart
156                inst['ami'] = bart.name()
157
158        return service, barts
159
160    def compile(self, ctx, service_name, service, barts):
161        infra = service['infrastructure']
162
163        # Add provider attributes
164        for res_kind_name, res_kind_obj in infra.iteritems():
165            for res_name, res in res_kind_obj.iteritems():
166                res['provider'] = 'aws.%s' % service.get('environment', 'default')
167
168        instances = infra.get('aws_instance', {})
169
170        # Convert AMI name to id
171        for inst_name, inst in instances.iteritems():
172            ami = inst['ami']
173            if ami in barts:
174                inst['ami'] = barts[ami].id
175
176        # Process commands
177        for inst_name, inst in instances.iteritems():
178            cmds = inst['cmds']
179            boot_cmds = inst['bootCmds']
180            if inst.get('user_data'):
181                raise RuntimeError('Cannot use user_data, use cmds instead.')
182            inst['user_data'] = self.compileStartupScript(cmds, boot_cmds)
183            inst.pop('cmds', None)
184            inst.pop('bootCmds', None)
185
186        return {
187            'service.%s.tf.json' % self.fullName(ctx + [service_name]): {
188                'resource': infra,
189                'output': {
190                    k: { 'value': v }
191                    for k, v in service['outputs'].iteritems()
192                }
193            }
194        }
195