Repository: deis/deis

Issue:
Add `flavors:update` to CLI
It's implemented on the server but not yet wrapped in the CLI.



Discussion:
The --params arg should be treated as a JSON dict to _merge_ with the existing one, not replace it.


Resolving patch:
diff --git a/api/views.py b/api/views.py
--- a/api/views.py
+++ b/api/views.py
@@ -133,6 +133,19 @@ class FlavorViewSet(OwnerViewSet):
     serializer_class = serializers.FlavorSerializer
     lookup_field = 'id'
 
+    def update(self, request, *args, **kwargs):
+        """
+        Override default update behavior to ensure that the params
+        field is handled as a dict merge.
+        """
+        params = self.get_object().params
+        new_params = json.loads(request.DATA.get('params', '{}'))
+        params.update(new_params)
+        # remove param if we provided a null value
+        [params.pop(k) for k, v in params.items() if v is None]
+        request.DATA['params'] = json.dumps(params)
+        return super(FlavorViewSet, self).update(request, *args, **kwargs)
+
 
 class FormationViewSet(OwnerViewSet):
     """RESTful views for :class:`~api.models.Formation`."""
diff --git a/client/deis.py b/client/deis.py
--- a/client/deis.py
+++ b/client/deis.py
@@ -940,6 +940,27 @@ def flavors_list(self, args):
         else:
             raise ResponseError(response)
 
+    def flavors_update(self, args):
+        """
+        Update an existing node flavor
+
+        Usage: deis flavors:update <id> [<params>] [--provider=<provider>]
+        """
+        id_ = args.get('<id>')
+        body = {'id': id_}
+        params = args.get('<params>')
+        if params:
+            body['params'] = params
+        provider = args.get('--provider')
+        if provider:
+            body['provider'] = provider
+        response = self._dispatch(
+            'patch', '/api/flavors/{}'.format(id_), json.dumps(body))
+        if response.status_code == requests.codes.ok:  # @UndefinedVariable
+            print(json.dumps(response.json(), indent=2))
+        else:
+            raise ResponseError(response)
+
     def formations(self, args):
         """
         Valid commands for formations:


Test patch:
diff --git a/api/tests/flavor.py b/api/tests/flavor.py
--- a/api/tests/flavor.py
+++ b/api/tests/flavor.py
@@ -45,11 +45,6 @@ def test_flavor(self):
         url = "/api/flavors/{flavor_id}".format(**locals())
         response = self.client.get(url)
         self.assertEqual(response.status_code, 200)
-        new_params = {'instance_size': 't1.micro'}
-        body = {'params': json.dumps(new_params)}
-        response = self.client.patch(url, json.dumps(body), content_type='application/json')
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(json.loads(response.data['params']), new_params)
         response = self.client.delete(url)
         self.assertEqual(response.status_code, 204)
 
@@ -82,6 +77,7 @@ def test_flavor_update(self):
         params = {
             'size': 'c1.xlarge',
             'image': 'ami-c98d1bf9',
+            'zone': None
         }
         body = {'id': flavor_id, 'params': json.dumps(params)}
         response = self.client.patch(url, json.dumps(body), content_type='application/json')
@@ -95,7 +91,7 @@ def test_flavor_update(self):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.data['uuid'], uuid)
         params = json.loads(response.data['params'])
-        self.assertNotIn('region', params)
+        self.assertIn('region', params)
         self.assertNotIn('zone', params)
         self.assertEqual(params['size'], 'c1.xlarge')
         self.assertEqual(params['image'], 'ami-c98d1bf9')
diff --git a/client/tests/test_flavors.py b/client/tests/test_flavors.py
--- a/client/tests/test_flavors.py
+++ b/client/tests/test_flavors.py
@@ -30,6 +30,7 @@ def tearDownClass(cls):
         teardown(cls.username, cls.password)
 
     def test_create(self):
+        # create a new flavor
         id_ = "test-flavor-{}".format(uuid4().hex[:4])
         child = pexpect.spawn("{} flavors:create {} --provider={} --params='{}'".format(
             DEIS, id_, 'ec2',
@@ -37,15 +38,85 @@ def test_create(self):
         ))
         child.expect(id_)
         child.expect(pexpect.EOF)
+        # list the flavors and make sure it's in there
+        child = pexpect.spawn("{} flavors".format(DEIS))
+        child.expect(pexpect.EOF)
+        flavors = re.findall('([\w|-]+): .*', child.before)
+        self.assertIn(id_, flavors)
+        # delete the new flavor
         child = pexpect.spawn("{} flavors:delete {}".format(DEIS, id_))
         child.expect(pexpect.EOF)
         self.assertNotIn('Error', child.before)
 
-    # def test_update(self):
-    #     pass
+    def test_update(self):
+        # create a new flavor
+        id_ = "test-flavor-{}".format(uuid4().hex[:4])
+        child = pexpect.spawn("{} flavors:create {} --provider={} --params={}".format(
+            DEIS, id_, 'mock', '{}'))
+        child.expect(id_)
+        child.expect(pexpect.EOF)
+        # update the provider
+        child = pexpect.spawn("{} flavors:update {} --provider={}".format(DEIS, id_, 'ec2'))
+        child.expect(pexpect.EOF)
+        # update the params
+        child = pexpect.spawn("{} flavors:update {} {}".format(
+            DEIS, id_, "'{\"key1\": \"val1\"}'"))
+        child.expect(pexpect.EOF)
+        # test the flavor contents
+        child = pexpect.spawn("{} flavors:info {}".format(DEIS, id_))
+        child.expect(pexpect.EOF)
+        results = json.loads(child.before)
+        self.assertEqual('ec2', results['provider'])
+        self.assertIn('key1', results['params'])
+        self.assertIn('val1', results['params'])
+        # update the params and provider
+        child = pexpect.spawn("{} flavors:update {} {} --provider={}".format(
+            DEIS, id_, "'{\"key2\": \"val2\"}'", 'mock'))
+        child.expect(pexpect.EOF)
+        # test the flavor contents
+        child = pexpect.spawn("{} flavors:info {}".format(DEIS, id_))
+        child.expect(pexpect.EOF)
+        results = json.loads(child.before)
+        self.assertIn('key1', results['params'])
+        self.assertIn('val1', results['params'])
+        self.assertIn('key2', results['params'])
+        self.assertIn('val2', results['params'])
+        self.assertEqual('mock', results['provider'])
+        # update the params to remove a value
+        child = pexpect.spawn("{} flavors:update {} {}".format(
+            DEIS, id_, "'{\"key1\": null}'"))
+        child.expect(pexpect.EOF)
+        # test the flavor contents
+        child = pexpect.spawn("{} flavors:info {}".format(DEIS, id_))
+        child.expect(pexpect.EOF)
+        results = json.loads(child.before)
+        self.assertNotIn('key1', results['params'])
+        self.assertNotIn('val1', results['params'])
+        self.assertIn('key2', results['params'])
+        self.assertIn('val2', results['params'])
+        # delete the new flavor
+        child = pexpect.spawn("{} flavors:delete {}".format(DEIS, id_))
+        child.expect(pexpect.EOF)
+        self.assertNotIn('Error', child.before)
 
-    # def test_delete(self):
-    #     pass
+    def test_delete(self):
+        # create a new flavor
+        id_ = "test-flavor-{}".format(uuid4().hex[:4])
+        child = pexpect.spawn("{} flavors:create {} --provider={} --params='{}'".format(
+            DEIS, id_, 'ec2',
+            '{"region":"ap-southeast-2","image":"ami-d5f66bef","zone":"any","size":"m1.medium"}'
+        ))
+        child.expect(id_)
+        child.expect(pexpect.EOF)
+        # delete the new flavor
+        child = pexpect.spawn("{} flavors:delete {}".format(DEIS, id_))
+        child.expect(pexpect.EOF)
+        self.assertNotIn('Error', child.before)
+        # list the flavors and make sure it's not in there
+        child = pexpect.spawn("{} flavors".format(DEIS))
+        child.expect(pexpect.EOF)
+        flavors = re.findall('([\w|-]+): .*', child.before)
+        self.assertNotIn(id_, flavors)
 
     def test_list(self):
         child = pexpect.spawn("{} flavors".format(DEIS))
@@ -68,7 +139,6 @@ def test_info(self):
         # test that we received JSON results
         # TODO: There's some error here, but only when run as part of the
         # entire test suite?
-        print child.before
         results = json.loads(child.before)
         self.assertIn('created', results)
         self.assertIn('updated', results)