Skip to content

Commit f306aad

Browse files
author
Gabriel Monroy
committed
move run_node to CM module, add container list/info endpoints, beef test coverage
1 parent 60bdbdd commit f306aad

11 files changed

Lines changed: 101 additions & 33 deletions

File tree

api/models.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -497,12 +497,10 @@ def logs(self):
497497
def run(self, command):
498498
"""Run a one-off command in an ephemeral app container."""
499499
nodes = self.formation.node_set.order_by('?')
500-
releases = self.release_set.order_by('-created')
501500
if not nodes:
502501
raise EnvironmentError('No nodes available to run command')
503-
if not releases:
504-
raise EnvironmentError('No release available to run command')
505-
node, release = nodes[0], releases[0]
502+
app_id, node = self.id, nodes[0]
503+
release = self.release_set.order_by('-created')[0]
506504
# prepare ssh command
507505
version = release.version
508506
docker_args = ' '.join(
@@ -513,7 +511,7 @@ def run(self, command):
513511
"`find /app/.profile.d/*.sh -type f`; do . $profile; done"
514512
command = "/bin/sh -c '{base_cmd} && {command}'".format(**locals())
515513
command = "sudo docker run {docker_args} {command}".format(**locals())
516-
return node.run(self, command)
514+
return node.run(command)
517515

518516

519517
@python_2_unicode_compatible

api/serializers.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,6 @@ class Meta:
139139
model = models.Layer
140140
read_only_fields = ('created', 'updated')
141141

142-
@property
143-
def data(self):
144-
"""Custom data property that removes secure fields"""
145-
d = super(LayerSerializer, self).data
146-
for f in []: # ('ssh_private_key',):
147-
if f in d:
148-
del d[f]
149-
return d
150-
151142

152143
class NodeSerializer(serializers.ModelSerializer):
153144
"""Serialize a :class:`~api.models.Node` model."""

api/tasks.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
from celery import task
66
from celery.canvas import group
77

8-
from api.ssh import connect_ssh, exec_ssh
98
from deis import settings
109
from provider import import_provider_module
1110

12-
# now that we've defined models that may be imported by celery tasks
1311
# import user-defined config management module
1412
CM = importlib.import_module(settings.CM_MODULE)
1513

@@ -54,10 +52,7 @@ def converge_node(node):
5452

5553
@task
5654
def run_node(node, command):
57-
ssh = connect_ssh(node.layer.ssh_username,
58-
node.fqdn, 22,
59-
node.layer.ssh_private_key)
60-
output, rc = exec_ssh(ssh, command, pty=True)
55+
output, rc = CM.run_node(node.flat(), command)
6156
return output, rc
6257

6358

api/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def send_patch(self, path, data='', content_type='application/octet-stream',
2727
RequestFactory.patch = construct_patch
2828
Client.patch = send_patch
2929

30+
from .app import * # noqa
3031
from .auth import * # noqa
3132
from .build import * # noqa
3233
from .config import * # noqa

api/tests/app.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,8 @@ def test_app_actions(self):
117117
if not os.path.exists(settings.DEIS_LOG_DIR):
118118
os.mkdir(settings.DEIS_LOG_DIR)
119119
path = os.path.join(settings.DEIS_LOG_DIR, app_id + '.log')
120-
try:
121-
os.remove(path) # cleanup any old log files
122-
except:
123-
pass
120+
if os.path.exists(path):
121+
os.remove(path)
124122
url = '/api/apps/{app_id}/logs'.format(**locals())
125123
response = self.client.post(url)
126124
self.assertEqual(response.status_code, 404)
@@ -133,12 +131,27 @@ def test_app_actions(self):
133131
self.assertEqual(response.data, FAKE_LOG_DATA)
134132
# test run
135133
url = '/api/apps/{app_id}/run'.format(**locals())
136-
body = {'commands': 'ls -al'}
134+
body = {'command': 'ls -al'}
137135
response = self.client.post(url, json.dumps(body), content_type='application/json')
138136
self.assertEqual(response.status_code, 200)
139-
self.assertIn('.gitignore', response.data[0])
140137
self.assertEqual(response.data[1], 0)
141138

139+
def test_app_errors(self):
140+
formation_id, app_id = 'autotest', 'autotest-errors'
141+
url = '/api/apps'
142+
body = {'formation': formation_id, 'id': app_id}
143+
response = self.client.post(url, json.dumps(body), content_type='application/json')
144+
self.assertEqual(response.status_code, 201)
145+
app_id = response.data['id'] # noqa
146+
url = '/api/formations/{formation_id}/scale'.format(**locals())
147+
body = {'proxy': 0, 'runtime': 0}
148+
response = self.client.post(url, json.dumps(body), content_type='application/json')
149+
self.assertEqual(response.status_code, 200)
150+
url = '/api/apps/{app_id}/run'.format(**locals())
151+
body = {'command': 'ls -al'}
152+
response = self.client.post(url, json.dumps(body), content_type='application/json')
153+
self.assertContains(response, 'No nodes available to run command', status_code=400)
154+
142155

143156
FAKE_LOG_DATA = """
144157
2013-08-15 12:41:25 [33454] [INFO] Starting gunicorn 17.5

api/tests/container.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def setUp(self):
6565
response = self.client.post(url, json.dumps(body), content_type='application/json')
6666
self.assertEqual(response.status_code, 200)
6767

68-
def test_container_scale(self):
68+
def test_container(self):
6969
url = '/api/apps'
7070
body = {'formation': 'autotest'}
7171
response = self.client.post(url, json.dumps(body), content_type='application/json')
@@ -89,6 +89,16 @@ def test_container_scale(self):
8989
response = self.client.get(url)
9090
self.assertEqual(response.status_code, 200)
9191
self.assertEqual(response.data['containers'], json.dumps(body))
92+
# test listing/retrieving container info
93+
url = "/api/apps/{app_id}/containers/web".format(**locals())
94+
response = self.client.get(url)
95+
self.assertEqual(response.status_code, 200)
96+
self.assertEqual(len(response.data['results']), 4)
97+
num = response.data['results'][0]['num']
98+
url = "/api/apps/{app_id}/containers/web/{num}".format(**locals())
99+
response = self.client.get(url)
100+
self.assertEqual(response.status_code, 200)
101+
self.assertEqual(response.data['num'], num)
92102
# scale down
93103
url = "/api/apps/{app_id}/scale".format(**locals())
94104
body = {'web': 2, 'worker': 1}
@@ -115,7 +125,7 @@ def test_container_scale(self):
115125
self.assertEqual(response.status_code, 200)
116126
self.assertEqual(response.data['containers'], json.dumps(body))
117127

118-
def test_container_scale_errors(self):
128+
def test_container_errors(self):
119129
url = '/api/apps'
120130
body = {'formation': 'autotest'}
121131
response = self.client.post(url, json.dumps(body), content_type='application/json')
@@ -126,7 +136,7 @@ def test_container_scale_errors(self):
126136
response = self.client.post(url, json.dumps(body), content_type='application/json')
127137
self.assertContains(response, 'Invalid scaling format', status_code=400)
128138

129-
def test_container_scale_single_layer(self):
139+
def test_container_single_layer(self):
130140
# create & scale a single layer formation
131141
response = self.client.post('/api/formations', json.dumps(
132142
{'id': 'single-layer', 'domain': 'localhost.localdomain'}),
@@ -191,7 +201,7 @@ def test_container_scale_single_layer(self):
191201
self.assertEqual(response.status_code, 200)
192202
self.assertEqual(response.data['containers'], json.dumps(body))
193203

194-
def test_container_scale_allocation(self):
204+
def test_container_allocation(self):
195205
url = '/api/apps'
196206
formation_id = 'autotest'
197207
body = {'formation': formation_id}

api/tests/node.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,32 @@ def test_node_scale_errors(self):
165165
body = {'runtime': 1}
166166
response = self.client.post(url, json.dumps(body), content_type='application/json')
167167
self.assertContains(response, 'No provider credentials available', status_code=400)
168+
169+
def test_node_actions(self):
170+
url = '/api/formations'
171+
body = {'id': 'autotest'}
172+
response = self.client.post(url, json.dumps(body), content_type='application/json')
173+
self.assertEqual(response.status_code, 201)
174+
formation_id = response.data['id']
175+
url = '/api/formations/{formation_id}/layers'.format(**locals())
176+
body = {'id': 'runtime', 'flavor': 'autotest', 'runtime': True}
177+
response = self.client.post(url, json.dumps(body), content_type='application/json')
178+
self.assertEqual(response.status_code, 201)
179+
url = '/api/formations/{formation_id}/scale'.format(**locals())
180+
body = {'runtime': 1}
181+
response = self.client.post(url, json.dumps(body), content_type='application/json')
182+
self.assertEqual(response.status_code, 200)
183+
# get our node
184+
url = '/api/formations/{formation_id}/nodes'.format(**locals())
185+
response = self.client.get(url)
186+
self.assertEqual(response.status_code, 200)
187+
self.assertEqual(response.data['count'], 1)
188+
node_id = response.data['results'][0]['id']
189+
url = '/api/formations/{formation_id}/nodes/{node_id}'.format(**locals())
190+
response = self.client.get(url)
191+
self.assertEqual(response.status_code, 200)
192+
self.assertEqual(node_id, response.data['id'])
193+
node = response.data
194+
url = '/api/nodes/{id}/converge'.format(**node)
195+
response = self.client.post(url)
196+
self.assertEqual(response.status_code, 200)

api/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,10 @@
300300
url(r'^apps/(?P<id>[a-z0-9-]+)/releases/?',
301301
views.AppReleaseViewSet.as_view({'get': 'list'})),
302302
# application infrastructure
303+
url(r'^apps/(?P<id>[a-z0-9-]+)/containers/(?P<type>[a-z0-9-]+)/(?P<num>[a-z0-9-]+)/?',
304+
views.AppContainerViewSet.as_view({'get': 'retrieve'})),
305+
url(r'^apps/(?P<id>[a-z0-9-]+)/containers/(?P<type>[a-z0-9-.]+)/?',
306+
views.AppContainerViewSet.as_view({'get': 'list'})),
303307
url(r'^apps/(?P<id>[a-z0-9-]+)/containers/?',
304308
views.AppContainerViewSet.as_view({'get': 'list'})),
305309
# application actions
@@ -315,6 +319,8 @@
315319
url(r'^apps/?',
316320
views.AppViewSet.as_view({'post': 'create', 'get': 'list'})),
317321
# nodes
322+
url(r'^nodes/(?P<node>[a-z0-9-]+)/converge/?',
323+
views.NodeViewSet.as_view({'post': 'converge'})),
318324
url(r'^nodes/(?P<node>[a-z0-9-]+)/?',
319325
views.NodeViewSet.as_view({
320326
'get': 'retrieve', 'delete': 'destroy'})),

api/views.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,11 @@ class NodeViewSet(FormationNodeViewSet):
242242
def get_queryset(self, **kwargs):
243243
return self.model.objects.filter(owner=self.request.user)
244244

245+
def converge(self, request, **kwargs):
246+
node = self.get_object()
247+
output, _ = node.converge()
248+
return Response(output, status=status.HTTP_200_OK, content_type='text/plain')
249+
245250

246251
class AppViewSet(OwnerViewSet):
247252
"""RESTful views for :class:`~api.models.App`."""
@@ -288,7 +293,11 @@ def logs(self, request, **kwargs):
288293

289294
def run(self, request, **kwargs):
290295
app = self.get_object()
291-
output_and_rc = app.run(request.DATA['commands'])
296+
command = request.DATA['command']
297+
try:
298+
output_and_rc = app.run(command)
299+
except EnvironmentError as e:
300+
return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
292301
return Response(output_and_rc, status=status.HTTP_200_OK,
293302
content_type='text/plain')
294303

@@ -376,9 +385,13 @@ class AppContainerViewSet(OwnerViewSet):
376385
def get_queryset(self, **kwargs):
377386
app = models.App.objects.get(
378387
owner=self.request.user, id=self.kwargs['id'])
379-
return self.model.objects.filter(owner=self.request.user, app=app)
388+
qs = self.model.objects.filter(owner=self.request.user, app=app)
389+
container_type = self.kwargs.get('type')
390+
if container_type:
391+
qs = qs.filter(type=container_type)
392+
return qs
380393

381394
def get_object(self, *args, **kwargs):
382395
qs = self.get_queryset(**kwargs)
383-
obj = qs.get(pk=self.kwargs['container'])
396+
obj = qs.get(num=self.kwargs['num'])
384397
return obj

cm/chef.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ def converge_node(node):
137137
return output, rc
138138

139139

140+
def run_node(node, command):
141+
ssh = connect_ssh(node.layer.ssh_username,
142+
node.fqdn, 22,
143+
node.layer.ssh_private_key)
144+
output, rc = exec_ssh(ssh, command, pty=True)
145+
return output, rc
146+
147+
140148
def converge_formation(formation):
141149
nodes = formation.node_set.all()
142150
subtasks = []

0 commit comments

Comments
 (0)