Skip to content

Commit 68f40b3

Browse files
committed
Fixed #170 -- "deis nodes:create" allows adding external instances.
1 parent 8210ff9 commit 68f40b3

10 files changed

Lines changed: 159 additions & 6 deletions

File tree

api/models.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ class Provider(UuidAuditedModel):
107107
PROVIDERS = (
108108
('ec2', 'Amazon Elastic Compute Cloud (EC2)'),
109109
('mock', 'Mock Reference Provider'),
110+
('static', 'Static Node Provider'),
110111
)
111112

112113
owner = models.ForeignKey(settings.AUTH_USER_MODEL)
@@ -290,7 +291,7 @@ def destroy(self):
290291
@python_2_unicode_compatible
291292
class NodeManager(models.Manager):
292293

293-
def new(self, formation, layer):
294+
def new(self, formation, layer, fqdn=None):
294295
existing_nodes = self.filter(formation=formation, layer=layer).order_by('-created')
295296
if existing_nodes:
296297
next_num = existing_nodes[0].num + 1
@@ -300,7 +301,8 @@ def new(self, formation, layer):
300301
formation=formation,
301302
layer=layer,
302303
num=next_num,
303-
id="{0}-{1}-{2}".format(formation.id, layer.id, next_num))
304+
id="{0}-{1}-{2}".format(formation.id, layer.id, next_num),
305+
fqdn=fqdn)
304306
return node
305307

306308
def scale(self, formation, structure, **kwargs):

api/tests/auth.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import json
1010
import urllib
1111

12+
from django.conf import settings
1213
from django.test import TestCase
1314

1415

@@ -57,7 +58,7 @@ def test_auth(self):
5758
url = '/api/providers'
5859
response = self.client.get(url)
5960
self.assertEqual(response.status_code, 200)
60-
self.assertEqual(response.data['count'], 1)
61+
self.assertEqual(response.data['count'], len(settings.PROVIDER_MODULES))
6162
url = '/api/flavors'
6263
response = self.client.get(url)
6364
self.assertEqual(response.status_code, 200)

api/tests/node.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,43 @@ def test_node_actions(self):
194194
url = '/api/nodes/{id}/converge'.format(**node)
195195
response = self.client.post(url)
196196
self.assertEqual(response.status_code, 200)
197+
198+
def test_node_create(self):
199+
url = '/api/formations'
200+
body = {'id': 'autotest'}
201+
response = self.client.post(url, json.dumps(body), content_type='application/json')
202+
self.assertEqual(response.status_code, 201)
203+
formation_id = response.data['id']
204+
url = '/api/formations/{formation_id}/layers'.format(**locals())
205+
body = {'id': 'runtime', 'flavor': 'autotest', 'runtime': True}
206+
response = self.client.post(url, json.dumps(body), content_type='application/json')
207+
self.assertEqual(response.status_code, 201)
208+
# create a node for an existing instance
209+
url = '/api/formations/{formation_id}/nodes'.format(**locals())
210+
body = {'fqdn': 'example.com', 'layer': 'runtime'}
211+
response = self.client.post(url, json.dumps(body), content_type='application/json')
212+
self.assertEqual(response.status_code, 201)
213+
# create it again, expecting an error
214+
url = '/api/formations/{formation_id}/nodes'.format(**locals())
215+
body = {'fqdn': 'example.com', 'layer': 'runtime'}
216+
response = self.client.post(url, json.dumps(body), content_type='application/json')
217+
self.assertEqual(response.status_code, 409)
218+
# get our node
219+
url = '/api/formations/{formation_id}/nodes'.format(**locals())
220+
response = self.client.get(url)
221+
self.assertEqual(response.status_code, 200)
222+
self.assertEqual(response.data['count'], 1)
223+
node_id = response.data['results'][0]['id']
224+
url = '/api/formations/{formation_id}/nodes/{node_id}'.format(**locals())
225+
response = self.client.get(url)
226+
self.assertEqual(response.status_code, 200)
227+
self.assertEqual(node_id, response.data['id'])
228+
node = response.data
229+
# delete our node
230+
response = self.client.delete(url)
231+
self.assertEqual(response.status_code, 204)
232+
# check the node is gone
233+
url = '/api/formations/{formation_id}/nodes'.format(**locals())
234+
response = self.client.get(url)
235+
self.assertEqual(response.status_code, 200)
236+
self.assertEqual(response.data['count'], 0)

api/urls.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@
129129
130130
Retrieve a :class:`~api.models.Node` by its `id`.
131131
132+
.. http:post:: /api/formations/(string:id)/nodes/
133+
134+
Create a new :class:`~api.models.Node` for an existing instance.
135+
132136
.. http:delete:: /api/formations/(string:id)/nodes/(string:id)/
133137
134138
Destroy a :class:`~api.models.Node` by its `id`.
@@ -344,7 +348,7 @@
344348
views.FormationNodeViewSet.as_view({
345349
'get': 'retrieve', 'delete': 'destroy'})),
346350
url(r'^formations/(?P<id>[-_\w]+)/nodes/?',
347-
views.FormationNodeViewSet.as_view({'get': 'list'})),
351+
views.FormationNodeViewSet.as_view({'get': 'list', 'post': 'add'})),
348352
# formation actions
349353
url(r'^formations/(?P<id>[-_\w]+)/scale/?',
350354
views.FormationViewSet.as_view({'post': 'scale'})),

api/views.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
from rest_framework.response import Response
1717
from rest_framework.status import HTTP_400_BAD_REQUEST
1818

19-
from api import models, tasks
19+
from api import models
2020
from api import serializers
21+
from api import tasks
2122

2223

2324
class AnonymousAuthentication(BaseAuthentication):
@@ -232,6 +233,19 @@ def get_object(self, *args, **kwargs):
232233
obj = get_object_or_404(qs, id=self.kwargs['node'])
233234
return obj
234235

236+
def add(self, request, **kwargs):
237+
fqdn = request.DATA['fqdn']
238+
formation = models.Formation.objects.get(
239+
owner=self.request.user, id=self.kwargs['id'])
240+
layer = models.Layer.objects.get(
241+
owner=self.request.user, id=request.DATA['layer'])
242+
if self.model.objects.filter(fqdn=fqdn, formation=formation, layer=layer).exists():
243+
msg = "A node with fqdn={} already exists in the {} formation".format(fqdn, formation)
244+
return Response(data=msg, status=status.HTTP_409_CONFLICT)
245+
node = models.Node.objects.new(formation, layer, fqdn)
246+
node.build()
247+
return Response(status=status.HTTP_201_CREATED)
248+
235249
def destroy(self, request, **kwargs):
236250
node = self.get_object()
237251
node.destroy()

client/deis.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,6 +1336,31 @@ def nodes(self, args):
13361336
args = docopt(self.nodes_list.__doc__)
13371337
return self.nodes_list(args)
13381338

1339+
def nodes_create(self, args):
1340+
"""
1341+
Add an existing node to a formation.
1342+
1343+
Usage: deis nodes:create <formation> <fqdn> --layer=<layer>
1344+
"""
1345+
formation = args.get('<formation>')
1346+
fqdn, layer = args.get('<fqdn>'), args.get('--layer')
1347+
body = {'fqdn': fqdn, 'layer': layer}
1348+
sys.stdout.write("Creating node for {}... ".format(fqdn))
1349+
sys.stdout.flush()
1350+
try:
1351+
progress = TextProgress()
1352+
progress.start()
1353+
before = time.time()
1354+
response = self._dispatch('post', "/api/formations/{}/nodes".format(formation),
1355+
json.dumps(body))
1356+
finally:
1357+
progress.cancel()
1358+
progress.join()
1359+
if response.status_code == requests.codes.created: # @UndefinedVariable
1360+
print('done in {}s'.format(int(time.time() - before)))
1361+
else:
1362+
raise ResponseError(response)
1363+
13391364
def nodes_info(self, args):
13401365
"""
13411366
Print info about a particular node

docs/server/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Server Reference
3232

3333
provider.ec2
3434
provider.mock
35+
provider.static
3536

3637
web.urls
3738
web.views

docs/server/provider.static.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
:description: Python API Reference for the Deis provider.static module
2+
:keywords: deis, provider.static, python, celery, api
3+
4+
===============
5+
provider.static
6+
===============
7+
8+
.. contents::
9+
:local:
10+
.. currentmodule:: provider.static
11+
12+
.. automodule:: provider.static
13+
:members:
14+
:undoc-members:

provider/mock.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def build_node(node):
4949
:rtype: a tuple of (provider_id, fully_qualified_domain_name, metadata)
5050
"""
5151
provider_id = 'i-1234567'
52-
fqdn = 'localhost.localdomain.local'
52+
fqdn = node.get('fqdn') or 'localhost.localdomain.local'
5353
metadata = {'state': 'running'}
5454
return provider_id, fqdn, metadata
5555

provider/static.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""
2+
Deis cloud provider implementation for static nodes created outside Deis.
3+
"""
4+
5+
6+
def seed_flavors():
7+
"""Seed the database with default static flavor.
8+
9+
:rtype: list of dicts containing flavor data
10+
"""
11+
return [{
12+
'id': 'static',
13+
'provider': 'static',
14+
'params': {},
15+
}]
16+
17+
18+
def build_layer(layer):
19+
"""
20+
Build a layer.
21+
22+
:param layer: a dict containing formation, id, params, and creds info
23+
"""
24+
pass
25+
26+
27+
def destroy_layer(layer):
28+
"""
29+
Destroy a layer.
30+
31+
:param layer: a dict containing formation, id, params, and creds info
32+
"""
33+
pass
34+
35+
36+
def build_node(node):
37+
"""
38+
Build a node.
39+
40+
:param node: a dict containing formation, layer, params, and creds info.
41+
:rtype: a tuple of (provider_id, fully_qualified_domain_name, metadata)
42+
"""
43+
return ('static', node['fqdn'], {})
44+
45+
46+
def destroy_node(node):
47+
"""
48+
Destroy a node.
49+
50+
:param node: a dict containing a node's provider_id, params, and creds
51+
"""
52+
pass

0 commit comments

Comments
 (0)