Skip to content

Commit bd2ba0a

Browse files
author
Gabriel Monroy
committed
publish SSH Keys to etcd instead of a chef data bag
1 parent d42cee2 commit bd2ba0a

5 files changed

Lines changed: 58 additions & 17 deletions

File tree

api/models.py

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@
1919
from django.dispatch import receiver
2020
from django.dispatch.dispatcher import Signal
2121
from django.utils.encoding import python_2_unicode_compatible
22+
import etcd
2223
from guardian.shortcuts import get_users_with_perms
2324
from json_field.fields import JSONField # @UnusedImport
2425

2526
from api import fields, tasks
2627
from provider import import_provider_module
27-
from utils import dict_diff
28+
from utils import dict_diff, fingerprint
2829

2930

3031
logger = logging.getLogger(__name__)
@@ -907,16 +908,6 @@ def _publish_to_cm(**kwargs):
907908
kwargs['instance'].publish()
908909

909910

910-
def _publish_user_to_cm(**kwargs):
911-
if kwargs.get('update_fields') == frozenset(['last_login']):
912-
return
913-
kwargs['instance'].publish()
914-
915-
916-
def _purge_user_from_cm(**kwargs):
917-
kwargs['instance'].purge()
918-
919-
920911
def _log_build_created(**kwargs):
921912
if kwargs.get('created'):
922913
build = kwargs['instance']
@@ -934,13 +925,41 @@ def _log_config_updated(**kwargs):
934925
log_event(config.app, "Config {} updated".format(config))
935926

936927

928+
def _etcd_publish_key(**kwargs):
929+
key = kwargs['instance']
930+
_etcd_client.write('/deis/builder/users/{}/{}'.format(
931+
key.owner.username, fingerprint(key.public)), key.public)
932+
933+
934+
def _etcd_purge_key(**kwargs):
935+
key = kwargs['instance']
936+
_etcd_client.delete('/deis/builder/users/{}/{}'.format(
937+
key.owner.username, fingerprint(key.public)))
938+
939+
940+
def _etcd_purge_user(**kwargs):
941+
username = kwargs['instance'].username
942+
_etcd_client.delete('/deis/builder/users/{}'.format(username), dir=True, recursive=True)
943+
944+
937945
# Connect Django model signals
938946
# Sync database updates with the configuration management backend
939947
post_save.connect(_publish_to_cm, sender=App, dispatch_uid='api.models')
940948
post_save.connect(_publish_to_cm, sender=Formation, dispatch_uid='api.models')
941-
post_save.connect(_publish_user_to_cm, sender=User, dispatch_uid='api.models')
942-
post_delete.connect(_purge_user_from_cm, sender=User, dispatch_uid='api.models')
943949
# Log significant app-related events
944950
post_save.connect(_log_build_created, sender=Build, dispatch_uid='api.models')
945951
post_save.connect(_log_release_created, sender=Release, dispatch_uid='api.models')
946952
post_save.connect(_log_config_updated, sender=Config, dispatch_uid='api.models')
953+
954+
# wire up etcd publishing if we can connect
955+
try:
956+
_etcd_client = etcd.Client(host=settings.ETCD_HOST, port=int(settings.ETCD_PORT))
957+
_etcd_client.get('/deis')
958+
except etcd.EtcdException:
959+
logger.log(logging.WARNING, 'Cannot synchronize with etcd cluster')
960+
_etcd_client = None
961+
962+
if _etcd_client:
963+
post_save.connect(_etcd_publish_key, sender=Key, dispatch_uid='api.models')
964+
post_delete.connect(_etcd_purge_key, sender=Key, dispatch_uid='api.models')
965+
post_delete.connect(_etcd_purge_user, sender=User, dispatch_uid='api.models')

api/tests/test_key.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@
1616
from deis import settings
1717

1818

19+
PUBKEY = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCfQkkUUoxpvcNMkvv7jqnfodgs37M2eBO" \
20+
"APgLK+KNBMaZaaKB4GF1QhTCMfFhoiTW3rqa0J75bHJcdkoobtTHlK8XUrFqsquWyg3XhsT" \
21+
"Yr/3RQQXvO86e2sF7SVDJqVtpnbQGc5SgNrHCeHJmf5HTbXSIjCO/AJSvIjnituT/SIAMGe" \
22+
"Bw0Nq/iSltwYAek1hiKO7wSmLcIQ8U4A00KEUtalaumf2aHOcfjgPfzlbZGP0S0cuBwSqLr" \
23+
"8b5XGPmkASNdUiuJY4MJOce7bFU14B7oMAy2xacODUs1momUeYtGI9T7X2WMowJaO7tP3Gl" \
24+
"sgBMP81VfYTfYChAyJpKp2yoP autotest@autotesting comment"
25+
26+
1927
@override_settings(CELERY_ALWAYS_EAGER=True)
2028
class KeyTest(TestCase):
2129

@@ -32,7 +40,7 @@ def test_key(self):
3240
Test that a user can add, remove and manage their SSH public keys
3341
"""
3442
url = '/api/keys'
35-
body = {'id': 'mykey@box.local', 'public': 'ssh-rsa XXX'}
43+
body = {'id': 'mykey@box.local', 'public': PUBKEY}
3644
response = self.client.post(url, json.dumps(body), content_type='application/json')
3745
self.assertEqual(response.status_code, 201)
3846
key_id = response.data['id']
@@ -52,7 +60,7 @@ def test_key_cm(self):
5260
Test that creating and deleting a key updates configuration management
5361
"""
5462
url = '/api/keys'
55-
body = {'id': 'mykey@box.local', 'public': 'ssh-rsa XXX'}
63+
body = {'id': 'mykey@box.local', 'public': PUBKEY}
5664
response = self.client.post(url, json.dumps(body), content_type='application/json')
5765
self.assertEqual(response.status_code, 201)
5866
key_id = response.data['id']
@@ -75,7 +83,7 @@ def test_key_duplicate(self):
7583
Test that a user cannot add a duplicate key
7684
"""
7785
url = '/api/keys'
78-
body = {'id': 'mykey@box.local', 'public': 'ssh-rsa XXX'}
86+
body = {'id': 'mykey@box.local', 'public': PUBKEY}
7987
response = self.client.post(url, json.dumps(body), content_type='application/json')
8088
self.assertEqual(response.status_code, 201)
8189
response = self.client.post(url, json.dumps(body), content_type='application/json')

api/utils.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""
22
Helper functions used by the Deis server.
33
"""
4-
4+
import base64
5+
import hashlib
56
import random
67

78

@@ -98,6 +99,15 @@ def dict_diff(dict1, dict2):
9899
return {k: diff[k] for k in diff if diff[k]}
99100

100101

102+
def fingerprint(key):
103+
"""
104+
Return the fingerprint for an SSH Public Key
105+
"""
106+
key = base64.b64decode(key.strip().split()[1].encode('ascii'))
107+
fp_plain = hashlib.md5(key).hexdigest()
108+
return ':'.join(a + b for a, b in zip(fp_plain[::2], fp_plain[1::2]))
109+
110+
101111
if __name__ == "__main__":
102112
import doctest
103113
doctest.testmod()

deis/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,9 @@
276276
# N is number of nodes in largest formation
277277
CELERYD_CONCURRENCY = 8
278278

279+
# etcd settings
280+
ETCD_HOST, ETCD_PORT = os.environ.get('ETCD', '127.0.0.1:4001').split(',')[0].split(':')
281+
279282
# default deis settings
280283
DEIS_LOG_DIR = os.path.abspath(os.path.join(__file__, '..', '..', 'logs'))
281284
LOG_LINES = 1000

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ gunicorn==18.0
1313
paramiko==1.12.1
1414
psycopg2==2.5.2
1515
pycrypto==2.6.1
16+
python-etcd==0.3.0
1617
pyrax==1.6.2
1718
PyYAML==3.10
1819
redis==2.8.0

0 commit comments

Comments
 (0)