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
=== modified file 'config.yaml'
--- config.yaml 2013-01-22 17:49:36 +0000
+++ config.yaml 2013-03-08 21:36:24 +0000
@@ -112,3 +112,7 @@
112 default: "false"112 default: "false"
113 type: string113 type: string
114 description: "Enable PKI token signing (Grizzly and beyond)"114 description: "Enable PKI token signing (Grizzly and beyond)"
115 https-service-endpoints:
116 default: "False"
117 type: string
118 description: "Manage SSL certificates for all service endpoints."
115119
=== added symlink 'hooks/cluster-relation-joined'
=== target is u'keystone-hooks'
=== modified file 'hooks/keystone-hooks'
--- hooks/keystone-hooks 2013-02-22 19:20:54 +0000
+++ hooks/keystone-hooks 2013-03-08 21:36:24 +0000
@@ -2,13 +2,18 @@
22
3import sys3import sys
4import time4import time
5import urlparse
6
7from base64 import b64encode
58
6from utils import *9from utils import *
10
7from lib.openstack_common import *11from lib.openstack_common import *
12import lib.unison as unison
813
9config = config_get()14config = config_get()
1015
11packages = "keystone python-mysqldb pwgen haproxy python-jinja2"16packages = "keystone python-mysqldb pwgen haproxy python-jinja2 openssl unison"
12service = "keystone"17service = "keystone"
1318
14# used to verify joined services are valid openstack components.19# used to verify joined services are valid openstack components.
@@ -81,6 +86,11 @@
81 execute("service keystone stop", echo=True)86 execute("service keystone stop", echo=True)
82 execute("keystone-manage db_sync")87 execute("keystone-manage db_sync")
83 execute("service keystone start", echo=True)88 execute("service keystone start", echo=True)
89
90 # ensure /var/lib/keystone is g+wrx for peer relations that
91 # may be syncing data there via SSH_USER.
92 execute("chmod -R g+wrx /var/lib/keystone/")
93
84 time.sleep(5)94 time.sleep(5)
85 ensure_initial_admin(config)95 ensure_initial_admin(config)
8696
@@ -97,6 +107,7 @@
97 'db_host' not in relation_data):107 'db_host' not in relation_data):
98 juju_log("db_host or password not set. Peer not ready, exit 0")108 juju_log("db_host or password not set. Peer not ready, exit 0")
99 exit(0)109 exit(0)
110
100 update_config_block('sql', connection="mysql://%s:%s@%s/%s" %111 update_config_block('sql', connection="mysql://%s:%s@%s/%s" %
101 (config["database-user"],112 (config["database-user"],
102 relation_data["password"],113 relation_data["password"],
@@ -125,6 +136,21 @@
125 (id, unit))136 (id, unit))
126 identity_changed(relation_id=id, remote_unit=unit)137 identity_changed(relation_id=id, remote_unit=unit)
127138
139def ensure_valid_service(service):
140 if service not in valid_services.keys():
141 juju_log("WARN: Invalid service requested: '%s'" % service)
142 realtion_set({ "admin_token": -1 })
143 return
144
145def add_endpoint(region, service, public_url, admin_url, internal_url):
146 desc = valid_services[service]["desc"]
147 service_type = valid_services[service]["type"]
148 create_service_entry(service, service_type, desc)
149 create_endpoint_template(region=region, service=service,
150 public_url=public_url,
151 admin_url=admin_url,
152 internal_url=internal_url)
153
128def identity_joined():154def identity_joined():
129 """ Do nothing until we get information about requested service """155 """ Do nothing until we get information about requested service """
130 pass156 pass
@@ -170,7 +196,6 @@
170 'internal_url'])196 'internal_url'])
171 if single.issubset(settings):197 if single.issubset(settings):
172 # other end of relation advertised only one endpoint198 # other end of relation advertised only one endpoint
173
174 if 'None' in [v for k,v in settings.iteritems()]:199 if 'None' in [v for k,v in settings.iteritems()]:
175 # Some backend services advertise no endpoint but require a200 # Some backend services advertise no endpoint but require a
176 # hook execution to update auth strategy.201 # hook execution to update auth strategy.
@@ -191,11 +216,14 @@
191216
192217
193 ensure_valid_service(settings['service'])218 ensure_valid_service(settings['service'])
219
194 add_endpoint(region=settings['region'], service=settings['service'],220 add_endpoint(region=settings['region'], service=settings['service'],
195 publicurl=settings['public_url'],221 publicurl=settings['public_url'],
196 adminurl=settings['admin_url'],222 adminurl=settings['admin_url'],
197 internalurl=settings['internal_url'])223 internalurl=settings['internal_url'])
198 service_username = settings['service']224 service_username = settings['service']
225 https_cn = urlparse.urlparse(settings['internal_url'])
226 https_cn = https_cn.hostname
199 else:227 else:
200 # assemble multiple endpoints from relation data. service name228 # assemble multiple endpoints from relation data. service name
201 # should be prepended to setting name, ie:229 # should be prepended to setting name, ie:
@@ -217,10 +245,11 @@
217 for k,v in settings.iteritems():245 for k,v in settings.iteritems():
218 ep = k.split('_')[0]246 ep = k.split('_')[0]
219 x = '_'.join(k.split('_')[1:])247 x = '_'.join(k.split('_')[1:])
220 if ep not in endpoints:248 if ep not in endpoints:
221 endpoints[ep] = {}249 endpoints[ep] = {}
222 endpoints[ep][x] = v250 endpoints[ep][x] = v
223 services = []251 services = []
252 https_cn = None
224 for ep in endpoints:253 for ep in endpoints:
225 # weed out any unrelated relation stuff Juju might have added254 # weed out any unrelated relation stuff Juju might have added
226 # by ensuring each possible endpiont has appropriate fields255 # by ensuring each possible endpiont has appropriate fields
@@ -233,6 +262,9 @@
233 adminurl=ep['admin_url'],262 adminurl=ep['admin_url'],
234 internalurl=ep['internal_url'])263 internalurl=ep['internal_url'])
235 services.append(ep['service'])264 services.append(ep['service'])
265 if not https_cn:
266 https_cn = urlparse.urlparse(ep['internal_url'])
267 https_cn = https_cn.hostname
236 service_username = '_'.join(services)268 service_username = '_'.join(services)
237269
238 if 'None' in [v for k,v in settings.iteritems()]:270 if 'None' in [v for k,v in settings.iteritems()]:
@@ -262,8 +294,16 @@
262 "auth_port": config["admin-port"],294 "auth_port": config["admin-port"],
263 "service_username": service_username,295 "service_username": service_username,
264 "service_password": service_password,296 "service_password": service_password,
265 "service_tenant": config['service-tenant']297 "service_tenant": config['service-tenant'],
298 "https_keystone": "False",
299 "ssl_cert": "",
300 "ssl_key": "",
301 "ca_cert": ""
266 }302 }
303
304 if relation_id:
305 relation_data['rid'] = relation_id
306
267 # Check if clustered and use vip + haproxy ports if so307 # Check if clustered and use vip + haproxy ports if so
268 if is_clustered():308 if is_clustered():
269 relation_data["auth_host"] = config['vip']309 relation_data["auth_host"] = config['vip']
@@ -271,7 +311,19 @@
271 relation_data["service_host"] = config['vip']311 relation_data["service_host"] = config['vip']
272 relation_data["service_port"] = SERVICE_PORTS['keystone_service']312 relation_data["service_port"] = SERVICE_PORTS['keystone_service']
273313
274 relation_set(relation_data)314 # generate or get a new cert/key for service if set to manage certs.
315 if config['https-service-endpoints'] in ['True', 'true']:
316 ca = get_ca(user=SSH_USER)
317 service = os.getenv('JUJU_REMOTE_UNIT').split('/')[0]
318 cert, key = ca.get_cert_and_key(common_name=https_cn)
319 ca_bundle= ca.get_ca_bundle()
320 relation_data['ssl_cert'] = b64encode(cert)
321 relation_data['ssl_key'] = b64encode(key)
322 relation_data['ca_cert'] = b64encode(ca_bundle)
323 relation_data['https_keystone'] = 'True'
324 unison.sync_to_peers(peer_interface='cluster',
325 paths=[SSL_DIR], user=SSH_USER, verbose=True)
326 relation_set_2(**relation_data)
275 synchronize_service_credentials()327 synchronize_service_credentials()
276328
277def config_changed():329def config_changed():
@@ -318,12 +370,24 @@
318 "keystone_service": int(config['service-port']) + 1370 "keystone_service": int(config['service-port']) + 1
319 }371 }
320372
373def cluster_joined():
374 unison.ssh_authorized_peers(user=SSH_USER,
375 group='keystone',
376 peer_interface='cluster',
377 ensure_user=True)
321378
322def cluster_changed():379def cluster_changed():
380 unison.ssh_authorized_peers(user=SSH_USER,
381 group='keystone',
382 peer_interface='cluster',
383 ensure_user=True)
323 cluster_hosts = {}384 cluster_hosts = {}
324 cluster_hosts['self'] = config['hostname']385 cluster_hosts['self'] = config['hostname']
325 for r_id in relation_ids('cluster'):386 for r_id in relation_ids('cluster'):
326 for unit in relation_list(r_id):387 for unit in relation_list(r_id):
388 # trigger identity-changed to reconfigure HTTPS
389 # as necessary.
390 identity_changed(relation_id=r_id, remote_unit=unit)
327 cluster_hosts[unit.replace('/','-')] = \391 cluster_hosts[unit.replace('/','-')] = \
328 relation_get_dict(relation_id=r_id,392 relation_get_dict(relation_id=r_id,
329 remote_unit=unit)['private-address']393 remote_unit=unit)['private-address']
@@ -332,6 +396,10 @@
332396
333 synchronize_service_credentials()397 synchronize_service_credentials()
334398
399 for r_id in relation_ids('identity-service'):
400 for unit in relation_list(r_id):
401 # trigger identity-changed to reconfigure HTTPS as necessary
402 identity_changed(relation_id=r_id, remote_unit=unit)
335403
336def ha_relation_changed():404def ha_relation_changed():
337 relation_data = relation_get_dict()405 relation_data = relation_get_dict()
@@ -400,6 +468,7 @@
400 "identity-service-relation-joined": identity_joined,468 "identity-service-relation-joined": identity_joined,
401 "identity-service-relation-changed": identity_changed,469 "identity-service-relation-changed": identity_changed,
402 "config-changed": config_changed,470 "config-changed": config_changed,
471 "cluster-relation-joined": cluster_joined,
403 "cluster-relation-changed": cluster_changed,472 "cluster-relation-changed": cluster_changed,
404 "cluster-relation-departed": cluster_changed,473 "cluster-relation-departed": cluster_changed,
405 "ha-relation-joined": ha_relation_joined,474 "ha-relation-joined": ha_relation_joined,
406475
=== added file 'hooks/keystone_ssl.py'
--- hooks/keystone_ssl.py 1970-01-01 00:00:00 +0000
+++ hooks/keystone_ssl.py 2013-03-08 21:36:24 +0000
@@ -0,0 +1,298 @@
1#!/usr/bin/python
2
3import base64
4import os
5import shutil
6import subprocess
7import tarfile
8import tempfile
9from utils import *
10
11CA_EXPIRY = '365'
12ORG_NAME = 'Ubuntu'
13ORG_UNIT = 'Ubuntu Cloud'
14CA_BUNDLE='/usr/local/share/ca-certificates/juju_ca_cert.crt'
15
16CA_CONFIG = """
17[ ca ]
18default_ca = CA_default
19
20[ CA_default ]
21dir = %(ca_dir)s
22policy = policy_match
23database = $dir/index.txt
24serial = $dir/serial
25certs = $dir/certs
26crl_dir = $dir/crl
27new_certs_dir = $dir/newcerts
28certificate = $dir/cacert.pem
29private_key = $dir/private/cacert.key
30RANDFILE = $dir/private/.rand
31default_md = default
32
33[ req ]
34default_bits = 1024
35default_md = sha1
36
37prompt = no
38distinguished_name = ca_distinguished_name
39
40x509_extensions = ca_extensions
41
42[ ca_distinguished_name ]
43organizationName = %(org_name)s
44organizationalUnitName = %(org_unit_name)s Certificate Authority
45commonName = %(common_name)s
46
47[ policy_match ]
48countryName = optional
49stateOrProvinceName = optional
50organizationName = match
51organizationalUnitName = optional
52commonName = supplied
53
54[ ca_extensions ]
55basicConstraints = critical,CA:true
56subjectKeyIdentifier = hash
57authorityKeyIdentifier = keyid:always, issuer
58keyUsage = cRLSign, keyCertSign
59"""
60
61SIGNING_CONFIG="""
62[ ca ]
63default_ca = CA_default
64
65[ CA_default ]
66dir = %(ca_dir)s
67policy = policy_match
68database = $dir/index.txt
69serial = $dir/serial
70certs = $dir/certs
71crl_dir = $dir/crl
72new_certs_dir = $dir/newcerts
73certificate = $dir/cacert.pem
74private_key = $dir/private/cacert.key
75RANDFILE = $dir/private/.rand
76default_md = default
77
78[ req ]
79default_bits = 1024
80default_md = sha1
81
82prompt = no
83distinguished_name = req_distinguished_name
84
85x509_extensions = req_extensions
86
87[ req_distinguished_name ]
88organizationName = %(org_name)s
89organizationalUnitName = %(org_unit_name)s Server Farm
90
91[ policy_match ]
92countryName = optional
93stateOrProvinceName = optional
94organizationName = match
95organizationalUnitName = optional
96commonName = supplied
97
98[ req_extensions ]
99basicConstraints = CA:false
100subjectKeyIdentifier = hash
101authorityKeyIdentifier = keyid:always, issuer
102keyUsage = digitalSignature, keyEncipherment, keyAgreement
103extendedKeyUsage = serverAuth, clientAuth
104"""
105
106
107def init_ca(ca_dir, common_name, org_name=ORG_NAME, org_unit_name=ORG_UNIT):
108 print 'Ensuring certificate authority exists at %s.' % ca_dir
109 if not os.path.exists(ca_dir):
110 print 'Initializing new certificate authority at %s' % ca_dir
111 os.mkdir(ca_dir)
112
113 for i in ['certs', 'crl', 'newcerts', 'private']:
114 d = os.path.join(ca_dir, i)
115 if not os.path.exists(d):
116 print 'Creating %s.' % d
117 os.mkdir(d)
118 os.chmod(os.path.join(ca_dir, 'private'), 0710)
119
120 if not os.path.isfile(os.path.join(ca_dir, 'serial')):
121 with open(os.path.join(ca_dir, 'serial'), 'wb') as out:
122 out.write('01\n')
123
124 if not os.path.isfile(os.path.join(ca_dir, 'index.txt')):
125 with open(os.path.join(ca_dir, 'index.txt'), 'wb') as out:
126 out.write('')
127 if not os.path.isfile(os.path.join(ca_dir, 'ca.cnf')):
128 print 'Creating new CA config in %s' % ca_dir
129 with open(os.path.join(ca_dir, 'ca.cnf'), 'wb') as out:
130 out.write(CA_CONFIG % locals())
131
132
133def root_ca_crt_key(ca_dir):
134 init = False
135 crt = os.path.join(ca_dir, 'cacert.pem')
136 key = os.path.join(ca_dir, 'private', 'cacert.key')
137 for f in [crt, key]:
138 if not os.path.isfile(f):
139 print 'Missing %s, will re-initialize cert+key.' % f
140 init = True
141 else:
142 print 'Found %s.' % f
143 if init:
144 cmd = ['openssl', 'req', '-config', os.path.join(ca_dir, 'ca.cnf'), '-x509',
145 '-nodes', '-newkey', 'rsa', '-days', '21360', '-keyout', key,
146 '-out', crt, '-outform', 'PEM']
147 subprocess.check_call(cmd)
148 return crt, key
149
150
151def intermediate_ca_csr_key(ca_dir):
152 print 'Creating new intermediate CSR.'
153 key = os.path.join(ca_dir, 'private', 'cacert.key')
154 csr = os.path.join(ca_dir, 'cacert.csr')
155 cmd = ['openssl', 'req', '-config', os.path.join(ca_dir, 'ca.cnf'), '-sha1',
156 '-newkey', 'rsa', '-nodes', '-keyout', key, '-out', csr, '-outform',
157 'PEM']
158 subprocess.check_call(cmd)
159 return csr, key
160
161
162def sign_int_csr(ca_dir, csr, common_name):
163 print 'Signing certificate request %s.' % csr
164 crt = os.path.join(ca_dir, 'certs',
165 '%s.crt' % os.path.basename(csr).split('.')[0])
166 subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name)
167 cmd = ['openssl', 'ca', '-batch', '-config', os.path.join(ca_dir, 'ca.cnf'),
168 '-extensions', 'ca_extensions', '-days', CA_EXPIRY, '-notext',
169 '-in', csr, '-out', crt, '-subj', subj, '-batch']
170 print ' '.join(cmd)
171 subprocess.check_call(cmd)
172 return crt
173
174
175def init_root_ca(ca_dir, common_name):
176 init_ca(ca_dir, common_name)
177 return root_ca_crt_key(ca_dir)
178
179
180def init_intermediate_ca(ca_dir, common_name, root_ca_dir,
181 org_name=ORG_NAME, org_unit_name=ORG_UNIT):
182 init_ca(ca_dir, common_name)
183 if not os.path.isfile(os.path.join(ca_dir, 'cacert.pem')):
184 csr, key = intermediate_ca_csr_key(ca_dir)
185 crt = sign_int_csr(root_ca_dir, csr, common_name)
186 shutil.copy(crt, os.path.join(ca_dir, 'cacert.pem'))
187 else:
188 print 'Intermediate CA certificate already exists.'
189
190 if not os.path.isfile(os.path.join(ca_dir, 'signing.cnf')):
191 print 'Creating new signing config in %s' % ca_dir
192 with open(os.path.join(ca_dir, 'signing.cnf'), 'wb') as out:
193 out.write(SIGNING_CONFIG % locals())
194
195
196def create_certificate(ca_dir, service):
197 common_name = service
198 subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name)
199 csr = os.path.join(ca_dir, 'certs', '%s.csr' % service)
200 key = os.path.join(ca_dir, 'certs', '%s.key' % service)
201 cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa', '-nodes', '-keyout',
202 key, '-out', csr, '-subj', subj]
203 subprocess.check_call(cmd)
204 crt = sign_csr(ca_dir, csr, common_name)
205 print 'Signed new CSR, crt @ %s' % crt
206 return
207
208def update_bundle(bundle_file, new_bundle):
209 return
210 if os.path.isfile(bundle_file):
211 current = open(bundle_file, 'r').read().strip()
212 if new_bundle == current:
213 print 'CA Bundle @ %s is up to date.' % bundle_file
214 return
215 else:
216 print 'Updating CA bundle @ %s.' % bundle_file
217
218 with open(bundle_file, 'wb') as out:
219 out.write(new_bundle)
220 subprocess.check_call(['update-ca-certificates'])
221
222def tar_directory(path):
223 cwd = os.getcwd()
224 parent=os.path.dirname(path)
225 directory=os.path.basename(path)
226 tmp = tempfile.TemporaryFile()
227 os.chdir(parent)
228 tarball = tarfile.TarFile(fileobj=tmp, mode='w')
229 tarball.add(directory)
230 tarball.close()
231 tmp.seek(0)
232 out = tmp.read()
233 tmp.close()
234 os.chdir(cwd)
235 return out
236
237class JujuCA(object):
238 def __init__(self, name, ca_dir, root_ca_dir, user, group):
239 root_crt, root_key = init_root_ca(root_ca_dir,
240 '%s Certificate Authority' % name)
241 init_intermediate_ca(ca_dir,
242 '%s Intermediate Certificate Authority' % name,
243 root_ca_dir)
244 cmd = ['chown', '-R', '%s.%s' % (user, group), ca_dir]
245 subprocess.check_call(cmd)
246 cmd = ['chown', '-R', '%s.%s' % (user, group), root_ca_dir]
247 subprocess.check_call(cmd)
248 self.ca_dir = ca_dir
249 self.root_ca_dir = root_ca_dir
250 self.user = user
251 self.group = group
252 update_bundle(CA_BUNDLE, self.get_ca_bundle())
253
254 def _sign_csr(self, csr, service, common_name):
255 subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name)
256 crt = os.path.join(self.ca_dir, 'certs', '%s.crt' % common_name)
257 cmd = ['openssl', 'ca', '-config',
258 os.path.join(self.ca_dir, 'signing.cnf'), '-extensions',
259 'req_extensions', '-days', '365', '-notext', '-in', csr,
260 '-out', crt, '-batch', '-subj', subj]
261 subprocess.check_call(cmd)
262 return crt
263
264 def _create_certificate(self, service, common_name):
265 subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name)
266 csr = os.path.join(self.ca_dir, 'certs', '%s.csr' % service)
267 key = os.path.join(self.ca_dir, 'certs', '%s.key' % service)
268 cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa', '-nodes', '-keyout',
269 key, '-out', csr, '-subj', subj]
270 subprocess.check_call(cmd)
271 crt = self._sign_csr(csr, service, common_name)
272 cmd = ['chown', '-R', '%s.%s' % (self.user, self.group), self.ca_dir]
273 subprocess.check_call(cmd)
274 print 'Signed new CSR, crt @ %s' % crt
275 return crt, key
276
277 def get_cert_and_key(self, common_name):
278 print 'Getting certificate and key for %s.' % common_name
279 key = os.path.join(self.ca_dir, 'certs', '%s.key' % common_name)
280 crt = os.path.join(self.ca_dir, 'certs', '%s.crt' % common_name)
281 if os.path.isfile(crt):
282 print 'Found existing certificate for %s.' % common_name
283 crt = open(crt, 'r').read()
284 try:
285 key = open(key, 'r').read()
286 except:
287 print 'Could not load ssl private key for %s from %s' %\
288 (common_name, key)
289 exit(1)
290 return crt, key
291 crt, key = self._create_certificate(common_name, common_name)
292 return open(crt, 'r').read(), open(key, 'r').read()
293
294 def get_ca_bundle(self):
295 int_cert = open(os.path.join(self.ca_dir, 'cacert.pem')).read()
296 root_cert = open(os.path.join(self.root_ca_dir, 'cacert.pem')).read()
297 # NOTE: ordering of certs in bundle matters!
298 return int_cert + root_cert
0299
=== added file 'hooks/lib/unison.py'
--- hooks/lib/unison.py 1970-01-01 00:00:00 +0000
+++ hooks/lib/unison.py 2013-03-08 21:36:24 +0000
@@ -0,0 +1,215 @@
1#!/usr/bin/python
2#
3# Easy file synchronization among peer units using ssh + unison.
4#
5# From *both* peer relation -joined and -changed, add a call to
6# ssh_authorized_peers() describing the peer relation and the desired
7# user + group. After all peer relations have settled, all hosts should
8# be able to connect to on another via key auth'd ssh as the specified user.
9#
10# Other hooks are then free to synchronize files and directories using
11# sync_to_peers().
12#
13# For a peer relation named 'cluster', for example:
14#
15# cluster-relation-joined:
16# ...
17# ssh_authorized_peers(peer_interface='cluster',
18# user='juju_ssh', group='juju_ssh',
19# ensure_user=True)
20# ...
21#
22# cluster-relation-changed:
23# ...
24# ssh_authorized_peers(peer_interface='cluster',
25# user='juju_ssh', group='juju_ssh',
26# ensure_user=True)
27# ...
28#
29# Hooks are now free to sync files as easily as:
30#
31# files = ['/etc/fstab', '/etc/apt.conf.d/']
32# sync_to_peers(peer_interface='cluster',
33# user='juju_ssh, paths=[files])
34#
35# It is assumed the charm itself has setup permissions on each unit
36# such that 'juju_ssh' has read + write permissions. Also assumed
37# that the calling charm takes care of leader delegation.
38#
39# TODO: Currently depends on the utils.py shipped with the keystone charm.
40# Either copy required functionality to this library or depend on
41# something more generic.
42
43import json
44import os
45import sys
46import utils
47import subprocess
48import shutil
49import grp
50import pwd
51
52
53def get_homedir(user):
54 try:
55 user = pwd.getpwnam(user)
56 return user.pw_dir
57 except KeyError:
58 utils.juju_log('Could not get homedir for user %s: user exists?')
59 sys.exit(1)
60
61
62def get_keypair(user):
63 home_dir = get_homedir(user)
64 ssh_dir = os.path.join(home_dir, '.ssh')
65 if not os.path.isdir(ssh_dir):
66 os.mkdir(ssh_dir)
67
68 priv_key = os.path.join(ssh_dir, 'id_rsa')
69 if not os.path.isfile(priv_key):
70 utils.juju_log('Generating new ssh key for user %s.' % user)
71 cmd = ['ssh-keygen', '-q', '-N', '', '-t', 'rsa', '-b', '2048',
72 '-f', priv_key]
73 subprocess.check_call(cmd)
74
75 pub_key = '%s.pub' % priv_key
76 if not os.path.isfile(pub_key):
77 utils.juju_log('Generatring missing ssh public key @ %s.' % pub_key)
78 cmd = ['ssh-keygen', '-y', '-f', priv_key]
79 p = subprocess.check_output(cmd).strip()
80 with open(pub_key, 'wb') as out:
81 out.write(p)
82 subprocess.check_call(['chown', '-R', user, ssh_dir])
83 return open(priv_key, 'r').read().strip(), open(pub_key, 'r').read().strip()
84
85
86def write_authorized_keys(user, keys):
87 home_dir = get_homedir(user)
88 ssh_dir = os.path.join(home_dir, '.ssh')
89 auth_keys = os.path.join(ssh_dir, 'authorized_keys')
90 utils.juju_log('Syncing authorized_keys @ %s.' % auth_keys)
91 with open(auth_keys, 'wb') as out:
92 for k in keys:
93 out.write('%s\n' % k)
94
95
96def write_known_hosts(user, hosts):
97 home_dir = get_homedir(user)
98 ssh_dir = os.path.join(home_dir, '.ssh')
99 known_hosts = os.path.join(ssh_dir, 'known_hosts')
100 khosts = []
101 for host in hosts:
102 cmd = ['ssh-keyscan', '-H', '-t', 'rsa', host]
103 remote_key = subprocess.check_output(cmd).strip()
104 khosts.append(remote_key)
105 utils.juju_log('Syncing known_hosts @ %s.' % known_hosts)
106 with open(known_hosts, 'wb') as out:
107 for host in khosts:
108 out.write('%s\n' % host)
109
110
111def _ensure_user(user, group=None):
112 # need to ensure a bash shell'd user exists.
113 try:
114 pwd.getpwnam(user)
115 except KeyError:
116 utils.juju_log('Creating new user %s.%s.' % (user, group))
117 cmd = ['adduser', '--system', '--shell', '/bin/bash', user]
118 if group:
119 try:
120 grp.getgrnam(group)
121 except KeyError:
122 subprocess.check_call(['addgroup', group])
123 cmd += ['--ingroup', group]
124 subprocess.check_call(cmd)
125
126
127def ssh_authorized_peers(peer_interface, user, group=None, ensure_user=False):
128 """
129 Main setup function, should be called from both peer -changed and -joined
130 hooks with the same parameters.
131 """
132 if ensure_user:
133 _ensure_user(user, group)
134 priv_key, pub_key = get_keypair(user)
135 hook = os.path.basename(sys.argv[0])
136 if hook == '%s-relation-joined' % peer_interface:
137 utils.relation_set_2(ssh_pub_key=pub_key)
138 print 'joined'
139 elif hook == '%s-relation-changed' % peer_interface:
140 hosts = []
141 keys = []
142 for r_id in utils.relation_ids(peer_interface):
143 for unit in utils.relation_list(r_id):
144 settings = utils.relation_get_dict(relation_id=r_id,
145 remote_unit=unit)
146 if 'ssh_pub_key' in settings:
147 keys.append(settings['ssh_pub_key'])
148 hosts.append(settings['private-address'])
149 else:
150 utils.juju_log('ssh_authorized_peers(): ssh_pub_key '\
151 'missing for unit %s, skipping.' % unit)
152 write_authorized_keys(user, keys)
153 write_known_hosts(user, hosts)
154 authed_hosts = ':'.join(hosts)
155 utils.relation_set_2(ssh_authorized_hosts=authed_hosts)
156
157
158def _run_as_user(user):
159 try:
160 user = pwd.getpwnam(user)
161 except KeyError:
162 utils.juju_log('Invalid user: %s' % user)
163 sys.exit(1)
164 uid, gid = user.pw_uid, user.pw_gid
165 os.environ['HOME'] = user.pw_dir
166 def _inner():
167 os.setgid(gid)
168 os.setuid(uid)
169 return _inner
170
171def run_as_user(user, cmd):
172 return subprocess.check_output(cmd, preexec_fn=_run_as_user(user))
173
174def sync_to_peers(peer_interface, user, paths=[], verbose=False):
175 base_cmd = ['unison', '-auto', '-batch=true', '-confirmbigdel=false',
176 '-fastcheck=true', '-group=false', '-owner=false', '-prefer=newer',
177 '-times=true']
178 if not verbose:
179 base_cmd.append('-silent')
180
181 hosts = []
182 for r_id in (utils.relation_ids(peer_interface) or []):
183 for unit in utils.relation_list(r_id):
184 settings = utils.relation_get_dict(relation_id=r_id,
185 remote_unit=unit)
186 try:
187 authed_hosts = settings['ssh_authorized_hosts'].split(':')
188 except KeyError:
189 print 'unison sync_to_peers: peer has not authorized *any* '\
190 'hosts yet.'
191 return
192
193 unit_hostname = utils.unit_get('private-address')
194 add_host = None
195 for authed_host in authed_hosts:
196 if unit_hostname == authed_host:
197 add_host = settings['private-address']
198 if add_host:
199 hosts.append(settings['private-address'])
200 else:
201 print 'unison sync_to_peers: peer (%s) has not authorized '\
202 '*this* host yet, skipping.' %\
203 settings['private-address']
204
205 for path in paths:
206 # removing trailing slash from directory paths, unison
207 # doesn't like these.
208 if path.endswith('/'):
209 path = path[:(len(path)-1)]
210 for host in hosts:
211 cmd = base_cmd + [path, 'ssh://%s@%s/%s' % (user, host, path)]
212 utils.juju_log('Syncing local path %s to %s@%s:%s' %\
213 (path, user, host, path))
214 print ' '.join(cmd)
215 run_as_user(user, cmd)
0216
=== modified file 'hooks/utils.py'
--- hooks/utils.py 2013-02-22 19:20:54 +0000
+++ hooks/utils.py 2013-03-08 21:36:24 +0000
@@ -4,15 +4,25 @@
4import sys4import sys
5import json5import json
6import os6import os
7import tarfile
8import tempfile
7import time9import time
810
9from lib.openstack_common import *11from lib.openstack_common import *
1012
13import keystone_ssl as ssl
14import lib.unison as unison
15
11keystone_conf = "/etc/keystone/keystone.conf"16keystone_conf = "/etc/keystone/keystone.conf"
12stored_passwd = "/var/lib/keystone/keystone.passwd"17stored_passwd = "/var/lib/keystone/keystone.passwd"
13stored_token = "/var/lib/keystone/keystone.token"18stored_token = "/var/lib/keystone/keystone.token"
14SERVICE_PASSWD_PATH = '/var/lib/keystone/services.passwd'19SERVICE_PASSWD_PATH = '/var/lib/keystone/services.passwd'
1520
21SSL_DIR = '/var/lib/keystone/juju_ssl/'
22SSL_CA_NAME = 'Ubuntu Cloud'
23
24SSH_USER='juju_keystone'
25
16def execute(cmd, die=False, echo=False):26def execute(cmd, die=False, echo=False):
17 """ Executes a command27 """ Executes a command
1828
@@ -95,6 +105,19 @@
95 cmd += args105 cmd += args
96 subprocess.check_call(cmd)106 subprocess.check_call(cmd)
97107
108
109def unit_get(attribute):
110 cmd = [
111 'unit-get',
112 attribute
113 ]
114 value = subprocess.check_output(cmd).strip() # IGNORE:E1103
115 if value == "":
116 return None
117 else:
118 return value
119
120
98def relation_get(relation_data):121def relation_get(relation_data):
99 """ Obtain all current relation data122 """ Obtain all current relation data
100 relation_data is a list of options to query from the relation123 relation_data is a list of options to query from the relation
@@ -356,8 +379,7 @@
356 create_role("KeystoneAdmin", config["admin-user"], 'admin')379 create_role("KeystoneAdmin", config["admin-user"], 'admin')
357 create_role("KeystoneServiceAdmin", config["admin-user"], 'admin')380 create_role("KeystoneServiceAdmin", config["admin-user"], 'admin')
358 create_service_entry("keystone", "identity", "Keystone Identity Service")381 create_service_entry("keystone", "identity", "Keystone Identity Service")
359 # following documentation here, perhaps we should be using juju382
360 # public/private addresses for public/internal urls.
361 if is_clustered():383 if is_clustered():
362 juju_log("Creating endpoint for clustered configuration")384 juju_log("Creating endpoint for clustered configuration")
363 for region in config['region'].split():385 for region in config['region'].split():
@@ -543,17 +565,32 @@
543 Broadcast service credentials to peers or consume those that have been565 Broadcast service credentials to peers or consume those that have been
544 broadcasted by peer, depending on hook context.566 broadcasted by peer, depending on hook context.
545 '''567 '''
546 if os.path.basename(sys.argv[0]) == 'cluster-relation-changed':568 if (not eligible_leader() or
547 r_data = relation_get_dict()569 not os.path.isfile(SERVICE_PASSWD_PATH)):
548 if 'service_credentials' in r_data:
549 juju_log('Saving service passwords from peer.')
550 save_stored_passwords(**json.loads(r_data['service_credentials']))
551 return
552
553 creds = load_stored_passwords()
554 if not creds:
555 return570 return
556 juju_log('Synchronizing service passwords to all peers.')571 juju_log('Synchronizing service passwords to all peers.')
557 creds = json.dumps(creds)572 unison.sync_to_peers(peer_interface='cluster',
558 for r_id in (relation_ids('cluster') or []):573 paths=[SERVICE_PASSWD_PATH], user=SSH_USER,
559 relation_set_2(rid=r_id, service_credentials=creds)574 verbose=True)
575
576CA = []
577def get_ca(user='keystone', group='keystone'):
578 """
579 Initialize a new CA object if one hasn't already been loaded.
580 This will create a new CA or load an existing one.
581 """
582 if not CA:
583 if not os.path.isdir(SSL_DIR):
584 os.mkdir(SSL_DIR)
585 d_name = '_'.join(SSL_CA_NAME.lower().split(' '))
586 ca = ssl.JujuCA(name=SSL_CA_NAME, user=user, group=group,
587 ca_dir=os.path.join(SSL_DIR,
588 '%s_intermediate_ca' % d_name),
589 root_ca_dir=os.path.join(SSL_DIR,
590 '%s_root_ca' % d_name))
591 # SSL_DIR is synchronized via all peers over unison+ssh, need
592 # to ensure permissions.
593 execute('chown -R %s.%s %s' % (user, group, SSL_DIR))
594 execute('chmod -R g+rwx %s' % SSL_DIR)
595 CA.append(ca)
596 return CA[0]
560597
=== modified file 'revision'
--- revision 2013-03-06 21:50:30 +0000
+++ revision 2013-03-08 21:36:24 +0000
@@ -1,1 +1,1 @@
11981211

Subscribers

People subscribed via source and target branches