Merge lp:~hazmat/charms/precise/mysql/ssl-everywhere into lp:charms/mysql

Proposed by Kapil Thangavelu
Status: Superseded
Proposed branch: lp:~hazmat/charms/precise/mysql/ssl-everywhere
Merge into: lp:charms/mysql
Diff against target: 824 lines (+481/-135)
8 files modified
config.yaml (+18/-0)
hooks/common.py (+49/-11)
hooks/config-changed (+37/-6)
hooks/db-relation-joined (+32/-26)
hooks/shared_db_relations.py (+88/-91)
hooks/slave-relation-changed (+3/-0)
hooks/sslca.py (+253/-0)
revision (+1/-1)
To merge this branch: bzr merge lp:~hazmat/charms/precise/mysql/ssl-everywhere
Reviewer Review Type Date Requested Status
charmers Pending
Review via email: mp+207904@code.launchpad.net

This proposal has been superseded by a proposal from 2014-03-04.

Description of the change

SSL Support for MySQL

Adds a few additional configuration options for SSL.

'ssl' is the primary one, it can be 'off', 'on', or 'only'.

If its off, no changes. If its 'on' then ssl support is
enabled and ssl_ca cert is sent to clients along the relation.

If its only, then all clients must connect using ssl.

The server cert/key and ca cert can be provided via config. If
they are not, the service will act as its own ca, and also
in only mode will also generate client certs and pass them
along the relation (the client use of those certs is not
required atm).

https://codereview.appspot.com/68120043/

To post a comment you must log in.
Revision history for this message
Kapil Thangavelu (hazmat) wrote :

Reviewers: mp+207904_code.launchpad.net,

Message:
Please take a look.

Description:
SSL Support for MySQL

Adds a few additional configuration options for SSL.

'ssl' is the primary one, it can be 'off', 'on', or 'only'.

If its off, no changes. If its 'on' then ssl support is
enabled and ssl_ca cert is sent to clients along the relation.

If its only, then all clients must connect using ssl.

The server cert/key and ca cert can be provided via config. If
they are not, the service will act as its own ca, and also
in only mode will also generate client certs and pass them
along the relation (the client use of those certs is not
required atm).

https://code.launchpad.net/~hazmat/charms/precise/mysql/ssl-everywhere/+merge/207904

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/68120043/

Affected files (+483, -135 lines):
   A [revision details]
   M config.yaml
   M hooks/common.py
   M hooks/config-changed
   M hooks/db-relation-joined
   M hooks/shared_db_relations.py
   M hooks/slave-relation-changed
   A hooks/sslca.py
   M revision

Revision history for this message
Marco Ceppi (marcoceppi) wrote :

I have some concerns about the way this affects the mysql interface
below. Overall, this is a great change and will bring security to
communication within a deployed environment, I'm open to discussion
about the points below - I simply want to make sure the charm stays as
compatible as possible as it grows.

https://codereview.appspot.com/68120043/diff/1/config.yaml
File config.yaml (right):

https://codereview.appspot.com/68120043/diff/1/config.yaml#newcode38
config.yaml:38: ssl_key:
I don't like that these are _'d while the majority of the configuration
options for MySQL are hypenated (with the exception of vip_iface, and
vip_cidr which I would have rejected had I maintained the charm at the
time). If there isn't a hard requirement for these to be _ then I'd
prefer if they followed the convention of the majority of the charm's
config.

https://codereview.appspot.com/68120043/diff/1/hooks/db-relation-joined
File hooks/db-relation-joined (right):

https://codereview.appspot.com/68120043/diff/1/hooks/db-relation-joined#newcode91
hooks/db-relation-joined:91: cmd.append("%s=%s" % (k, v))
With this, you're now overloading the mysql interface to the point
where, if a user deploys the MySQL charm, configures it for SSL, then
joins to a charm that doesn't know how to speak to MySQL on SSL with on
and off, no change (I assume), with ssl=only that charm just won't work.

Given the nature of this change, it seem more apt that this should be
implemented as a new interface, maybe mysql-ssl - so when you connect on
that interface and no ssl cert/ca/key are provided MySQL generates one,
otherwise uses the values provided by the configuration. In doing so
services can expose their compat with MySQL over SSL without having
cases that charms will break if MySQL is configured in a manner that's
too restrictive.

https://codereview.appspot.com/68120043/

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

i disagree, i think that makes for a lousy experience to make a separate
interface, instead of adding ssl to one place now you have to reconfigure
your entire infrastructure to add it later (breaking and readding
relations, etc). Imagine trying to upgrade a production system to take
advantage of ssl.. instead of ugprade charm and config ssl, you now have to
break your entire production system. Its only the config ssl only that
forces the client to ssl. the ssl_ key names are chosen for consistency
with ssl support through the charm ecosystem, there are several others
already using these keys.

-k

On Thu, Feb 27, 2014 at 9:24 AM, Marco Ceppi <email address hidden> wrote:

> I have some concerns about the way this affects the mysql interface
> below. Overall, this is a great change and will bring security to
> communication within a deployed environment, I'm open to discussion
> about the points below - I simply want to make sure the charm stays as
> compatible as possible as it grows.
>
>
> https://codereview.appspot.com/68120043/diff/1/config.yaml
> File config.yaml (right):
>
> https://codereview.appspot.com/68120043/diff/1/config.yaml#newcode38
> config.yaml:38: ssl_key:
> I don't like that these are _'d while the majority of the configuration
> options for MySQL are hypenated (with the exception of vip_iface, and
> vip_cidr which I would have rejected had I maintained the charm at the
> time). If there isn't a hard requirement for these to be _ then I'd
> prefer if they followed the convention of the majority of the charm's
> config.
>
> https://codereview.appspot.com/68120043/diff/1/hooks/db-relation-joined
> File hooks/db-relation-joined (right):
>
>
> https://codereview.appspot.com/68120043/diff/1/hooks/db-relation-joined#newcode91
> hooks/db-relation-joined:91: cmd.append("%s=%s" % (k, v))
> With this, you're now overloading the mysql interface to the point
> where, if a user deploys the MySQL charm, configures it for SSL, then
> joins to a charm that doesn't know how to speak to MySQL on SSL with on
> and off, no change (I assume), with ssl=only that charm just won't work.
>
> Given the nature of this change, it seem more apt that this should be
> implemented as a new interface, maybe mysql-ssl - so when you connect on
> that interface and no ssl cert/ca/key are provided MySQL generates one,
> otherwise uses the values provided by the configuration. In doing so
> services can expose their compat with MySQL over SSL without having
> cases that charms will break if MySQL is configured in a manner that's
> too restrictive.
>
> https://codereview.appspot.com/68120043/
>
> --
>
> https://code.launchpad.net/~hazmat/charms/precise/mysql/ssl-everywhere/+merge/207904
> You are the owner of lp:~hazmat/charms/precise/mysql/ssl-everywhere.
>

Revision history for this message
Kapil Thangavelu (hazmat) wrote :
Download full text (3.1 KiB)

plus with that logic, your talking about 3 extra interfaces to mirror the
three client interfaces we have now on mysql. that seems silly.

On Thu, Feb 27, 2014 at 9:30 AM, Kapil Thangavelu <
<email address hidden>> wrote:

> i disagree, i think that makes for a lousy experience to make a separate
> interface, instead of adding ssl to one place now you have to reconfigure
> your entire infrastructure to add it later (breaking and readding
> relations, etc). Imagine trying to upgrade a production system to take
> advantage of ssl.. instead of ugprade charm and config ssl, you now have to
> break your entire production system. Its only the config ssl only that
> forces the client to ssl. the ssl_ key names are chosen for consistency
> with ssl support through the charm ecosystem, there are several others
> already using these keys.
>
> -k
>
>
>
> On Thu, Feb 27, 2014 at 9:24 AM, Marco Ceppi <email address hidden> wrote:
>
>> I have some concerns about the way this affects the mysql interface
>> below. Overall, this is a great change and will bring security to
>> communication within a deployed environment, I'm open to discussion
>> about the points below - I simply want to make sure the charm stays as
>> compatible as possible as it grows.
>>
>>
>> https://codereview.appspot.com/68120043/diff/1/config.yaml
>> File config.yaml (right):
>>
>> https://codereview.appspot.com/68120043/diff/1/config.yaml#newcode38
>> config.yaml:38: ssl_key:
>> I don't like that these are _'d while the majority of the configuration
>> options for MySQL are hypenated (with the exception of vip_iface, and
>> vip_cidr which I would have rejected had I maintained the charm at the
>> time). If there isn't a hard requirement for these to be _ then I'd
>> prefer if they followed the convention of the majority of the charm's
>> config.
>>
>> https://codereview.appspot.com/68120043/diff/1/hooks/db-relation-joined
>> File hooks/db-relation-joined (right):
>>
>>
>> https://codereview.appspot.com/68120043/diff/1/hooks/db-relation-joined#newcode91
>> hooks/db-relation-joined:91<https://codereview.appspot.com/68120043/diff/1/hooks/db-relation-joined#newcode91hooks/db-relation-joined:91>:
>> cmd.append("%s=%s" % (k, v))
>> With this, you're now overloading the mysql interface to the point
>> where, if a user deploys the MySQL charm, configures it for SSL, then
>> joins to a charm that doesn't know how to speak to MySQL on SSL with on
>> and off, no change (I assume), with ssl=only that charm just won't work.
>>
>> Given the nature of this change, it seem more apt that this should be
>> implemented as a new interface, maybe mysql-ssl - so when you connect on
>> that interface and no ssl cert/ca/key are provided MySQL generates one,
>> otherwise uses the values provided by the configuration. In doing so
>> services can expose their compat with MySQL over SSL without having
>> cases that charms will break if MySQL is configured in a manner that's
>> too restrictive.
>>
>> https://codereview.appspot.com/68120043/
>>
>> --
>>
>> https://code.launchpad.net/~hazmat/charms/precise/mysql/ssl-everywhere/+merge/207904
>> You are the owner of lp:~hazmat/charms/precise/mysql/ssl-everywhere....

Read more...

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

fwiw, i'm refactoring up this branch a bit to use charm-helpers and to cleanly re-configure existing relations when the setting changes at runtime. the core feature functionality is the same though, so the interface extension discussion remains.

Unmerged revisions

118. By Kapil Thangavelu

external ca and cert support

117. By Kapil Thangavelu

mysql is picky about cert gen, accomodate

116. By Kapil Thangavelu

ftest fixes

115. By Kapil Thangavelu

off is yaml magic for bool, quote appropriately

114. By Kapil Thangavelu

ssl support

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 2014-02-04 21:48:28 +0000
3+++ config.yaml 2014-02-24 11:48:01 +0000
4@@ -31,6 +31,24 @@
5 default: 'MIXED'
6 type: string
7 description: If binlogging is enabled, this is the format that will be used. Ignored when tuning-level == fast.
8+ ssl:
9+ default: 'off'
10+ type: string
11+ description: Enable SSL connections on mysql, valid values are 'off', 'on', and 'only'
12+ ssl_key:
13+ type: string
14+ description: private unencrypted key in PEM format (starts "-----BEGIN RSA PRIVATE KEY-----")
15+ default: ""
16+ ssl_cert:
17+ type: string
18+ description: X.509 certificate in PEM format (starts "-----BEGIN CERTIFICATE-----")
19+ default: ""
20+ ssl_ca:
21+ type: string
22+ description: |
23+ Certificate authority cert that signed pem. Optional if the ssl_cert is signed by a ca
24+ recognized by the os.
25+ default: ""
26 vip:
27 type: string
28 default: ''
29
30=== modified file 'hooks/common.py'
31--- hooks/common.py 2013-02-12 17:54:29 +0000
32+++ hooks/common.py 2014-02-24 11:48:01 +0000
33@@ -1,15 +1,51 @@
34 # vim: syntax=python
35
36+import base64
37 import os
38+import json
39 import sys
40 import MySQLdb
41+import sslca
42 import subprocess
43 import uuid
44
45+
46 def get_service_user_file(service):
47 return '/var/lib/mysql/%s.service_user2' % service
48
49
50+def get_service_ca():
51+ service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
52+ ca_path = os.path.join(os.environ['CHARM_DIR'], 'ca')
53+ ca = sslca.ServiceCA(service_name, ca_path, sslca.MYSQL_CERT)
54+ ca.init()
55+ return ca
56+
57+
58+def is_ext_ssl_ca():
59+ ssl_key = configs.get('ssl_key')
60+ ssl_cert = configs.get('ssl_cert')
61+ return all((ssl_key, ssl_cert))
62+
63+
64+def configure_client_ssl():
65+ if configs.get('ssl', '') not in ('only', 'on'):
66+ return dict()
67+ if is_ext_ssl_ca():
68+ if configs.get('ssl_ca'):
69+ return dict(ssl_ca=base64.b64encode(configs['ssl_ca']))
70+ return
71+ # self-managed-client-certs
72+ ca = get_service_ca()
73+ ca_bundle = ca.get_ca_bundle()
74+ service_cert, service_key = ca.get_or_create_cert(
75+ change_unit.split('/', 1)[0])
76+ return dict(
77+ ssl_ca=base64.b64encode(ca_bundle),
78+ ssl_key=base64.b64encode(service_key),
79+ ssl_cert=base64.b64encode(service_cert))
80+
81+
82 def get_service_user(service):
83 if service == '':
84 return (None, None)
85@@ -17,7 +53,8 @@
86 if os.path.exists(sfile):
87 with open(sfile, 'r') as f:
88 return (f.readline().strip(), f.readline().strip())
89- (suser, service_password) = subprocess.check_output(['pwgen', '-N 2', '15']).strip().split("\n")
90+ (suser, service_password) = subprocess.check_output(
91+ ['pwgen', '-N 2', '15']).strip().split("\n")
92 with open(sfile, 'w') as f:
93 f.write("%s\n" % suser)
94 f.write("%s\n" % service_password)
95@@ -55,6 +92,8 @@
96 slave = os.path.exists('/var/lib/juju/i.am.a.slave')
97 broken_path = '/var/lib/juju/%s.mysql.broken' % database_name
98 broken = os.path.exists(broken_path)
99+configs = json.loads(subprocess.check_output(['config-get', '--format=json']))
100+
101
102 def get_db_cursor():
103 # Connect to mysql
104@@ -95,15 +134,14 @@
105 return "GRANT ALL PRIVILEGES ON `{}`".format(db_name) in grants
106
107
108-def create_grant(db_name, db_user,
109- remote_ip, password):
110+def create_grant(db_name, db_user, remote_ip, password):
111 cursor = get_db_cursor()
112+ grant = "GRANT ALL PRIVILEGES ON {}.* TO '{}'@'{}' IDENTIFIED BY '{}'"
113+ # http://dev.mysql.com/doc/refman/5.5/en/grant.html
114+ if configs.get('ssl') == 'only':
115+ grant += ' REQUIRE SSL'
116 try:
117- cursor.execute("GRANT ALL PRIVILEGES ON {}.* TO '{}'@'{}' "\
118- "IDENTIFIED BY '{}'".format(db_name,
119- db_user,
120- remote_ip,
121- password))
122+ cursor.execute(grant.format(db_name, db_user, remote_ip, password))
123 finally:
124 cursor.close()
125
126@@ -112,8 +150,8 @@
127 remote_ip):
128 cursor = get_db_cursor()
129 try:
130- cursor.execute("DROP FROM mysql.user WHERE user='{}' "\
131- "AND HOST='{}'".format(db_user,
132- remote_ip))
133+ cursor.execute(
134+ "DROP FROM mysql.user WHERE user='{}' AND HOST='{}'".format(
135+ db_user, remote_ip))
136 finally:
137 cursor.close()
138
139=== modified file 'hooks/config-changed'
140--- hooks/config-changed 2014-02-15 21:37:45 +0000
141+++ hooks/config-changed 2014-02-24 11:48:01 +0000
142@@ -10,6 +10,8 @@
143 import platform
144 from string import upper
145
146+from common import get_service_ca, configs
147+
148 num_re = re.compile('^[0-9]+$')
149
150 # There should be a library for this
151@@ -48,7 +50,6 @@
152 if IS_32BIT_SYSTEM:
153 check_call(['juju-log','-l','INFO','32bit system restrictions in play'])
154
155-configs=json.loads(check_output(['config-get','--format=json']))
156
157 def get_memtotal():
158 with open('/proc/meminfo') as meminfo_file:
159@@ -153,6 +154,36 @@
160 else:
161 configs['max-connections'] = 'max_connections = %s' % configs['max-connections']
162
163+
164+if configs.get('ssl', 'off').strip() in ('on', 'only'):
165+ service_name = os.environ.get('JUJU_UNIT_NAME').split('/')[0]
166+ ssl_cert, ssl_key, ssl_ca = (
167+ configs.get('ssl_cert'),
168+ configs.get('ssl_key'),
169+ configs.get('ssl_ca'))
170+
171+ if any((ssl_cert, ssl_key, ssl_ca)):
172+ if not all((ssl_cert, ssl_key)):
173+ print "ERROR", "SSL Key and cert are both required if either specified"
174+ sys.exit(1)
175+ else:
176+ ca = get_service_ca()
177+ ssl_cert, ssl_key = ca.create_certificate(service_name)
178+ ssl_ca = ca.get_ca_bundle()
179+
180+ with open('/etc/mysql/cacert.pem', 'w') as fh:
181+ fh.write(ssl_ca)
182+ with open('/etc/mysql/server-cert.pem', 'w') as fh:
183+ fh.write(ssl_cert)
184+ with open('/etc/mysql/server-key.pem', 'w') as fh:
185+ fh.write(ssl_key)
186+ if ssl_ca: # In case a user providers key/cert but no ca cause its official.
187+ configs['ssl_ca_marker'] = ''
188+ if ssl_key:
189+ configs['ssl_cert_marker'] = ''
190+else:
191+ configs['ssl_cert_marker'] = configs['ssl_ca_marker'] = "#"
192+
193 template="""
194 ######################################
195 #
196@@ -171,7 +202,7 @@
197 # You can copy this to one of:
198 # - "/etc/mysql/my.cnf" to set global options,
199 # - "~/.my.cnf" to set user-specific options.
200-#
201+#
202 # One can use all long options that the program supports.
203 # Run program with --help to get a list of available options and with
204 # --print-defaults to see which it would actually understand and use.
205@@ -277,9 +308,9 @@
206 #
207 # For generating SSL certificates I recommend the OpenSSL GUI "tinyca".
208 #
209-# ssl-ca=/etc/mysql/cacert.pem
210-# ssl-cert=/etc/mysql/server-cert.pem
211-# ssl-key=/etc/mysql/server-key.pem
212+%(ssl_ca_marker)s ssl-ca=/etc/mysql/cacert.pem
213+%(ssl_cert_marker)s ssl-cert=/etc/mysql/server-cert.pem
214+%(ssl_cert_marker)s ssl-key=/etc/mysql/server-key.pem
215
216
217
218@@ -326,7 +357,7 @@
219
220 need_restart = False
221 for target,content in targets.iteritems():
222- tdir = os.path.dirname(target)
223+ tdir = os.path.dirname(target)
224 if len(content) == 0 and os.path.exists(target):
225 os.unlink(target)
226 need_restart = True
227
228=== modified file 'hooks/db-relation-joined'
229--- hooks/db-relation-joined 2012-11-02 06:41:12 +0000
230+++ hooks/db-relation-joined 2014-02-24 11:48:01 +0000
231@@ -1,22 +1,27 @@
232 #!/usr/bin/env python
233
234-from common import *
235+from common import (
236+ change_unit, configs, get_db_cursor, get_service_ca, is_ext_ssl_ca,
237+ configure_client_ssl)
238
239-import urllib
240-import subprocess
241+import base64
242+import random
243 import os
244+import pickle
245 import sys
246 import string
247-import random
248-import pickle
249+import subprocess
250+import urllib
251+
252
253 cursor = get_db_cursor()
254
255 admin = os.path.basename(sys.argv[0]) == 'db-admin-relation-joined'
256
257 def runsql(sql):
258- print "[%s]" % sql
259- cursor.execute(sql)
260+ print "[%s]" % sql
261+ cursor.execute(sql)
262+
263
264 runsql(
265 "grant replication client on *.* to `%s` identified by '%s'" % (
266@@ -27,33 +32,33 @@
267 try:
268 runsql(
269 "revoke all on `%s`.* from `%s`" % (
270- database_name,
271+ database_name,
272 user))
273 except:
274 print "revoke failed, ignoring error"
275 runsql(
276 "grant select on `%s`.* to `%s`" % (
277- database_name,
278+ database_name,
279 user))
280 else:
281 if admin:
282- runsql(
283- "grant all privileges on *.* to `%s` identified by '%s'" % (
284- user,
285- service_password))
286+ grant = "grant all privileges on *.* to `%s` identified by '%s'"
287+ if configs.get('ssl', '') == 'only':
288+ grant += " REQUIRE SSL"
289+ runsql( grant % (user, service_password))
290 else:
291- runsql(
292- "grant all on `%s`.* to `%s` identified by '%s'" % (
293- database_name,
294- user,
295- service_password))
296+ grant = "grant all on `%s`.* to `%s` identified by '%s'"
297+ if configs.get('ssl', '') == 'only':
298+ grant += " REQUIRE SSL"
299+ runsql( grant % (database_name, user, service_password))
300
301 hostname = subprocess.check_output(['unit-get','private-address']).strip()
302
303+
304 print str(["relation-set",
305 "database=%s" % database_name,
306 "user=%s" % user,
307- "password=%s" % service_password,
308+# "password=%s" % service_password,
309 'host=%s' % hostname,
310 'slave=%s' % slave])
311
312@@ -78,10 +83,11 @@
313 cursor.close()
314
315 # Store new values in relation settings.
316-subprocess.call(
317- ["relation-set",
318- "database=%s" % database_name,
319- "user=%s" % user,
320- "password=%s" % service_password,
321- 'host=%s' % hostname,
322- 'slave=%s' % slave,])
323+cmd = ["relation-set", "database=%s" % database_name, 'host=%s' % hostname,
324+ "user=%s" % user, "password=%s" % service_password,
325+ 'slave=%s' % slave,])
326+
327+for k, v in configure_client_ssl().items():
328+ cmd.append("%s=%s" % (k, v))
329+
330+subprocess.check_call(cmd)
331
332=== modified file 'hooks/shared_db_relations.py'
333--- hooks/shared_db_relations.py 2013-07-10 16:32:52 +0000
334+++ hooks/shared_db_relations.py 2014-02-24 11:48:01 +0000
335@@ -11,15 +11,22 @@
336 from common import (
337 database_exists,
338 create_database,
339+ configure_client_ssl,
340 grant_exists,
341- create_grant
342- )
343-import subprocess
344+ create_grant,
345+ configs,
346+ change_unit,
347+ get_service_ca)
348+
349+import base64
350+import functools
351 import json
352-import socket
353-import os
354 import lib.utils as utils
355 import lib.cluster_utils as cluster
356+import os
357+import subprocess
358+import socket
359+
360
361 LEADER_RES = 'res_mysql_vip'
362
363@@ -29,44 +36,36 @@
364
365
366 def relation_get():
367- return json.loads(subprocess.check_output(
368- ['relation-get',
369- '--format',
370- 'json']
371- )
372- )
373+ return json.loads(
374+ subprocess.check_output(['relation-get', '--format', 'json']))
375+
376+
377+local_hostname = utils.unit_get('private-address')
378+
379+
380+def configure_db(hostname, database, username):
381+ passwd_file = "/var/lib/mysql/mysql-{}.passwd".format(username)
382+ if hostname != local_hostname:
383+ remote_ip = socket.gethostbyname(hostname)
384+ else:
385+ remote_ip = '127.0.0.1'
386+
387+ if not os.path.exists(passwd_file):
388+ password = pwgen()
389+ with open(passwd_file, 'w') as pfile:
390+ pfile.write(password)
391+ else:
392+ with open(passwd_file) as pfile:
393+ password = pfile.read().strip()
394+
395+ if not database_exists(database):
396+ create_database(database)
397+ if not grant_exists(database, username, remote_ip):
398+ create_grant(database, username, remote_ip, password)
399+ return password
400
401
402 def shared_db_changed():
403-
404- def configure_db(hostname,
405- database,
406- username):
407- passwd_file = "/var/lib/mysql/mysql-{}.passwd"\
408- .format(username)
409- if hostname != local_hostname:
410- remote_ip = socket.gethostbyname(hostname)
411- else:
412- remote_ip = '127.0.0.1'
413-
414- if not os.path.exists(passwd_file):
415- password = pwgen()
416- with open(passwd_file, 'w') as pfile:
417- pfile.write(password)
418- else:
419- with open(passwd_file) as pfile:
420- password = pfile.read().strip()
421-
422- if not database_exists(database):
423- create_database(database)
424- if not grant_exists(database,
425- username,
426- remote_ip):
427- create_grant(database,
428- username,
429- remote_ip, password)
430- return password
431-
432 if not cluster.eligible_leader(LEADER_RES):
433 utils.juju_log('INFO',
434 'MySQL service is peered, bailing shared-db relation'
435@@ -74,64 +73,62 @@
436 return
437
438 settings = relation_get()
439- local_hostname = utils.unit_get('private-address')
440- singleset = set([
441- 'database',
442- 'username',
443- 'hostname'
444- ])
445+ singleset = set(['database', 'username', 'hostname'])
446+
447+ if configs.get('ssl', '').strip() in ('on', 'only'):
448+ client_ssl = configure_client_ssl()
449+ relation_set = functools.partial(utils.relation_set, **client_ssl)
450+ else:
451+ relation_set = utils.relation_set
452
453 if singleset.issubset(settings):
454 # Process a single database configuration
455- password = configure_db(settings['hostname'],
456- settings['database'],
457- settings['username'])
458+ password = configure_db(
459+ settings['hostname'], settings['database'], settings['username'])
460 if not cluster.is_clustered():
461- utils.relation_set(db_host=local_hostname,
462- password=password)
463+ relation_set(db_host=local_hostname, password=password)
464 else:
465- utils.relation_set(db_host=utils.config_get("vip"),
466- password=password)
467+ relation_set(db_host=utils.config_get("vip"), password=password)
468+ return
469
470+ # Process multiple database setup requests.
471+ # from incoming relation data:
472+ # nova_database=xxx nova_username=xxx nova_hostname=xxx
473+ # quantum_database=xxx quantum_username=xxx quantum_hostname=xxx
474+ # create
475+ #{
476+ # "nova": {
477+ # "username": xxx,
478+ # "database": xxx,
479+ # "hostname": xxx
480+ # },
481+ # "quantum": {
482+ # "username": xxx,
483+ # "database": xxx,
484+ # "hostname": xxx
485+ # }
486+ #}
487+ #
488+ databases = {}
489+ for k, v in settings.iteritems():
490+ db = k.split('_')[0]
491+ x = '_'.join(k.split('_')[1:])
492+ if db not in databases:
493+ databases[db] = {}
494+ databases[db][x] = v
495+ return_data = {}
496+ for db in databases:
497+ if singleset.issubset(databases[db]):
498+ return_data['_'.join([db, 'password'])] = \
499+ configure_db(databases[db]['hostname'],
500+ databases[db]['database'],
501+ databases[db]['username'])
502+ if len(return_data) > 0:
503+ relation_set(**return_data)
504+ if not cluster.is_clustered():
505+ relation_set(db_host=local_hostname)
506 else:
507- # Process multiple database setup requests.
508- # from incoming relation data:
509- # nova_database=xxx nova_username=xxx nova_hostname=xxx
510- # quantum_database=xxx quantum_username=xxx quantum_hostname=xxx
511- # create
512- #{
513- # "nova": {
514- # "username": xxx,
515- # "database": xxx,
516- # "hostname": xxx
517- # },
518- # "quantum": {
519- # "username": xxx,
520- # "database": xxx,
521- # "hostname": xxx
522- # }
523- #}
524- #
525- databases = {}
526- for k, v in settings.iteritems():
527- db = k.split('_')[0]
528- x = '_'.join(k.split('_')[1:])
529- if db not in databases:
530- databases[db] = {}
531- databases[db][x] = v
532- return_data = {}
533- for db in databases:
534- if singleset.issubset(databases[db]):
535- return_data['_'.join([db, 'password'])] = \
536- configure_db(databases[db]['hostname'],
537- databases[db]['database'],
538- databases[db]['username'])
539- if len(return_data) > 0:
540- utils.relation_set(**return_data)
541- if not cluster.is_clustered():
542- utils.relation_set(db_host=local_hostname)
543- else:
544- utils.relation_set(db_host=utils.config_get("vip"))
545+ relation_set(db_host=utils.config_get("vip"))
546
547 hooks = {
548 "shared-db-relation-changed": shared_db_changed
549
550=== modified file 'hooks/slave-relation-changed'
551--- hooks/slave-relation-changed 2013-02-12 17:54:29 +0000
552+++ hooks/slave-relation-changed 2014-02-24 11:48:01 +0000
553@@ -87,3 +87,6 @@
554 mysql $ROOTARGS -e "START SLAVE"
555 mysql $ROOTARGS -e "SHOW SLAVE STATUS"
556 touch /var/lib/juju/i.am.a.slave
557+
558+
559+configs=json.loads(check_output(['config-get','--format=json']))
560
561=== added file 'hooks/sslca.py'
562--- hooks/sslca.py 1970-01-01 00:00:00 +0000
563+++ hooks/sslca.py 2014-02-24 11:48:01 +0000
564@@ -0,0 +1,253 @@
565+import logging
566+import datetime
567+import os
568+from os.path import join as path_join
569+from os.path import exists
570+import subprocess
571+
572+
573+log = logging.getLogger("service_ca")
574+
575+logging.basicConfig(level=logging.DEBUG)
576+
577+STD_CERT = "standard"
578+
579+# Mysql server is fairly picky about cert creation
580+# and types, spec its creation separately for now.
581+MYSQL_CERT = "mysql"
582+
583+
584+class ServiceCA(object):
585+
586+ default_expiry = str(365 * 2)
587+ default_ca_expiry = str(365 * 6)
588+
589+ def __init__(self, name, ca_dir, cert_type=STD_CERT):
590+ self.name = name
591+ self.ca_dir = ca_dir
592+ self.cert_type = cert_type
593+
594+ def init(self):
595+ log.debug("initializing service ca")
596+ if not exists(self.ca_dir):
597+ self._init_ca_dir(self.ca_dir)
598+ self._init_ca()
599+
600+ @property
601+ def ca_key(self):
602+ return path_join(self.ca_dir, 'private', 'cacert.key')
603+
604+ @property
605+ def ca_cert(self):
606+ return path_join(self.ca_dir, 'cacert.pem')
607+
608+ @property
609+ def ca_conf(self):
610+ return path_join(self.ca_dir, 'ca.cnf')
611+
612+ @property
613+ def signing_conf(self):
614+ return path_join(self.ca_dir, 'signing.cnf')
615+
616+ def _init_ca_dir(self, ca_dir):
617+ os.mkdir(ca_dir)
618+ for i in ['certs', 'crl', 'newcerts', 'private']:
619+ sd = path_join(ca_dir, i)
620+ if not exists(sd):
621+ os.mkdir(sd)
622+
623+ if not exists(path_join(ca_dir, 'serial')):
624+ with open(path_join(ca_dir, 'serial'), 'wb') as fh:
625+ fh.write('02\n')
626+
627+ if not exists(path_join(ca_dir, 'index.txt')):
628+ with open(path_join(ca_dir, 'index.txt'), 'wb') as fh:
629+ fh.write('')
630+
631+ def _init_ca(self):
632+ """Generate the root ca's cert and key.
633+ """
634+ if not exists(path_join(self.ca_dir, 'ca.cnf')):
635+ with open(path_join(self.ca_dir, 'ca.cnf'), 'wb') as fh:
636+ fh.write(
637+ CA_CONF_TEMPLATE % (self.get_conf_variables()))
638+
639+ if not exists(path_join(self.ca_dir, 'signing.cnf')):
640+ with open(path_join(self.ca_dir, 'signing.cnf'), 'wb') as fh:
641+ fh.write(
642+ SIGNING_CONF_TEMPLATE % (self.get_conf_variables()))
643+
644+ if exists(self.ca_cert) or exists(self.ca_key):
645+ raise RuntimeError("Initialized called when CA already exists")
646+ cmd = ['openssl', 'req', '-config', self.ca_conf,
647+ '-x509', '-nodes', '-newkey', 'rsa',
648+ '-days', self.default_ca_expiry,
649+ '-keyout', self.ca_key, '-out', self.ca_cert,
650+ '-outform', 'PEM']
651+ output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
652+ log.debug("CA Init:\n %s", output)
653+
654+ def get_conf_variables(self):
655+ return dict(
656+ org_name="juju",
657+ org_unit_name="%s service" % self.name,
658+ common_name=self.name,
659+ ca_dir=self.ca_dir)
660+
661+ def get_or_create_cert(self, common_name):
662+ if common_name in self:
663+ return self.get_certificate(common_name)
664+ return self.create_certificate(common_name)
665+
666+ def create_certificate(self, common_name):
667+ if common_name in self:
668+ return self.get_certificate(common_name)
669+ key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)
670+ crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
671+ csr_p = path_join(self.ca_dir, "certs", "%s.csr" % common_name)
672+ self._create_certificate(common_name, key_p, csr_p, crt_p)
673+ return self.get_certificate(common_name)
674+
675+ def get_certificate(self, common_name):
676+ if not common_name in self:
677+ raise ValueError("No certificate for %s" % common_name)
678+ key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)
679+ crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
680+ with open(crt_p) as fh:
681+ crt = fh.read()
682+ with open(key_p) as fh:
683+ key = fh.read()
684+ # Required for mysql client/server not sure if it matters
685+ # to others.
686+ #key = key.replace('BEGIN PRIVATE', 'BEGIN RSA PRIVATE')
687+ #key = key.replace('END PRIVATE', 'END RSA PRIVATE')
688+ return crt, key
689+
690+ def __contains__(self, common_name):
691+ crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
692+ return exists(crt_p)
693+
694+ def _create_certificate(self, common_name, key_p, csr_p, crt_p):
695+ template_vars = self.get_conf_variables()
696+ template_vars['common_name'] = common_name
697+ subj = '/O=%(org_name)s/OU=%(org_unit_name)s/CN=%(common_name)s' % (
698+ template_vars)
699+
700+ log.debug("CA Create Cert %s", common_name)
701+ cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa:2048',
702+ '-nodes', '-days', self.default_expiry,
703+ '-keyout', key_p, '-out', csr_p, '-subj', subj]
704+ subprocess.check_call(cmd)
705+ cmd = ['openssl', 'rsa', '-in', key_p, '-out', key_p]
706+ subprocess.check_call(cmd)
707+
708+ log.debug("CA Sign Cert %s", common_name)
709+ if self.cert_type == MYSQL_CERT:
710+ cmd = ['openssl', 'x509', '-req',
711+ '-in', csr_p, '-days', self.default_expiry,
712+ '-CA', self.ca_cert, '-CAkey', self.ca_key,
713+ '-set_serial', '01', '-out', crt_p]
714+ else:
715+ cmd = ['openssl', 'ca', '-config', self.signing_conf,
716+ '-extensions', 'req_extensions',
717+ '-days', self.default_expiry, '-notext',
718+ '-in', csr_p, '-out', crt_p, '-subj', subj, '-batch']
719+ log.debug("running %s", " ".join(cmd))
720+ subprocess.check_call(cmd)
721+
722+ def get_ca_bundle(self):
723+ with open(self.ca_cert) as fh:
724+ return fh.read()
725+
726+
727+CA_CONF_TEMPLATE = """
728+[ ca ]
729+default_ca = CA_default
730+
731+[ CA_default ]
732+dir = %(ca_dir)s
733+policy = policy_match
734+database = $dir/index.txt
735+serial = $dir/serial
736+certs = $dir/certs
737+crl_dir = $dir/crl
738+new_certs_dir = $dir/newcerts
739+certificate = $dir/cacert.pem
740+private_key = $dir/private/cacert.key
741+RANDFILE = $dir/private/.rand
742+default_md = default
743+
744+[ req ]
745+default_bits = 1024
746+default_md = sha1
747+
748+prompt = no
749+distinguished_name = ca_distinguished_name
750+
751+x509_extensions = ca_extensions
752+
753+[ ca_distinguished_name ]
754+organizationName = %(org_name)s
755+organizationalUnitName = %(org_unit_name)s Certificate Authority
756+
757+
758+[ policy_match ]
759+countryName = optional
760+stateOrProvinceName = optional
761+organizationName = match
762+organizationalUnitName = optional
763+commonName = supplied
764+
765+[ ca_extensions ]
766+basicConstraints = critical,CA:true
767+subjectKeyIdentifier = hash
768+authorityKeyIdentifier = keyid:always, issuer
769+keyUsage = cRLSign, keyCertSign
770+"""
771+
772+
773+SIGNING_CONF_TEMPLATE = """
774+[ ca ]
775+default_ca = CA_default
776+
777+[ CA_default ]
778+dir = %(ca_dir)s
779+policy = policy_match
780+database = $dir/index.txt
781+serial = $dir/serial
782+certs = $dir/certs
783+crl_dir = $dir/crl
784+new_certs_dir = $dir/newcerts
785+certificate = $dir/cacert.pem
786+private_key = $dir/private/cacert.key
787+RANDFILE = $dir/private/.rand
788+default_md = default
789+
790+[ req ]
791+default_bits = 1024
792+default_md = sha1
793+
794+prompt = no
795+distinguished_name = req_distinguished_name
796+
797+x509_extensions = req_extensions
798+
799+[ req_distinguished_name ]
800+organizationName = %(org_name)s
801+organizationalUnitName = %(org_unit_name)s machine resources
802+commonName = %(common_name)s
803+
804+[ policy_match ]
805+countryName = optional
806+stateOrProvinceName = optional
807+organizationName = match
808+organizationalUnitName = optional
809+commonName = supplied
810+
811+[ req_extensions ]
812+basicConstraints = CA:false
813+subjectKeyIdentifier = hash
814+authorityKeyIdentifier = keyid:always, issuer
815+keyUsage = digitalSignature, keyEncipherment, keyAgreement
816+extendedKeyUsage = serverAuth, clientAuth
817+"""
818
819=== modified file 'revision'
820--- revision 2014-02-13 18:51:20 +0000
821+++ revision 2014-02-24 11:48:01 +0000
822@@ -1,1 +1,1 @@
823-311
824+321

Subscribers

People subscribed via source and target branches