Merge lp:~gandelman-a/charms/precise/keystone/ssl_file_sync into lp:~openstack-charmers/charms/precise/keystone/ha-support

Proposed by Adam Gandelman
Status: Merged
Merged at revision: 57
Proposed branch: lp:~gandelman-a/charms/precise/keystone/ssl_file_sync
Merge into: lp:~openstack-charmers/charms/precise/keystone/ha-support
Diff against target: 843 lines (+643/-20)
6 files modified
config.yaml (+4/-0)
hooks/keystone-hooks (+74/-5)
hooks/keystone_ssl.py (+298/-0)
hooks/lib/unison.py (+215/-0)
hooks/utils.py (+51/-14)
revision (+1/-1)
To merge this branch: bzr merge lp:~gandelman-a/charms/precise/keystone/ssl_file_sync
Reviewer Review Type Date Requested Status
James Page Approve
Review via email: mp+150390@code.launchpad.net

Description of the change

Adds support for Keystone managed SSL certificates for service endpoints. Adds a keystone_ssl.py library that will initialize a local directory as what will be used as the certificate authority. In addition to service credentials, new services that join via the identity-service relation will also receive a signed SSL certificate, key and CA cert. The remote service charm is in charge of using this cert/key to configure its own HTTPS. When it has done so, it can update its endpoint in keystone catalog via its relation. Every service now has an optional ssl_cert + ssl_key config option in case admin wants to update certificates manually, override those installed by keystone, or use certificates signed by a real certificate authority.

The KS certificate authority needs to be synchronized among all peer keystone units. To acehive this, I've added a new unison.py library that takes care of setting up peer SSH access via an intermediate user, and allows for file and directory synchronization between peers using unison. It's assumed that only one node will be synchornizing files to peers (the eligible_leader). I've replaced the old file-sync-via-base64-relation-data for service passwords with this new method as well, and it is working well.

It is not required that the remote charms support HTTPS endpoint reconfiguration, they can simply choose not to use the generated certificate data. But the reviews for charms that support this are filed:

https://code.launchpad.net/~gandelman-a/charms/precise/nova-cloud-controller/https_endpoint/+merge/150387
https://code.launchpad.net/~gandelman-a/charms/precise/cinder/https_endpoint/+merge/150383
https://code.launchpad.net/~gandelman-a/charms/precise/glance/https_endpoint/+merge/150382
https://code.launchpad.net/~gandelman-a/charms/precise/nova-compute/https_endpoint/+merge/150381

To post a comment you must log in.
Revision history for this message
Adam Gandelman (gandelman-a) wrote :

Also:

- This allows HTTPS to be turned on/off at will for the entire catalog via the config setting in keystone, if all services are using KS-managed SSL certs/keys.

- I should not that these changes do not yet take care of HTTPS for the actual keystone endpoint (OS_AUTH_URL). Only for the corresponding services in the catalog. If the approach proposed here works for other services, it should be easy to port to the keystone charm and have it manage its own endpoint similarly.

- After enabling HTTPs, many client tools will fail SSL verification and fail to interact with the API servers. Clients need to add the CA cert to their local system, eg:

$ curl http://$NOVA_CC_HOST/keystone_juju_ca_cert.crt | sudo tee /usr/local/share/ca-certificates/ks.crt && sudo update-ca-certificates

Revision history for this message
James Page (james-page) wrote :

Tested OK!

review: Approve
60. By Adam Gandelman

Rebase against current ha-support branch.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'config.yaml'
2--- config.yaml 2013-01-22 17:49:36 +0000
3+++ config.yaml 2013-03-08 21:36:24 +0000
4@@ -112,3 +112,7 @@
5 default: "false"
6 type: string
7 description: "Enable PKI token signing (Grizzly and beyond)"
8+ https-service-endpoints:
9+ default: "False"
10+ type: string
11+ description: "Manage SSL certificates for all service endpoints."
12
13=== added symlink 'hooks/cluster-relation-joined'
14=== target is u'keystone-hooks'
15=== modified file 'hooks/keystone-hooks'
16--- hooks/keystone-hooks 2013-02-22 19:20:54 +0000
17+++ hooks/keystone-hooks 2013-03-08 21:36:24 +0000
18@@ -2,13 +2,18 @@
19
20 import sys
21 import time
22+import urlparse
23+
24+from base64 import b64encode
25
26 from utils import *
27+
28 from lib.openstack_common import *
29+import lib.unison as unison
30
31 config = config_get()
32
33-packages = "keystone python-mysqldb pwgen haproxy python-jinja2"
34+packages = "keystone python-mysqldb pwgen haproxy python-jinja2 openssl unison"
35 service = "keystone"
36
37 # used to verify joined services are valid openstack components.
38@@ -81,6 +86,11 @@
39 execute("service keystone stop", echo=True)
40 execute("keystone-manage db_sync")
41 execute("service keystone start", echo=True)
42+
43+ # ensure /var/lib/keystone is g+wrx for peer relations that
44+ # may be syncing data there via SSH_USER.
45+ execute("chmod -R g+wrx /var/lib/keystone/")
46+
47 time.sleep(5)
48 ensure_initial_admin(config)
49
50@@ -97,6 +107,7 @@
51 'db_host' not in relation_data):
52 juju_log("db_host or password not set. Peer not ready, exit 0")
53 exit(0)
54+
55 update_config_block('sql', connection="mysql://%s:%s@%s/%s" %
56 (config["database-user"],
57 relation_data["password"],
58@@ -125,6 +136,21 @@
59 (id, unit))
60 identity_changed(relation_id=id, remote_unit=unit)
61
62+def ensure_valid_service(service):
63+ if service not in valid_services.keys():
64+ juju_log("WARN: Invalid service requested: '%s'" % service)
65+ realtion_set({ "admin_token": -1 })
66+ return
67+
68+def add_endpoint(region, service, public_url, admin_url, internal_url):
69+ desc = valid_services[service]["desc"]
70+ service_type = valid_services[service]["type"]
71+ create_service_entry(service, service_type, desc)
72+ create_endpoint_template(region=region, service=service,
73+ public_url=public_url,
74+ admin_url=admin_url,
75+ internal_url=internal_url)
76+
77 def identity_joined():
78 """ Do nothing until we get information about requested service """
79 pass
80@@ -170,7 +196,6 @@
81 'internal_url'])
82 if single.issubset(settings):
83 # other end of relation advertised only one endpoint
84-
85 if 'None' in [v for k,v in settings.iteritems()]:
86 # Some backend services advertise no endpoint but require a
87 # hook execution to update auth strategy.
88@@ -191,11 +216,14 @@
89
90
91 ensure_valid_service(settings['service'])
92+
93 add_endpoint(region=settings['region'], service=settings['service'],
94 publicurl=settings['public_url'],
95 adminurl=settings['admin_url'],
96 internalurl=settings['internal_url'])
97 service_username = settings['service']
98+ https_cn = urlparse.urlparse(settings['internal_url'])
99+ https_cn = https_cn.hostname
100 else:
101 # assemble multiple endpoints from relation data. service name
102 # should be prepended to setting name, ie:
103@@ -217,10 +245,11 @@
104 for k,v in settings.iteritems():
105 ep = k.split('_')[0]
106 x = '_'.join(k.split('_')[1:])
107- if ep not in endpoints:
108+ if ep not in endpoints:
109 endpoints[ep] = {}
110 endpoints[ep][x] = v
111 services = []
112+ https_cn = None
113 for ep in endpoints:
114 # weed out any unrelated relation stuff Juju might have added
115 # by ensuring each possible endpiont has appropriate fields
116@@ -233,6 +262,9 @@
117 adminurl=ep['admin_url'],
118 internalurl=ep['internal_url'])
119 services.append(ep['service'])
120+ if not https_cn:
121+ https_cn = urlparse.urlparse(ep['internal_url'])
122+ https_cn = https_cn.hostname
123 service_username = '_'.join(services)
124
125 if 'None' in [v for k,v in settings.iteritems()]:
126@@ -262,8 +294,16 @@
127 "auth_port": config["admin-port"],
128 "service_username": service_username,
129 "service_password": service_password,
130- "service_tenant": config['service-tenant']
131+ "service_tenant": config['service-tenant'],
132+ "https_keystone": "False",
133+ "ssl_cert": "",
134+ "ssl_key": "",
135+ "ca_cert": ""
136 }
137+
138+ if relation_id:
139+ relation_data['rid'] = relation_id
140+
141 # Check if clustered and use vip + haproxy ports if so
142 if is_clustered():
143 relation_data["auth_host"] = config['vip']
144@@ -271,7 +311,19 @@
145 relation_data["service_host"] = config['vip']
146 relation_data["service_port"] = SERVICE_PORTS['keystone_service']
147
148- relation_set(relation_data)
149+ # generate or get a new cert/key for service if set to manage certs.
150+ if config['https-service-endpoints'] in ['True', 'true']:
151+ ca = get_ca(user=SSH_USER)
152+ service = os.getenv('JUJU_REMOTE_UNIT').split('/')[0]
153+ cert, key = ca.get_cert_and_key(common_name=https_cn)
154+ ca_bundle= ca.get_ca_bundle()
155+ relation_data['ssl_cert'] = b64encode(cert)
156+ relation_data['ssl_key'] = b64encode(key)
157+ relation_data['ca_cert'] = b64encode(ca_bundle)
158+ relation_data['https_keystone'] = 'True'
159+ unison.sync_to_peers(peer_interface='cluster',
160+ paths=[SSL_DIR], user=SSH_USER, verbose=True)
161+ relation_set_2(**relation_data)
162 synchronize_service_credentials()
163
164 def config_changed():
165@@ -318,12 +370,24 @@
166 "keystone_service": int(config['service-port']) + 1
167 }
168
169+def cluster_joined():
170+ unison.ssh_authorized_peers(user=SSH_USER,
171+ group='keystone',
172+ peer_interface='cluster',
173+ ensure_user=True)
174
175 def cluster_changed():
176+ unison.ssh_authorized_peers(user=SSH_USER,
177+ group='keystone',
178+ peer_interface='cluster',
179+ ensure_user=True)
180 cluster_hosts = {}
181 cluster_hosts['self'] = config['hostname']
182 for r_id in relation_ids('cluster'):
183 for unit in relation_list(r_id):
184+ # trigger identity-changed to reconfigure HTTPS
185+ # as necessary.
186+ identity_changed(relation_id=r_id, remote_unit=unit)
187 cluster_hosts[unit.replace('/','-')] = \
188 relation_get_dict(relation_id=r_id,
189 remote_unit=unit)['private-address']
190@@ -332,6 +396,10 @@
191
192 synchronize_service_credentials()
193
194+ for r_id in relation_ids('identity-service'):
195+ for unit in relation_list(r_id):
196+ # trigger identity-changed to reconfigure HTTPS as necessary
197+ identity_changed(relation_id=r_id, remote_unit=unit)
198
199 def ha_relation_changed():
200 relation_data = relation_get_dict()
201@@ -400,6 +468,7 @@
202 "identity-service-relation-joined": identity_joined,
203 "identity-service-relation-changed": identity_changed,
204 "config-changed": config_changed,
205+ "cluster-relation-joined": cluster_joined,
206 "cluster-relation-changed": cluster_changed,
207 "cluster-relation-departed": cluster_changed,
208 "ha-relation-joined": ha_relation_joined,
209
210=== added file 'hooks/keystone_ssl.py'
211--- hooks/keystone_ssl.py 1970-01-01 00:00:00 +0000
212+++ hooks/keystone_ssl.py 2013-03-08 21:36:24 +0000
213@@ -0,0 +1,298 @@
214+#!/usr/bin/python
215+
216+import base64
217+import os
218+import shutil
219+import subprocess
220+import tarfile
221+import tempfile
222+from utils import *
223+
224+CA_EXPIRY = '365'
225+ORG_NAME = 'Ubuntu'
226+ORG_UNIT = 'Ubuntu Cloud'
227+CA_BUNDLE='/usr/local/share/ca-certificates/juju_ca_cert.crt'
228+
229+CA_CONFIG = """
230+[ ca ]
231+default_ca = CA_default
232+
233+[ CA_default ]
234+dir = %(ca_dir)s
235+policy = policy_match
236+database = $dir/index.txt
237+serial = $dir/serial
238+certs = $dir/certs
239+crl_dir = $dir/crl
240+new_certs_dir = $dir/newcerts
241+certificate = $dir/cacert.pem
242+private_key = $dir/private/cacert.key
243+RANDFILE = $dir/private/.rand
244+default_md = default
245+
246+[ req ]
247+default_bits = 1024
248+default_md = sha1
249+
250+prompt = no
251+distinguished_name = ca_distinguished_name
252+
253+x509_extensions = ca_extensions
254+
255+[ ca_distinguished_name ]
256+organizationName = %(org_name)s
257+organizationalUnitName = %(org_unit_name)s Certificate Authority
258+commonName = %(common_name)s
259+
260+[ policy_match ]
261+countryName = optional
262+stateOrProvinceName = optional
263+organizationName = match
264+organizationalUnitName = optional
265+commonName = supplied
266+
267+[ ca_extensions ]
268+basicConstraints = critical,CA:true
269+subjectKeyIdentifier = hash
270+authorityKeyIdentifier = keyid:always, issuer
271+keyUsage = cRLSign, keyCertSign
272+"""
273+
274+SIGNING_CONFIG="""
275+[ ca ]
276+default_ca = CA_default
277+
278+[ CA_default ]
279+dir = %(ca_dir)s
280+policy = policy_match
281+database = $dir/index.txt
282+serial = $dir/serial
283+certs = $dir/certs
284+crl_dir = $dir/crl
285+new_certs_dir = $dir/newcerts
286+certificate = $dir/cacert.pem
287+private_key = $dir/private/cacert.key
288+RANDFILE = $dir/private/.rand
289+default_md = default
290+
291+[ req ]
292+default_bits = 1024
293+default_md = sha1
294+
295+prompt = no
296+distinguished_name = req_distinguished_name
297+
298+x509_extensions = req_extensions
299+
300+[ req_distinguished_name ]
301+organizationName = %(org_name)s
302+organizationalUnitName = %(org_unit_name)s Server Farm
303+
304+[ policy_match ]
305+countryName = optional
306+stateOrProvinceName = optional
307+organizationName = match
308+organizationalUnitName = optional
309+commonName = supplied
310+
311+[ req_extensions ]
312+basicConstraints = CA:false
313+subjectKeyIdentifier = hash
314+authorityKeyIdentifier = keyid:always, issuer
315+keyUsage = digitalSignature, keyEncipherment, keyAgreement
316+extendedKeyUsage = serverAuth, clientAuth
317+"""
318+
319+
320+def init_ca(ca_dir, common_name, org_name=ORG_NAME, org_unit_name=ORG_UNIT):
321+ print 'Ensuring certificate authority exists at %s.' % ca_dir
322+ if not os.path.exists(ca_dir):
323+ print 'Initializing new certificate authority at %s' % ca_dir
324+ os.mkdir(ca_dir)
325+
326+ for i in ['certs', 'crl', 'newcerts', 'private']:
327+ d = os.path.join(ca_dir, i)
328+ if not os.path.exists(d):
329+ print 'Creating %s.' % d
330+ os.mkdir(d)
331+ os.chmod(os.path.join(ca_dir, 'private'), 0710)
332+
333+ if not os.path.isfile(os.path.join(ca_dir, 'serial')):
334+ with open(os.path.join(ca_dir, 'serial'), 'wb') as out:
335+ out.write('01\n')
336+
337+ if not os.path.isfile(os.path.join(ca_dir, 'index.txt')):
338+ with open(os.path.join(ca_dir, 'index.txt'), 'wb') as out:
339+ out.write('')
340+ if not os.path.isfile(os.path.join(ca_dir, 'ca.cnf')):
341+ print 'Creating new CA config in %s' % ca_dir
342+ with open(os.path.join(ca_dir, 'ca.cnf'), 'wb') as out:
343+ out.write(CA_CONFIG % locals())
344+
345+
346+def root_ca_crt_key(ca_dir):
347+ init = False
348+ crt = os.path.join(ca_dir, 'cacert.pem')
349+ key = os.path.join(ca_dir, 'private', 'cacert.key')
350+ for f in [crt, key]:
351+ if not os.path.isfile(f):
352+ print 'Missing %s, will re-initialize cert+key.' % f
353+ init = True
354+ else:
355+ print 'Found %s.' % f
356+ if init:
357+ cmd = ['openssl', 'req', '-config', os.path.join(ca_dir, 'ca.cnf'), '-x509',
358+ '-nodes', '-newkey', 'rsa', '-days', '21360', '-keyout', key,
359+ '-out', crt, '-outform', 'PEM']
360+ subprocess.check_call(cmd)
361+ return crt, key
362+
363+
364+def intermediate_ca_csr_key(ca_dir):
365+ print 'Creating new intermediate CSR.'
366+ key = os.path.join(ca_dir, 'private', 'cacert.key')
367+ csr = os.path.join(ca_dir, 'cacert.csr')
368+ cmd = ['openssl', 'req', '-config', os.path.join(ca_dir, 'ca.cnf'), '-sha1',
369+ '-newkey', 'rsa', '-nodes', '-keyout', key, '-out', csr, '-outform',
370+ 'PEM']
371+ subprocess.check_call(cmd)
372+ return csr, key
373+
374+
375+def sign_int_csr(ca_dir, csr, common_name):
376+ print 'Signing certificate request %s.' % csr
377+ crt = os.path.join(ca_dir, 'certs',
378+ '%s.crt' % os.path.basename(csr).split('.')[0])
379+ subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name)
380+ cmd = ['openssl', 'ca', '-batch', '-config', os.path.join(ca_dir, 'ca.cnf'),
381+ '-extensions', 'ca_extensions', '-days', CA_EXPIRY, '-notext',
382+ '-in', csr, '-out', crt, '-subj', subj, '-batch']
383+ print ' '.join(cmd)
384+ subprocess.check_call(cmd)
385+ return crt
386+
387+
388+def init_root_ca(ca_dir, common_name):
389+ init_ca(ca_dir, common_name)
390+ return root_ca_crt_key(ca_dir)
391+
392+
393+def init_intermediate_ca(ca_dir, common_name, root_ca_dir,
394+ org_name=ORG_NAME, org_unit_name=ORG_UNIT):
395+ init_ca(ca_dir, common_name)
396+ if not os.path.isfile(os.path.join(ca_dir, 'cacert.pem')):
397+ csr, key = intermediate_ca_csr_key(ca_dir)
398+ crt = sign_int_csr(root_ca_dir, csr, common_name)
399+ shutil.copy(crt, os.path.join(ca_dir, 'cacert.pem'))
400+ else:
401+ print 'Intermediate CA certificate already exists.'
402+
403+ if not os.path.isfile(os.path.join(ca_dir, 'signing.cnf')):
404+ print 'Creating new signing config in %s' % ca_dir
405+ with open(os.path.join(ca_dir, 'signing.cnf'), 'wb') as out:
406+ out.write(SIGNING_CONFIG % locals())
407+
408+
409+def create_certificate(ca_dir, service):
410+ common_name = service
411+ subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name)
412+ csr = os.path.join(ca_dir, 'certs', '%s.csr' % service)
413+ key = os.path.join(ca_dir, 'certs', '%s.key' % service)
414+ cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa', '-nodes', '-keyout',
415+ key, '-out', csr, '-subj', subj]
416+ subprocess.check_call(cmd)
417+ crt = sign_csr(ca_dir, csr, common_name)
418+ print 'Signed new CSR, crt @ %s' % crt
419+ return
420+
421+def update_bundle(bundle_file, new_bundle):
422+ return
423+ if os.path.isfile(bundle_file):
424+ current = open(bundle_file, 'r').read().strip()
425+ if new_bundle == current:
426+ print 'CA Bundle @ %s is up to date.' % bundle_file
427+ return
428+ else:
429+ print 'Updating CA bundle @ %s.' % bundle_file
430+
431+ with open(bundle_file, 'wb') as out:
432+ out.write(new_bundle)
433+ subprocess.check_call(['update-ca-certificates'])
434+
435+def tar_directory(path):
436+ cwd = os.getcwd()
437+ parent=os.path.dirname(path)
438+ directory=os.path.basename(path)
439+ tmp = tempfile.TemporaryFile()
440+ os.chdir(parent)
441+ tarball = tarfile.TarFile(fileobj=tmp, mode='w')
442+ tarball.add(directory)
443+ tarball.close()
444+ tmp.seek(0)
445+ out = tmp.read()
446+ tmp.close()
447+ os.chdir(cwd)
448+ return out
449+
450+class JujuCA(object):
451+ def __init__(self, name, ca_dir, root_ca_dir, user, group):
452+ root_crt, root_key = init_root_ca(root_ca_dir,
453+ '%s Certificate Authority' % name)
454+ init_intermediate_ca(ca_dir,
455+ '%s Intermediate Certificate Authority' % name,
456+ root_ca_dir)
457+ cmd = ['chown', '-R', '%s.%s' % (user, group), ca_dir]
458+ subprocess.check_call(cmd)
459+ cmd = ['chown', '-R', '%s.%s' % (user, group), root_ca_dir]
460+ subprocess.check_call(cmd)
461+ self.ca_dir = ca_dir
462+ self.root_ca_dir = root_ca_dir
463+ self.user = user
464+ self.group = group
465+ update_bundle(CA_BUNDLE, self.get_ca_bundle())
466+
467+ def _sign_csr(self, csr, service, common_name):
468+ subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name)
469+ crt = os.path.join(self.ca_dir, 'certs', '%s.crt' % common_name)
470+ cmd = ['openssl', 'ca', '-config',
471+ os.path.join(self.ca_dir, 'signing.cnf'), '-extensions',
472+ 'req_extensions', '-days', '365', '-notext', '-in', csr,
473+ '-out', crt, '-batch', '-subj', subj]
474+ subprocess.check_call(cmd)
475+ return crt
476+
477+ def _create_certificate(self, service, common_name):
478+ subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name)
479+ csr = os.path.join(self.ca_dir, 'certs', '%s.csr' % service)
480+ key = os.path.join(self.ca_dir, 'certs', '%s.key' % service)
481+ cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa', '-nodes', '-keyout',
482+ key, '-out', csr, '-subj', subj]
483+ subprocess.check_call(cmd)
484+ crt = self._sign_csr(csr, service, common_name)
485+ cmd = ['chown', '-R', '%s.%s' % (self.user, self.group), self.ca_dir]
486+ subprocess.check_call(cmd)
487+ print 'Signed new CSR, crt @ %s' % crt
488+ return crt, key
489+
490+ def get_cert_and_key(self, common_name):
491+ print 'Getting certificate and key for %s.' % common_name
492+ key = os.path.join(self.ca_dir, 'certs', '%s.key' % common_name)
493+ crt = os.path.join(self.ca_dir, 'certs', '%s.crt' % common_name)
494+ if os.path.isfile(crt):
495+ print 'Found existing certificate for %s.' % common_name
496+ crt = open(crt, 'r').read()
497+ try:
498+ key = open(key, 'r').read()
499+ except:
500+ print 'Could not load ssl private key for %s from %s' %\
501+ (common_name, key)
502+ exit(1)
503+ return crt, key
504+ crt, key = self._create_certificate(common_name, common_name)
505+ return open(crt, 'r').read(), open(key, 'r').read()
506+
507+ def get_ca_bundle(self):
508+ int_cert = open(os.path.join(self.ca_dir, 'cacert.pem')).read()
509+ root_cert = open(os.path.join(self.root_ca_dir, 'cacert.pem')).read()
510+ # NOTE: ordering of certs in bundle matters!
511+ return int_cert + root_cert
512
513=== added file 'hooks/lib/unison.py'
514--- hooks/lib/unison.py 1970-01-01 00:00:00 +0000
515+++ hooks/lib/unison.py 2013-03-08 21:36:24 +0000
516@@ -0,0 +1,215 @@
517+#!/usr/bin/python
518+#
519+# Easy file synchronization among peer units using ssh + unison.
520+#
521+# From *both* peer relation -joined and -changed, add a call to
522+# ssh_authorized_peers() describing the peer relation and the desired
523+# user + group. After all peer relations have settled, all hosts should
524+# be able to connect to on another via key auth'd ssh as the specified user.
525+#
526+# Other hooks are then free to synchronize files and directories using
527+# sync_to_peers().
528+#
529+# For a peer relation named 'cluster', for example:
530+#
531+# cluster-relation-joined:
532+# ...
533+# ssh_authorized_peers(peer_interface='cluster',
534+# user='juju_ssh', group='juju_ssh',
535+# ensure_user=True)
536+# ...
537+#
538+# cluster-relation-changed:
539+# ...
540+# ssh_authorized_peers(peer_interface='cluster',
541+# user='juju_ssh', group='juju_ssh',
542+# ensure_user=True)
543+# ...
544+#
545+# Hooks are now free to sync files as easily as:
546+#
547+# files = ['/etc/fstab', '/etc/apt.conf.d/']
548+# sync_to_peers(peer_interface='cluster',
549+# user='juju_ssh, paths=[files])
550+#
551+# It is assumed the charm itself has setup permissions on each unit
552+# such that 'juju_ssh' has read + write permissions. Also assumed
553+# that the calling charm takes care of leader delegation.
554+#
555+# TODO: Currently depends on the utils.py shipped with the keystone charm.
556+# Either copy required functionality to this library or depend on
557+# something more generic.
558+
559+import json
560+import os
561+import sys
562+import utils
563+import subprocess
564+import shutil
565+import grp
566+import pwd
567+
568+
569+def get_homedir(user):
570+ try:
571+ user = pwd.getpwnam(user)
572+ return user.pw_dir
573+ except KeyError:
574+ utils.juju_log('Could not get homedir for user %s: user exists?')
575+ sys.exit(1)
576+
577+
578+def get_keypair(user):
579+ home_dir = get_homedir(user)
580+ ssh_dir = os.path.join(home_dir, '.ssh')
581+ if not os.path.isdir(ssh_dir):
582+ os.mkdir(ssh_dir)
583+
584+ priv_key = os.path.join(ssh_dir, 'id_rsa')
585+ if not os.path.isfile(priv_key):
586+ utils.juju_log('Generating new ssh key for user %s.' % user)
587+ cmd = ['ssh-keygen', '-q', '-N', '', '-t', 'rsa', '-b', '2048',
588+ '-f', priv_key]
589+ subprocess.check_call(cmd)
590+
591+ pub_key = '%s.pub' % priv_key
592+ if not os.path.isfile(pub_key):
593+ utils.juju_log('Generatring missing ssh public key @ %s.' % pub_key)
594+ cmd = ['ssh-keygen', '-y', '-f', priv_key]
595+ p = subprocess.check_output(cmd).strip()
596+ with open(pub_key, 'wb') as out:
597+ out.write(p)
598+ subprocess.check_call(['chown', '-R', user, ssh_dir])
599+ return open(priv_key, 'r').read().strip(), open(pub_key, 'r').read().strip()
600+
601+
602+def write_authorized_keys(user, keys):
603+ home_dir = get_homedir(user)
604+ ssh_dir = os.path.join(home_dir, '.ssh')
605+ auth_keys = os.path.join(ssh_dir, 'authorized_keys')
606+ utils.juju_log('Syncing authorized_keys @ %s.' % auth_keys)
607+ with open(auth_keys, 'wb') as out:
608+ for k in keys:
609+ out.write('%s\n' % k)
610+
611+
612+def write_known_hosts(user, hosts):
613+ home_dir = get_homedir(user)
614+ ssh_dir = os.path.join(home_dir, '.ssh')
615+ known_hosts = os.path.join(ssh_dir, 'known_hosts')
616+ khosts = []
617+ for host in hosts:
618+ cmd = ['ssh-keyscan', '-H', '-t', 'rsa', host]
619+ remote_key = subprocess.check_output(cmd).strip()
620+ khosts.append(remote_key)
621+ utils.juju_log('Syncing known_hosts @ %s.' % known_hosts)
622+ with open(known_hosts, 'wb') as out:
623+ for host in khosts:
624+ out.write('%s\n' % host)
625+
626+
627+def _ensure_user(user, group=None):
628+ # need to ensure a bash shell'd user exists.
629+ try:
630+ pwd.getpwnam(user)
631+ except KeyError:
632+ utils.juju_log('Creating new user %s.%s.' % (user, group))
633+ cmd = ['adduser', '--system', '--shell', '/bin/bash', user]
634+ if group:
635+ try:
636+ grp.getgrnam(group)
637+ except KeyError:
638+ subprocess.check_call(['addgroup', group])
639+ cmd += ['--ingroup', group]
640+ subprocess.check_call(cmd)
641+
642+
643+def ssh_authorized_peers(peer_interface, user, group=None, ensure_user=False):
644+ """
645+ Main setup function, should be called from both peer -changed and -joined
646+ hooks with the same parameters.
647+ """
648+ if ensure_user:
649+ _ensure_user(user, group)
650+ priv_key, pub_key = get_keypair(user)
651+ hook = os.path.basename(sys.argv[0])
652+ if hook == '%s-relation-joined' % peer_interface:
653+ utils.relation_set_2(ssh_pub_key=pub_key)
654+ print 'joined'
655+ elif hook == '%s-relation-changed' % peer_interface:
656+ hosts = []
657+ keys = []
658+ for r_id in utils.relation_ids(peer_interface):
659+ for unit in utils.relation_list(r_id):
660+ settings = utils.relation_get_dict(relation_id=r_id,
661+ remote_unit=unit)
662+ if 'ssh_pub_key' in settings:
663+ keys.append(settings['ssh_pub_key'])
664+ hosts.append(settings['private-address'])
665+ else:
666+ utils.juju_log('ssh_authorized_peers(): ssh_pub_key '\
667+ 'missing for unit %s, skipping.' % unit)
668+ write_authorized_keys(user, keys)
669+ write_known_hosts(user, hosts)
670+ authed_hosts = ':'.join(hosts)
671+ utils.relation_set_2(ssh_authorized_hosts=authed_hosts)
672+
673+
674+def _run_as_user(user):
675+ try:
676+ user = pwd.getpwnam(user)
677+ except KeyError:
678+ utils.juju_log('Invalid user: %s' % user)
679+ sys.exit(1)
680+ uid, gid = user.pw_uid, user.pw_gid
681+ os.environ['HOME'] = user.pw_dir
682+ def _inner():
683+ os.setgid(gid)
684+ os.setuid(uid)
685+ return _inner
686+
687+def run_as_user(user, cmd):
688+ return subprocess.check_output(cmd, preexec_fn=_run_as_user(user))
689+
690+def sync_to_peers(peer_interface, user, paths=[], verbose=False):
691+ base_cmd = ['unison', '-auto', '-batch=true', '-confirmbigdel=false',
692+ '-fastcheck=true', '-group=false', '-owner=false', '-prefer=newer',
693+ '-times=true']
694+ if not verbose:
695+ base_cmd.append('-silent')
696+
697+ hosts = []
698+ for r_id in (utils.relation_ids(peer_interface) or []):
699+ for unit in utils.relation_list(r_id):
700+ settings = utils.relation_get_dict(relation_id=r_id,
701+ remote_unit=unit)
702+ try:
703+ authed_hosts = settings['ssh_authorized_hosts'].split(':')
704+ except KeyError:
705+ print 'unison sync_to_peers: peer has not authorized *any* '\
706+ 'hosts yet.'
707+ return
708+
709+ unit_hostname = utils.unit_get('private-address')
710+ add_host = None
711+ for authed_host in authed_hosts:
712+ if unit_hostname == authed_host:
713+ add_host = settings['private-address']
714+ if add_host:
715+ hosts.append(settings['private-address'])
716+ else:
717+ print 'unison sync_to_peers: peer (%s) has not authorized '\
718+ '*this* host yet, skipping.' %\
719+ settings['private-address']
720+
721+ for path in paths:
722+ # removing trailing slash from directory paths, unison
723+ # doesn't like these.
724+ if path.endswith('/'):
725+ path = path[:(len(path)-1)]
726+ for host in hosts:
727+ cmd = base_cmd + [path, 'ssh://%s@%s/%s' % (user, host, path)]
728+ utils.juju_log('Syncing local path %s to %s@%s:%s' %\
729+ (path, user, host, path))
730+ print ' '.join(cmd)
731+ run_as_user(user, cmd)
732
733=== modified file 'hooks/utils.py'
734--- hooks/utils.py 2013-02-22 19:20:54 +0000
735+++ hooks/utils.py 2013-03-08 21:36:24 +0000
736@@ -4,15 +4,25 @@
737 import sys
738 import json
739 import os
740+import tarfile
741+import tempfile
742 import time
743
744 from lib.openstack_common import *
745
746+import keystone_ssl as ssl
747+import lib.unison as unison
748+
749 keystone_conf = "/etc/keystone/keystone.conf"
750 stored_passwd = "/var/lib/keystone/keystone.passwd"
751 stored_token = "/var/lib/keystone/keystone.token"
752 SERVICE_PASSWD_PATH = '/var/lib/keystone/services.passwd'
753
754+SSL_DIR = '/var/lib/keystone/juju_ssl/'
755+SSL_CA_NAME = 'Ubuntu Cloud'
756+
757+SSH_USER='juju_keystone'
758+
759 def execute(cmd, die=False, echo=False):
760 """ Executes a command
761
762@@ -95,6 +105,19 @@
763 cmd += args
764 subprocess.check_call(cmd)
765
766+
767+def unit_get(attribute):
768+ cmd = [
769+ 'unit-get',
770+ attribute
771+ ]
772+ value = subprocess.check_output(cmd).strip() # IGNORE:E1103
773+ if value == "":
774+ return None
775+ else:
776+ return value
777+
778+
779 def relation_get(relation_data):
780 """ Obtain all current relation data
781 relation_data is a list of options to query from the relation
782@@ -356,8 +379,7 @@
783 create_role("KeystoneAdmin", config["admin-user"], 'admin')
784 create_role("KeystoneServiceAdmin", config["admin-user"], 'admin')
785 create_service_entry("keystone", "identity", "Keystone Identity Service")
786- # following documentation here, perhaps we should be using juju
787- # public/private addresses for public/internal urls.
788+
789 if is_clustered():
790 juju_log("Creating endpoint for clustered configuration")
791 for region in config['region'].split():
792@@ -543,17 +565,32 @@
793 Broadcast service credentials to peers or consume those that have been
794 broadcasted by peer, depending on hook context.
795 '''
796- if os.path.basename(sys.argv[0]) == 'cluster-relation-changed':
797- r_data = relation_get_dict()
798- if 'service_credentials' in r_data:
799- juju_log('Saving service passwords from peer.')
800- save_stored_passwords(**json.loads(r_data['service_credentials']))
801- return
802-
803- creds = load_stored_passwords()
804- if not creds:
805+ if (not eligible_leader() or
806+ not os.path.isfile(SERVICE_PASSWD_PATH)):
807 return
808 juju_log('Synchronizing service passwords to all peers.')
809- creds = json.dumps(creds)
810- for r_id in (relation_ids('cluster') or []):
811- relation_set_2(rid=r_id, service_credentials=creds)
812+ unison.sync_to_peers(peer_interface='cluster',
813+ paths=[SERVICE_PASSWD_PATH], user=SSH_USER,
814+ verbose=True)
815+
816+CA = []
817+def get_ca(user='keystone', group='keystone'):
818+ """
819+ Initialize a new CA object if one hasn't already been loaded.
820+ This will create a new CA or load an existing one.
821+ """
822+ if not CA:
823+ if not os.path.isdir(SSL_DIR):
824+ os.mkdir(SSL_DIR)
825+ d_name = '_'.join(SSL_CA_NAME.lower().split(' '))
826+ ca = ssl.JujuCA(name=SSL_CA_NAME, user=user, group=group,
827+ ca_dir=os.path.join(SSL_DIR,
828+ '%s_intermediate_ca' % d_name),
829+ root_ca_dir=os.path.join(SSL_DIR,
830+ '%s_root_ca' % d_name))
831+ # SSL_DIR is synchronized via all peers over unison+ssh, need
832+ # to ensure permissions.
833+ execute('chown -R %s.%s %s' % (user, group, SSL_DIR))
834+ execute('chmod -R g+rwx %s' % SSL_DIR)
835+ CA.append(ca)
836+ return CA[0]
837
838=== modified file 'revision'
839--- revision 2013-03-06 21:50:30 +0000
840+++ revision 2013-03-08 21:36:24 +0000
841@@ -1,1 +1,1 @@
842-198
843+211

Subscribers

People subscribed via source and target branches