Repository: deis/deis

Issue:
Flavors should also contain AMI id
Currently we just have _region_ in flavors and default the AMI, but it should be part of the flavor as well.



Discussion:
Flavors should contain:
- AMI ID
- Instance Size
- Region
- Zone (specify "all")


Resolving patch:
diff --git a/api/models.py b/api/models.py
--- a/api/models.py
+++ b/api/models.py
@@ -124,7 +124,8 @@ def __str__(self):
 class FlavorManager(models.Manager):
     """Manage database interactions for :class:`Flavor`."""
 
-    def load_cloud_config_base(self):
+    @staticmethod
+    def load_cloud_config_base():
         """Read the base configuration file and return YAML data."""
         # load cloud-config-base yaml_
         _cloud_config_path = os.path.abspath(
@@ -137,30 +138,38 @@ def seed(self, user, **kwargs):
         """Seed the database with default Flavors for each cloud region."""
         # TODO: add optimized AMIs to default flavors
         flavors = (
-            {'id': 'ec2-us-east-1',
-             'provider': 'ec2',
-             'params': json.dumps({'region': 'us-east-1'})},
-            {'id': 'ec2-us-west-1',
-             'provider': 'ec2',
-             'params': json.dumps({'region': 'us-west-1'})},
-            {'id': 'ec2-us-west-2',
-             'provider': 'ec2',
-             'params': json.dumps({'region': 'us-west-2'})},
-            {'id': 'ec2-eu-west-1',
-             'provider': 'ec2',
-             'params': json.dumps({'region': 'eu-west-1'})},
-            {'id': 'ec2-ap-northeast-1',
-             'provider': 'ec2',
-             'params': json.dumps({'region': 'ap-northeast-1'})},
-            {'id': 'ec2-ap-southeast-1',
-             'provider': 'ec2',
-             'params': json.dumps({'region': 'ap-southeast-1'})},
-            {'id': 'ec2-ap-southeast-2',
-             'provider': 'ec2',
-             'params': json.dumps({'region': 'ap-southeast-2'})},
-            {'id': 'ec2-sa-east-1',
-             'provider': 'ec2',
-             'params': json.dumps({'region': 'sa-east-1'})},
+            {'id': 'ec2-us-east-1', 'provider': 'ec2',
+             'params': json.dumps({
+                 'region': 'us-east-1', 'image': Flavor.IMAGE_MAP['us-east-1'],
+                 'zone': 'any', 'size': 'm1.medium'})},
+            {'id': 'ec2-us-west-1', 'provider': 'ec2',
+             'params': json.dumps({
+                 'region': 'us-west-1', 'image': Flavor.IMAGE_MAP['us-west-1'],
+                 'zone': 'any', 'size': 'm1.medium'})},
+            {'id': 'ec2-us-west-2', 'provider': 'ec2',
+             'params': json.dumps({
+                 'region': 'us-west-2', 'image': Flavor.IMAGE_MAP['us-west-2'],
+                 'zone': 'any', 'size': 'm1.medium'})},
+            {'id': 'ec2-eu-west-1', 'provider': 'ec2',
+             'params': json.dumps({
+                 'region': 'eu-west-1', 'image': Flavor.IMAGE_MAP['eu-west-1'],
+                 'zone': 'any', 'size': 'm1.medium'})},
+            {'id': 'ec2-ap-northeast-1', 'provider': 'ec2',
+             'params': json.dumps({
+                 'region': 'ap-northeast-1', 'image': Flavor.IMAGE_MAP['ap-northeast-1'],
+                 'zone': 'any', 'size': 'm1.medium'})},
+            {'id': 'ec2-ap-southeast-1', 'provider': 'ec2',
+             'params': json.dumps({
+                 'region': 'ap-southeast-1', 'image': Flavor.IMAGE_MAP['ap-southeast-1'],
+                 'zone': 'any', 'size': 'm1.medium'})},
+            {'id': 'ec2-ap-southeast-2', 'provider': 'ec2',
+             'params': json.dumps({
+                 'region': 'ap-southeast-2', 'image': Flavor.IMAGE_MAP['ap-southeast-2'],
+                 'zone': 'any', 'size': 'm1.medium'})},
+            {'id': 'ec2-sa-east-1', 'provider': 'ec2',
+             'params': json.dumps({
+                 'region': 'sa-east-1', 'image': Flavor.IMAGE_MAP['sa-east-1'],
+                 'zone': 'any', 'size': 'm1.medium'})},
         )
         cloud_config = self.load_cloud_config_base()
         for flavor in flavors:
@@ -184,6 +193,19 @@ class Flavor(UuidAuditedModel):
     params = fields.ParamsField()
     init = fields.CloudInitField()
 
+    # Deis-optimized EC2 amis -- with 3.8 kernel, chef 11 deps,
+    # and large docker images (e.g. buildstep) pre-installed
+    IMAGE_MAP = {
+        'ap-northeast-1': 'ami-a57aeca4',
+        'ap-southeast-1': 'ami-e03a72b2',
+        'ap-southeast-2': 'ami-bd801287',
+        'eu-west-1': 'ami-d9d3cdad',
+        'sa-east-1': 'ami-a7df7bba',
+        'us-east-1': 'ami-e85a2081',
+        'us-west-1': 'ami-ac6942e9',
+        'us-west-2': 'ami-b55ac885',
+    }
+
     class Meta:
         unique_together = (('owner', 'id'),)
 
diff --git a/api/views.py b/api/views.py
--- a/api/views.py
+++ b/api/views.py
@@ -137,8 +137,14 @@ def create(self, request, **kwargs):
         request._data = request.DATA.copy()
         # set default cloud-init configuration
         if not 'init' in request.DATA:
-            request.DATA['init'] = models.FlavorManager().load_cloud_config_base()
-        return viewsets.ModelViewSet.create(self, request, **kwargs)
+            request.DATA['init'] = models.FlavorManager.load_cloud_config_base()
+        params = json.loads(request.DATA['params'])
+        params.setdefault('region', 'us-east-1')
+        params.setdefault('image', models.Flavor.IMAGE_MAP[params['region']])
+        params.setdefault('size', 'm1.medium')
+        params.setdefault('zone', 'any')
+        request.DATA['params'] = json.dumps(params)
+        return super(FlavorViewSet, self).create(request, **kwargs)
 
 
 class FormationViewSet(OwnerViewSet):
diff --git a/celerytasks/ec2.py b/celerytasks/ec2.py
--- a/celerytasks/ec2.py
+++ b/celerytasks/ec2.py
@@ -9,23 +9,11 @@
 import yaml
 
 from . import util
+from api.models import Flavor
 from api.models import Node
 from deis import settings
 from celerytasks.chef import ChefAPI
 
-# deis optimized amis -- with 3.8 kernel, chef 11 deps,
-# and large docker images (e.g. buildstep) pre-installed
-EC2_IMAGE_MAP = {
-    'ap-northeast-1': 'ami-a57aeca4',
-    'ap-southeast-1': 'ami-e03a72b2',
-    'ap-southeast-2': 'ami-bd801287',
-    'eu-west-1': 'ami-d9d3cdad',
-    'sa-east-1': 'ami-a7df7bba',
-    'us-east-1': 'ami-e85a2081',
-    'us-west-1': 'ami-ac6942e9',
-    'us-west-2': 'ami-b55ac885',
-}
-
 
 @task(name='ec2.build_layer')
 def build_layer(layer, creds, params):
@@ -74,7 +62,7 @@ def launch_node(node_id, creds, params, init, ssh_username, ssh_private_key):
     params.setdefault('security_groups', []).append(sg.name)
     # retrieve the ami for launching this node
     image_id = params.get(
-        'image', getattr(settings, 'EC2_IMAGE_MAP', EC2_IMAGE_MAP)[region])
+        'image', getattr(settings, 'IMAGE_MAP', Flavor.IMAGE_MAP)[region])
     images = conn.get_all_images([image_id])
     if len(images) != 1:
         raise LookupError('Could not find AMI: %s' % image_id)


Test patch:
diff --git a/api/tests/build.py b/api/tests/build.py
--- a/api/tests/build.py
+++ b/api/tests/build.py
@@ -32,7 +32,7 @@ def setUp(self):
         body = {
             'id': 'autotest',
             'provider': 'autotest',
-            'params': json.dumps({'region': 'us-west-2', 'instance_size': 'm1.medium'}),
+            'params': json.dumps({'region': 'us-west-2'}),
         }
         response = self.client.post(url, json.dumps(body), content_type='application/json')
         self.assertEqual(response.status_code, 201)
diff --git a/api/tests/config.py b/api/tests/config.py
--- a/api/tests/config.py
+++ b/api/tests/config.py
@@ -27,7 +27,7 @@ def setUp(self):
         self.assertEqual(response.status_code, 201)
         url = '/api/flavors'
         body = {'id': 'autotest', 'provider': 'autotest',
-                'params': json.dumps({'region': 'us-west-2', 'instance_size': 'm1.medium'})}
+                'params': json.dumps({'region': 'us-west-2'})}
         response = self.client.post(url, json.dumps(body), content_type='application/json')
         self.assertEqual(response.status_code, 201)
 
diff --git a/api/tests/flavor.py b/api/tests/flavor.py
--- a/api/tests/flavor.py
+++ b/api/tests/flavor.py
@@ -50,3 +50,22 @@ def test_flavor(self):
         self.assertEqual(yaml.safe_load(response.data['init']), new_init)
         response = self.client.delete(url)
         self.assertEqual(response.status_code, 204)
+
+    def test_flavor_contents(self):
+        """Tests that flavors explicitly contain AMI ID, instance size, region, and zone."""
+        url = '/api/flavors'
+        body = {'id': 'autotest', 'provider': 'autotest', 'params': json.dumps({})}
+        response = self.client.post(url, json.dumps(body), content_type='application/json')
+        self.assertEqual(response.status_code, 201)
+        flavor_id = response.data['id']
+        response = self.client.get('/api/flavors')
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(len(response.data['results']), 1)
+        url = "/api/flavors/{flavor_id}".format(**locals())
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        params = json.loads(response.data['params'])
+        self.assertEqual(params['region'], 'us-east-1')
+        self.assertEqual(params['zone'], 'any')
+        self.assertEqual(params['size'], 'm1.medium')
+        self.assertTrue(params['image'])