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

Proposed by Kapil Thangavelu
Status: Work in progress
Proposed branch: lp:~openstack-charmers/charms/precise/mysql/ssl-everywhere
Merge into: lp:charms/mysql
Diff against target: 2104 lines (+1589/-200)
16 files modified
Makefile (+10/-0)
charm-helpers.yaml (+5/-0)
config.yaml (+41/-6)
hooks/charmhelpers/contrib/ssl/__init__.py (+78/-0)
hooks/charmhelpers/contrib/ssl/service.py (+267/-0)
hooks/charmhelpers/core/hookenv.py (+401/-0)
hooks/charmhelpers/core/host.py (+297/-0)
hooks/common.py (+326/-42)
hooks/config-changed (+13/-7)
hooks/db-relation-joined (+45/-47)
hooks/ha_relations.py (+1/-0)
hooks/lib/ceph_utils.py (+4/-2)
hooks/lib/utils.py (+13/-4)
hooks/shared_db_relations.py (+85/-91)
hooks/slave-relation-changed (+1/-0)
revision (+2/-1)
To merge this branch: bzr merge lp:~openstack-charmers/charms/precise/mysql/ssl-everywhere
Reviewer Review Type Date Requested Status
Marco Ceppi (community) Needs Resubmitting
Review via email: mp+209304@code.launchpad.net

This proposal supersedes a proposal from 2014-02-24.

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal
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 : Posted in a previous version of this proposal

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.

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

LGTM! Thanks!

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

There's some merge conflicts against trunk. Could you please resolve these, I tried my best but am uncertain about a few portions.

review: Needs Resubmitting

Unmerged revisions

135. By Kapil Thangavelu

use the normalized request instead of the unit data

134. By Kapil Thangavelu

on multi-request shared db relations, redo grants for all requests if needed

133. By Kapil Thangavelu

skip relation reconfig if relation has no units

132. By Kapil Thangavelu

fix errant python code in mysql charm

131. By Kapil Thangavelu

update configure_source with pocket split

130. By Kapil Thangavelu

add cloud-archive pockets for havana and icehouse

129. By Kapil Thangavelu

merge ceph client from cloud archive support

128. By Kapil Thangavelu

don't process queued relations

127. By Kapil Thangavelu

only reconfigure client ssl if on master

126. By Kapil Thangavelu

handle no user when revoking grant

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'Makefile'
2--- Makefile 1970-01-01 00:00:00 +0000
3+++ Makefile 2014-03-05 02:21:45 +0000
4@@ -0,0 +1,10 @@
5+#!/usr/bin/make
6+PYTHON := /usr/bin/env python
7+
8+lint:
9+ @flake8 --exclude hooks/charmhelpers hooks
10+ @flake8 --exclude hooks/charmhelpers unit_tests
11+ @charm proof
12+
13+sync:
14+ @charm-helper-sync -c charm-helpers.yaml
15
16=== added file 'charm-helpers.yaml'
17--- charm-helpers.yaml 1970-01-01 00:00:00 +0000
18+++ charm-helpers.yaml 2014-03-05 02:21:45 +0000
19@@ -0,0 +1,5 @@
20+branch: lp:~openstack-charmers/charm-helpers/ssl-everywhere
21+destination: hooks/charmhelpers
22+include:
23+ - core
24+ - contrib.ssl
25\ No newline at end of file
26
27=== modified file 'config.yaml'
28--- config.yaml 2014-02-04 21:48:28 +0000
29+++ config.yaml 2014-03-05 02:21:45 +0000
30@@ -31,6 +31,24 @@
31 default: 'MIXED'
32 type: string
33 description: If binlogging is enabled, this is the format that will be used. Ignored when tuning-level == fast.
34+ ssl:
35+ default: 'off'
36+ type: string
37+ description: Enable SSL connections on mysql, valid values are 'off', 'on', and 'only'
38+ ssl_key:
39+ type: string
40+ description: Private unencrypted key base64 encoded PEM format
41+ default: ""
42+ ssl_cert:
43+ type: string
44+ description: X.509 certificate in base64 encoded PEM format
45+ default: ""
46+ ssl_ca:
47+ type: string
48+ description: |
49+ Certificate authority cert that signed cert. Optional if the ssl_cert is signed by a ca
50+ recognized by the os. Base64 encoded value.
51+ default: ""
52 vip:
53 type: string
54 default: ''
55@@ -72,9 +90,26 @@
56 default: 2
57 type: int
58 description: |
59- This value dictates the number of replicas ceph must make of any
60- object it stores within the mysql rbd pool. Of course, this only
61- applies if using Ceph as a backend store. Note that once the mysql
62- rbd pool has been created, changing this value will not have any
63- effect (although it can be changed in ceph by manually configuring
64- your ceph cluster).
65+ This value dictates the number of replicas ceph must make of any
66+ object it stores within the mysql rbd pool. Of course, this only
67+ applies if using Ceph as a backend store. Note that once the mysql
68+ rbd pool has been created, changing this value will not have any
69+ effect (although it can be changed in ceph by manually configuring
70+ your ceph cluster).
71+ openstack-origin:
72+ default: distro
73+ type: string
74+ description: |
75+ Repository from which to install ceph. May be one of the following:
76+ distro (default), ppa:somecustom/ppa, a deb url sources entry,
77+ or a supported Cloud Archive release pocket.
78+
79+ Supported Cloud Archive sources include: cloud:precise-folsom,
80+ cloud:precise-folsom/updates, cloud:precise-folsom/staging,
81+ cloud:precise-folsom/proposed.
82+
83+ Note that updating this setting to a source that is known to
84+ provide a later version of OpenStack will trigger a software
85+ upgrade.
86+
87+ Also note that this setting is used only when adding relation to CEPH.
88
89=== added directory 'hooks/charmhelpers'
90=== added file 'hooks/charmhelpers/__init__.py'
91=== added directory 'hooks/charmhelpers/contrib'
92=== added file 'hooks/charmhelpers/contrib/__init__.py'
93=== added directory 'hooks/charmhelpers/contrib/ssl'
94=== added file 'hooks/charmhelpers/contrib/ssl/__init__.py'
95--- hooks/charmhelpers/contrib/ssl/__init__.py 1970-01-01 00:00:00 +0000
96+++ hooks/charmhelpers/contrib/ssl/__init__.py 2014-03-05 02:21:45 +0000
97@@ -0,0 +1,78 @@
98+import subprocess
99+from charmhelpers.core import hookenv
100+
101+
102+def generate_selfsigned(keyfile, certfile, keysize="1024", config=None, subject=None, cn=None):
103+ """Generate selfsigned SSL keypair
104+
105+ You must provide one of the 3 optional arguments:
106+ config, subject or cn
107+ If more than one is provided the leftmost will be used
108+
109+ Arguments:
110+ keyfile -- (required) full path to the keyfile to be created
111+ certfile -- (required) full path to the certfile to be created
112+ keysize -- (optional) SSL key length
113+ config -- (optional) openssl configuration file
114+ subject -- (optional) dictionary with SSL subject variables
115+ cn -- (optional) cerfificate common name
116+
117+ Required keys in subject dict:
118+ cn -- Common name (eq. FQDN)
119+
120+ Optional keys in subject dict
121+ country -- Country Name (2 letter code)
122+ state -- State or Province Name (full name)
123+ locality -- Locality Name (eg, city)
124+ organization -- Organization Name (eg, company)
125+ organizational_unit -- Organizational Unit Name (eg, section)
126+ email -- Email Address
127+ """
128+
129+ cmd = []
130+ if config:
131+ cmd = ["/usr/bin/openssl", "req", "-new", "-newkey",
132+ "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509",
133+ "-keyout", keyfile,
134+ "-out", certfile, "-config", config]
135+ elif subject:
136+ ssl_subject = ""
137+ if "country" in subject:
138+ ssl_subject = ssl_subject + "/C={}".format(subject["country"])
139+ if "state" in subject:
140+ ssl_subject = ssl_subject + "/ST={}".format(subject["state"])
141+ if "locality" in subject:
142+ ssl_subject = ssl_subject + "/L={}".format(subject["locality"])
143+ if "organization" in subject:
144+ ssl_subject = ssl_subject + "/O={}".format(subject["organization"])
145+ if "organizational_unit" in subject:
146+ ssl_subject = ssl_subject + "/OU={}".format(subject["organizational_unit"])
147+ if "cn" in subject:
148+ ssl_subject = ssl_subject + "/CN={}".format(subject["cn"])
149+ else:
150+ hookenv.log("When using \"subject\" argument you must "
151+ "provide \"cn\" field at very least")
152+ return False
153+ if "email" in subject:
154+ ssl_subject = ssl_subject + "/emailAddress={}".format(subject["email"])
155+
156+ cmd = ["/usr/bin/openssl", "req", "-new", "-newkey",
157+ "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509",
158+ "-keyout", keyfile,
159+ "-out", certfile, "-subj", ssl_subject]
160+ elif cn:
161+ cmd = ["/usr/bin/openssl", "req", "-new", "-newkey",
162+ "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509",
163+ "-keyout", keyfile,
164+ "-out", certfile, "-subj", "/CN={}".format(cn)]
165+
166+ if not cmd:
167+ hookenv.log("No config, subject or cn provided,"
168+ "unable to generate self signed SSL certificates")
169+ return False
170+ try:
171+ subprocess.check_call(cmd)
172+ return True
173+ except Exception as e:
174+ print "Execution of openssl command failed:\n{}".format(e)
175+ return False
176
177=== added file 'hooks/charmhelpers/contrib/ssl/service.py'
178--- hooks/charmhelpers/contrib/ssl/service.py 1970-01-01 00:00:00 +0000
179+++ hooks/charmhelpers/contrib/ssl/service.py 2014-03-05 02:21:45 +0000
180@@ -0,0 +1,267 @@
181+import logging
182+import os
183+from os.path import join as path_join
184+from os.path import exists
185+import subprocess
186+
187+
188+log = logging.getLogger("service_ca")
189+
190+logging.basicConfig(level=logging.DEBUG)
191+
192+STD_CERT = "standard"
193+
194+# Mysql server is fairly picky about cert creation
195+# and types, spec its creation separately for now.
196+MYSQL_CERT = "mysql"
197+
198+
199+class ServiceCA(object):
200+
201+ default_expiry = str(365 * 2)
202+ default_ca_expiry = str(365 * 6)
203+
204+ def __init__(self, name, ca_dir, cert_type=STD_CERT):
205+ self.name = name
206+ self.ca_dir = ca_dir
207+ self.cert_type = cert_type
208+
209+ ###############
210+ # Hook Helper API
211+ @staticmethod
212+ def get_ca(type=STD_CERT):
213+ service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
214+ ca_path = os.path.join(os.environ['CHARM_DIR'], 'ca')
215+ ca = ServiceCA(service_name, ca_path, type)
216+ ca.init()
217+ return ca
218+
219+ @classmethod
220+ def get_service_cert(cls, type=STD_CERT):
221+ service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
222+ ca = cls.get_ca()
223+ crt, key = ca.get_or_create_cert(service_name)
224+ return crt, key, ca.get_ca_bundle()
225+
226+ ###############
227+
228+ def init(self):
229+ log.debug("initializing service ca")
230+ if not exists(self.ca_dir):
231+ self._init_ca_dir(self.ca_dir)
232+ self._init_ca()
233+
234+ @property
235+ def ca_key(self):
236+ return path_join(self.ca_dir, 'private', 'cacert.key')
237+
238+ @property
239+ def ca_cert(self):
240+ return path_join(self.ca_dir, 'cacert.pem')
241+
242+ @property
243+ def ca_conf(self):
244+ return path_join(self.ca_dir, 'ca.cnf')
245+
246+ @property
247+ def signing_conf(self):
248+ return path_join(self.ca_dir, 'signing.cnf')
249+
250+ def _init_ca_dir(self, ca_dir):
251+ os.mkdir(ca_dir)
252+ for i in ['certs', 'crl', 'newcerts', 'private']:
253+ sd = path_join(ca_dir, i)
254+ if not exists(sd):
255+ os.mkdir(sd)
256+
257+ if not exists(path_join(ca_dir, 'serial')):
258+ with open(path_join(ca_dir, 'serial'), 'wb') as fh:
259+ fh.write('02\n')
260+
261+ if not exists(path_join(ca_dir, 'index.txt')):
262+ with open(path_join(ca_dir, 'index.txt'), 'wb') as fh:
263+ fh.write('')
264+
265+ def _init_ca(self):
266+ """Generate the root ca's cert and key.
267+ """
268+ if not exists(path_join(self.ca_dir, 'ca.cnf')):
269+ with open(path_join(self.ca_dir, 'ca.cnf'), 'wb') as fh:
270+ fh.write(
271+ CA_CONF_TEMPLATE % (self.get_conf_variables()))
272+
273+ if not exists(path_join(self.ca_dir, 'signing.cnf')):
274+ with open(path_join(self.ca_dir, 'signing.cnf'), 'wb') as fh:
275+ fh.write(
276+ SIGNING_CONF_TEMPLATE % (self.get_conf_variables()))
277+
278+ if exists(self.ca_cert) or exists(self.ca_key):
279+ raise RuntimeError("Initialized called when CA already exists")
280+ cmd = ['openssl', 'req', '-config', self.ca_conf,
281+ '-x509', '-nodes', '-newkey', 'rsa',
282+ '-days', self.default_ca_expiry,
283+ '-keyout', self.ca_key, '-out', self.ca_cert,
284+ '-outform', 'PEM']
285+ output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
286+ log.debug("CA Init:\n %s", output)
287+
288+ def get_conf_variables(self):
289+ return dict(
290+ org_name="juju",
291+ org_unit_name="%s service" % self.name,
292+ common_name=self.name,
293+ ca_dir=self.ca_dir)
294+
295+ def get_or_create_cert(self, common_name):
296+ if common_name in self:
297+ return self.get_certificate(common_name)
298+ return self.create_certificate(common_name)
299+
300+ def create_certificate(self, common_name):
301+ if common_name in self:
302+ return self.get_certificate(common_name)
303+ key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)
304+ crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
305+ csr_p = path_join(self.ca_dir, "certs", "%s.csr" % common_name)
306+ self._create_certificate(common_name, key_p, csr_p, crt_p)
307+ return self.get_certificate(common_name)
308+
309+ def get_certificate(self, common_name):
310+ if not common_name in self:
311+ raise ValueError("No certificate for %s" % common_name)
312+ key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)
313+ crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
314+ with open(crt_p) as fh:
315+ crt = fh.read()
316+ with open(key_p) as fh:
317+ key = fh.read()
318+ return crt, key
319+
320+ def __contains__(self, common_name):
321+ crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
322+ return exists(crt_p)
323+
324+ def _create_certificate(self, common_name, key_p, csr_p, crt_p):
325+ template_vars = self.get_conf_variables()
326+ template_vars['common_name'] = common_name
327+ subj = '/O=%(org_name)s/OU=%(org_unit_name)s/CN=%(common_name)s' % (
328+ template_vars)
329+
330+ log.debug("CA Create Cert %s", common_name)
331+ cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa:2048',
332+ '-nodes', '-days', self.default_expiry,
333+ '-keyout', key_p, '-out', csr_p, '-subj', subj]
334+ subprocess.check_call(cmd)
335+ cmd = ['openssl', 'rsa', '-in', key_p, '-out', key_p]
336+ subprocess.check_call(cmd)
337+
338+ log.debug("CA Sign Cert %s", common_name)
339+ if self.cert_type == MYSQL_CERT:
340+ cmd = ['openssl', 'x509', '-req',
341+ '-in', csr_p, '-days', self.default_expiry,
342+ '-CA', self.ca_cert, '-CAkey', self.ca_key,
343+ '-set_serial', '01', '-out', crt_p]
344+ else:
345+ cmd = ['openssl', 'ca', '-config', self.signing_conf,
346+ '-extensions', 'req_extensions',
347+ '-days', self.default_expiry, '-notext',
348+ '-in', csr_p, '-out', crt_p, '-subj', subj, '-batch']
349+ log.debug("running %s", " ".join(cmd))
350+ subprocess.check_call(cmd)
351+
352+ def get_ca_bundle(self):
353+ with open(self.ca_cert) as fh:
354+ return fh.read()
355+
356+
357+CA_CONF_TEMPLATE = """
358+[ ca ]
359+default_ca = CA_default
360+
361+[ CA_default ]
362+dir = %(ca_dir)s
363+policy = policy_match
364+database = $dir/index.txt
365+serial = $dir/serial
366+certs = $dir/certs
367+crl_dir = $dir/crl
368+new_certs_dir = $dir/newcerts
369+certificate = $dir/cacert.pem
370+private_key = $dir/private/cacert.key
371+RANDFILE = $dir/private/.rand
372+default_md = default
373+
374+[ req ]
375+default_bits = 1024
376+default_md = sha1
377+
378+prompt = no
379+distinguished_name = ca_distinguished_name
380+
381+x509_extensions = ca_extensions
382+
383+[ ca_distinguished_name ]
384+organizationName = %(org_name)s
385+organizationalUnitName = %(org_unit_name)s Certificate Authority
386+
387+
388+[ policy_match ]
389+countryName = optional
390+stateOrProvinceName = optional
391+organizationName = match
392+organizationalUnitName = optional
393+commonName = supplied
394+
395+[ ca_extensions ]
396+basicConstraints = critical,CA:true
397+subjectKeyIdentifier = hash
398+authorityKeyIdentifier = keyid:always, issuer
399+keyUsage = cRLSign, keyCertSign
400+"""
401+
402+
403+SIGNING_CONF_TEMPLATE = """
404+[ ca ]
405+default_ca = CA_default
406+
407+[ CA_default ]
408+dir = %(ca_dir)s
409+policy = policy_match
410+database = $dir/index.txt
411+serial = $dir/serial
412+certs = $dir/certs
413+crl_dir = $dir/crl
414+new_certs_dir = $dir/newcerts
415+certificate = $dir/cacert.pem
416+private_key = $dir/private/cacert.key
417+RANDFILE = $dir/private/.rand
418+default_md = default
419+
420+[ req ]
421+default_bits = 1024
422+default_md = sha1
423+
424+prompt = no
425+distinguished_name = req_distinguished_name
426+
427+x509_extensions = req_extensions
428+
429+[ req_distinguished_name ]
430+organizationName = %(org_name)s
431+organizationalUnitName = %(org_unit_name)s machine resources
432+commonName = %(common_name)s
433+
434+[ policy_match ]
435+countryName = optional
436+stateOrProvinceName = optional
437+organizationName = match
438+organizationalUnitName = optional
439+commonName = supplied
440+
441+[ req_extensions ]
442+basicConstraints = CA:false
443+subjectKeyIdentifier = hash
444+authorityKeyIdentifier = keyid:always, issuer
445+keyUsage = digitalSignature, keyEncipherment, keyAgreement
446+extendedKeyUsage = serverAuth, clientAuth
447+"""
448
449=== added directory 'hooks/charmhelpers/core'
450=== added file 'hooks/charmhelpers/core/__init__.py'
451=== added file 'hooks/charmhelpers/core/hookenv.py'
452--- hooks/charmhelpers/core/hookenv.py 1970-01-01 00:00:00 +0000
453+++ hooks/charmhelpers/core/hookenv.py 2014-03-05 02:21:45 +0000
454@@ -0,0 +1,401 @@
455+"Interactions with the Juju environment"
456+# Copyright 2013 Canonical Ltd.
457+#
458+# Authors:
459+# Charm Helpers Developers <juju@lists.ubuntu.com>
460+
461+import os
462+import json
463+import yaml
464+import subprocess
465+import sys
466+import UserDict
467+from subprocess import CalledProcessError
468+
469+CRITICAL = "CRITICAL"
470+ERROR = "ERROR"
471+WARNING = "WARNING"
472+INFO = "INFO"
473+DEBUG = "DEBUG"
474+MARKER = object()
475+
476+cache = {}
477+
478+
479+def cached(func):
480+ """Cache return values for multiple executions of func + args
481+
482+ For example:
483+
484+ @cached
485+ def unit_get(attribute):
486+ pass
487+
488+ unit_get('test')
489+
490+ will cache the result of unit_get + 'test' for future calls.
491+ """
492+ def wrapper(*args, **kwargs):
493+ global cache
494+ key = str((func, args, kwargs))
495+ try:
496+ return cache[key]
497+ except KeyError:
498+ res = func(*args, **kwargs)
499+ cache[key] = res
500+ return res
501+ return wrapper
502+
503+
504+def flush(key):
505+ """Flushes any entries from function cache where the
506+ key is found in the function+args """
507+ flush_list = []
508+ for item in cache:
509+ if key in item:
510+ flush_list.append(item)
511+ for item in flush_list:
512+ del cache[item]
513+
514+
515+def log(message, level=None):
516+ """Write a message to the juju log"""
517+ command = ['juju-log']
518+ if level:
519+ command += ['-l', level]
520+ command += [message]
521+ subprocess.call(command)
522+
523+
524+class Serializable(UserDict.IterableUserDict):
525+ """Wrapper, an object that can be serialized to yaml or json"""
526+
527+ def __init__(self, obj):
528+ # wrap the object
529+ UserDict.IterableUserDict.__init__(self)
530+ self.data = obj
531+
532+ def __getattr__(self, attr):
533+ # See if this object has attribute.
534+ if attr in ("json", "yaml", "data"):
535+ return self.__dict__[attr]
536+ # Check for attribute in wrapped object.
537+ got = getattr(self.data, attr, MARKER)
538+ if got is not MARKER:
539+ return got
540+ # Proxy to the wrapped object via dict interface.
541+ try:
542+ return self.data[attr]
543+ except KeyError:
544+ raise AttributeError(attr)
545+
546+ def __getstate__(self):
547+ # Pickle as a standard dictionary.
548+ return self.data
549+
550+ def __setstate__(self, state):
551+ # Unpickle into our wrapper.
552+ self.data = state
553+
554+ def json(self):
555+ """Serialize the object to json"""
556+ return json.dumps(self.data)
557+
558+ def yaml(self):
559+ """Serialize the object to yaml"""
560+ return yaml.dump(self.data)
561+
562+
563+def execution_environment():
564+ """A convenient bundling of the current execution context"""
565+ context = {}
566+ context['conf'] = config()
567+ if relation_id():
568+ context['reltype'] = relation_type()
569+ context['relid'] = relation_id()
570+ context['rel'] = relation_get()
571+ context['unit'] = local_unit()
572+ context['rels'] = relations()
573+ context['env'] = os.environ
574+ return context
575+
576+
577+def in_relation_hook():
578+ """Determine whether we're running in a relation hook"""
579+ return 'JUJU_RELATION' in os.environ
580+
581+
582+def relation_type():
583+ """The scope for the current relation hook"""
584+ return os.environ.get('JUJU_RELATION', None)
585+
586+
587+def relation_id():
588+ """The relation ID for the current relation hook"""
589+ return os.environ.get('JUJU_RELATION_ID', None)
590+
591+
592+def local_unit():
593+ """Local unit ID"""
594+ return os.environ['JUJU_UNIT_NAME']
595+
596+
597+def remote_unit():
598+ """The remote unit for the current relation hook"""
599+ return os.environ['JUJU_REMOTE_UNIT']
600+
601+
602+def service_name():
603+ """The name service group this unit belongs to"""
604+ return local_unit().split('/')[0]
605+
606+
607+def hook_name():
608+ """The name of the currently executing hook"""
609+ return os.path.basename(sys.argv[0])
610+
611+
612+@cached
613+def config(scope=None):
614+ """Juju charm configuration"""
615+ config_cmd_line = ['config-get']
616+ if scope is not None:
617+ config_cmd_line.append(scope)
618+ config_cmd_line.append('--format=json')
619+ try:
620+ return json.loads(subprocess.check_output(config_cmd_line))
621+ except ValueError:
622+ return None
623+
624+
625+@cached
626+def relation_get(attribute=None, unit=None, rid=None):
627+ """Get relation information"""
628+ _args = ['relation-get', '--format=json']
629+ if rid:
630+ _args.append('-r')
631+ _args.append(rid)
632+ _args.append(attribute or '-')
633+ if unit:
634+ _args.append(unit)
635+ try:
636+ return json.loads(subprocess.check_output(_args))
637+ except ValueError:
638+ return None
639+ except CalledProcessError, e:
640+ if e.returncode == 2:
641+ return None
642+ raise
643+
644+
645+def relation_set(relation_id=None, relation_settings={}, **kwargs):
646+ """Set relation information for the current unit"""
647+ relation_cmd_line = ['relation-set']
648+ if relation_id is not None:
649+ relation_cmd_line.extend(('-r', relation_id))
650+ for k, v in (relation_settings.items() + kwargs.items()):
651+ if v is None:
652+ relation_cmd_line.append('{}='.format(k))
653+ else:
654+ relation_cmd_line.append('{}={}'.format(k, v))
655+ subprocess.check_call(relation_cmd_line)
656+ # Flush cache of any relation-gets for local unit
657+ flush(local_unit())
658+
659+
660+@cached
661+def relation_ids(reltype=None):
662+ """A list of relation_ids"""
663+ reltype = reltype or relation_type()
664+ relid_cmd_line = ['relation-ids', '--format=json']
665+ if reltype is not None:
666+ relid_cmd_line.append(reltype)
667+ return json.loads(subprocess.check_output(relid_cmd_line)) or []
668+ return []
669+
670+
671+@cached
672+def related_units(relid=None):
673+ """A list of related units"""
674+ relid = relid or relation_id()
675+ units_cmd_line = ['relation-list', '--format=json']
676+ if relid is not None:
677+ units_cmd_line.extend(('-r', relid))
678+ return json.loads(subprocess.check_output(units_cmd_line)) or []
679+
680+
681+@cached
682+def relation_for_unit(unit=None, rid=None):
683+ """Get the json represenation of a unit's relation"""
684+ unit = unit or remote_unit()
685+ relation = relation_get(unit=unit, rid=rid)
686+ for key in relation:
687+ if key.endswith('-list'):
688+ relation[key] = relation[key].split()
689+ relation['__unit__'] = unit
690+ return relation
691+
692+
693+@cached
694+def relations_for_id(relid=None):
695+ """Get relations of a specific relation ID"""
696+ relation_data = []
697+ relid = relid or relation_ids()
698+ for unit in related_units(relid):
699+ unit_data = relation_for_unit(unit, relid)
700+ unit_data['__relid__'] = relid
701+ relation_data.append(unit_data)
702+ return relation_data
703+
704+
705+@cached
706+def relations_of_type(reltype=None):
707+ """Get relations of a specific type"""
708+ relation_data = []
709+ reltype = reltype or relation_type()
710+ for relid in relation_ids(reltype):
711+ for relation in relations_for_id(relid):
712+ relation['__relid__'] = relid
713+ relation_data.append(relation)
714+ return relation_data
715+
716+
717+@cached
718+def relation_types():
719+ """Get a list of relation types supported by this charm"""
720+ charmdir = os.environ.get('CHARM_DIR', '')
721+ mdf = open(os.path.join(charmdir, 'metadata.yaml'))
722+ md = yaml.safe_load(mdf)
723+ rel_types = []
724+ for key in ('provides', 'requires', 'peers'):
725+ section = md.get(key)
726+ if section:
727+ rel_types.extend(section.keys())
728+ mdf.close()
729+ return rel_types
730+
731+
732+@cached
733+def relations():
734+ """Get a nested dictionary of relation data for all related units"""
735+ rels = {}
736+ for reltype in relation_types():
737+ relids = {}
738+ for relid in relation_ids(reltype):
739+ units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
740+ for unit in related_units(relid):
741+ reldata = relation_get(unit=unit, rid=relid)
742+ units[unit] = reldata
743+ relids[relid] = units
744+ rels[reltype] = relids
745+ return rels
746+
747+
748+@cached
749+def is_relation_made(relation, keys='private-address'):
750+ '''
751+ Determine whether a relation is established by checking for
752+ presence of key(s). If a list of keys is provided, they
753+ must all be present for the relation to be identified as made
754+ '''
755+ if isinstance(keys, str):
756+ keys = [keys]
757+ for r_id in relation_ids(relation):
758+ for unit in related_units(r_id):
759+ context = {}
760+ for k in keys:
761+ context[k] = relation_get(k, rid=r_id,
762+ unit=unit)
763+ if None not in context.values():
764+ return True
765+ return False
766+
767+
768+def open_port(port, protocol="TCP"):
769+ """Open a service network port"""
770+ _args = ['open-port']
771+ _args.append('{}/{}'.format(port, protocol))
772+ subprocess.check_call(_args)
773+
774+
775+def close_port(port, protocol="TCP"):
776+ """Close a service network port"""
777+ _args = ['close-port']
778+ _args.append('{}/{}'.format(port, protocol))
779+ subprocess.check_call(_args)
780+
781+
782+@cached
783+def unit_get(attribute):
784+ """Get the unit ID for the remote unit"""
785+ _args = ['unit-get', '--format=json', attribute]
786+ try:
787+ return json.loads(subprocess.check_output(_args))
788+ except ValueError:
789+ return None
790+
791+
792+def unit_private_ip():
793+ """Get this unit's private IP address"""
794+ return unit_get('private-address')
795+
796+
797+class UnregisteredHookError(Exception):
798+ """Raised when an undefined hook is called"""
799+ pass
800+
801+
802+class Hooks(object):
803+ """A convenient handler for hook functions.
804+
805+ Example:
806+ hooks = Hooks()
807+
808+ # register a hook, taking its name from the function name
809+ @hooks.hook()
810+ def install():
811+ ...
812+
813+ # register a hook, providing a custom hook name
814+ @hooks.hook("config-changed")
815+ def config_changed():
816+ ...
817+
818+ if __name__ == "__main__":
819+ # execute a hook based on the name the program is called by
820+ hooks.execute(sys.argv)
821+ """
822+
823+ def __init__(self):
824+ super(Hooks, self).__init__()
825+ self._hooks = {}
826+
827+ def register(self, name, function):
828+ """Register a hook"""
829+ self._hooks[name] = function
830+
831+ def execute(self, args):
832+ """Execute a registered hook based on args[0]"""
833+ hook_name = os.path.basename(args[0])
834+ if hook_name in self._hooks:
835+ self._hooks[hook_name]()
836+ else:
837+ raise UnregisteredHookError(hook_name)
838+
839+ def hook(self, *hook_names):
840+ """Decorator, registering them as hooks"""
841+ def wrapper(decorated):
842+ for hook_name in hook_names:
843+ self.register(hook_name, decorated)
844+ else:
845+ self.register(decorated.__name__, decorated)
846+ if '_' in decorated.__name__:
847+ self.register(
848+ decorated.__name__.replace('_', '-'), decorated)
849+ return decorated
850+ return wrapper
851+
852+
853+def charm_dir():
854+ """Return the root directory of the current charm"""
855+ return os.environ.get('CHARM_DIR')
856
857=== added file 'hooks/charmhelpers/core/host.py'
858--- hooks/charmhelpers/core/host.py 1970-01-01 00:00:00 +0000
859+++ hooks/charmhelpers/core/host.py 2014-03-05 02:21:45 +0000
860@@ -0,0 +1,297 @@
861+"""Tools for working with the host system"""
862+# Copyright 2012 Canonical Ltd.
863+#
864+# Authors:
865+# Nick Moffitt <nick.moffitt@canonical.com>
866+# Matthew Wedgwood <matthew.wedgwood@canonical.com>
867+
868+import os
869+import pwd
870+import grp
871+import random
872+import string
873+import subprocess
874+import hashlib
875+
876+from collections import OrderedDict
877+
878+from hookenv import log
879+
880+
881+def service_start(service_name):
882+ """Start a system service"""
883+ return service('start', service_name)
884+
885+
886+def service_stop(service_name):
887+ """Stop a system service"""
888+ return service('stop', service_name)
889+
890+
891+def service_restart(service_name):
892+ """Restart a system service"""
893+ return service('restart', service_name)
894+
895+
896+def service_reload(service_name, restart_on_failure=False):
897+ """Reload a system service, optionally falling back to restart if reload fails"""
898+ service_result = service('reload', service_name)
899+ if not service_result and restart_on_failure:
900+ service_result = service('restart', service_name)
901+ return service_result
902+
903+
904+def service(action, service_name):
905+ """Control a system service"""
906+ cmd = ['service', service_name, action]
907+ return subprocess.call(cmd) == 0
908+
909+
910+def service_running(service):
911+ """Determine whether a system service is running"""
912+ try:
913+ output = subprocess.check_output(['service', service, 'status'])
914+ except subprocess.CalledProcessError:
915+ return False
916+ else:
917+ if ("start/running" in output or "is running" in output):
918+ return True
919+ else:
920+ return False
921+
922+
923+def adduser(username, password=None, shell='/bin/bash', system_user=False):
924+ """Add a user to the system"""
925+ try:
926+ user_info = pwd.getpwnam(username)
927+ log('user {0} already exists!'.format(username))
928+ except KeyError:
929+ log('creating user {0}'.format(username))
930+ cmd = ['useradd']
931+ if system_user or password is None:
932+ cmd.append('--system')
933+ else:
934+ cmd.extend([
935+ '--create-home',
936+ '--shell', shell,
937+ '--password', password,
938+ ])
939+ cmd.append(username)
940+ subprocess.check_call(cmd)
941+ user_info = pwd.getpwnam(username)
942+ return user_info
943+
944+
945+def add_user_to_group(username, group):
946+ """Add a user to a group"""
947+ cmd = [
948+ 'gpasswd', '-a',
949+ username,
950+ group
951+ ]
952+ log("Adding user {} to group {}".format(username, group))
953+ subprocess.check_call(cmd)
954+
955+
956+def rsync(from_path, to_path, flags='-r', options=None):
957+ """Replicate the contents of a path"""
958+ options = options or ['--delete', '--executability']
959+ cmd = ['/usr/bin/rsync', flags]
960+ cmd.extend(options)
961+ cmd.append(from_path)
962+ cmd.append(to_path)
963+ log(" ".join(cmd))
964+ return subprocess.check_output(cmd).strip()
965+
966+
967+def symlink(source, destination):
968+ """Create a symbolic link"""
969+ log("Symlinking {} as {}".format(source, destination))
970+ cmd = [
971+ 'ln',
972+ '-sf',
973+ source,
974+ destination,
975+ ]
976+ subprocess.check_call(cmd)
977+
978+
979+def mkdir(path, owner='root', group='root', perms=0555, force=False):
980+ """Create a directory"""
981+ log("Making dir {} {}:{} {:o}".format(path, owner, group,
982+ perms))
983+ uid = pwd.getpwnam(owner).pw_uid
984+ gid = grp.getgrnam(group).gr_gid
985+ realpath = os.path.abspath(path)
986+ if os.path.exists(realpath):
987+ if force and not os.path.isdir(realpath):
988+ log("Removing non-directory file {} prior to mkdir()".format(path))
989+ os.unlink(realpath)
990+ else:
991+ os.makedirs(realpath, perms)
992+ os.chown(realpath, uid, gid)
993+
994+
995+def write_file(path, content, owner='root', group='root', perms=0444):
996+ """Create or overwrite a file with the contents of a string"""
997+ log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
998+ uid = pwd.getpwnam(owner).pw_uid
999+ gid = grp.getgrnam(group).gr_gid
1000+ with open(path, 'w') as target:
1001+ os.fchown(target.fileno(), uid, gid)
1002+ os.fchmod(target.fileno(), perms)
1003+ target.write(content)
1004+
1005+
1006+def mount(device, mountpoint, options=None, persist=False):
1007+ """Mount a filesystem at a particular mountpoint"""
1008+ cmd_args = ['mount']
1009+ if options is not None:
1010+ cmd_args.extend(['-o', options])
1011+ cmd_args.extend([device, mountpoint])
1012+ try:
1013+ subprocess.check_output(cmd_args)
1014+ except subprocess.CalledProcessError, e:
1015+ log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
1016+ return False
1017+ if persist:
1018+ # TODO: update fstab
1019+ pass
1020+ return True
1021+
1022+
1023+def umount(mountpoint, persist=False):
1024+ """Unmount a filesystem"""
1025+ cmd_args = ['umount', mountpoint]
1026+ try:
1027+ subprocess.check_output(cmd_args)
1028+ except subprocess.CalledProcessError, e:
1029+ log('Error unmounting {}\n{}'.format(mountpoint, e.output))
1030+ return False
1031+ if persist:
1032+ # TODO: update fstab
1033+ pass
1034+ return True
1035+
1036+
1037+def mounts():
1038+ """Get a list of all mounted volumes as [[mountpoint,device],[...]]"""
1039+ with open('/proc/mounts') as f:
1040+ # [['/mount/point','/dev/path'],[...]]
1041+ system_mounts = [m[1::-1] for m in [l.strip().split()
1042+ for l in f.readlines()]]
1043+ return system_mounts
1044+
1045+
1046+def file_hash(path):
1047+ """Generate a md5 hash of the contents of 'path' or None if not found """
1048+ if os.path.exists(path):
1049+ h = hashlib.md5()
1050+ with open(path, 'r') as source:
1051+ h.update(source.read()) # IGNORE:E1101 - it does have update
1052+ return h.hexdigest()
1053+ else:
1054+ return None
1055+
1056+
1057+def restart_on_change(restart_map, stopstart=False):
1058+ """Restart services based on configuration files changing
1059+
1060+ This function is used a decorator, for example
1061+
1062+ @restart_on_change({
1063+ '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
1064+ })
1065+ def ceph_client_changed():
1066+ ...
1067+
1068+ In this example, the cinder-api and cinder-volume services
1069+ would be restarted if /etc/ceph/ceph.conf is changed by the
1070+ ceph_client_changed function.
1071+ """
1072+ def wrap(f):
1073+ def wrapped_f(*args):
1074+ checksums = {}
1075+ for path in restart_map:
1076+ checksums[path] = file_hash(path)
1077+ f(*args)
1078+ restarts = []
1079+ for path in restart_map:
1080+ if checksums[path] != file_hash(path):
1081+ restarts += restart_map[path]
1082+ services_list = list(OrderedDict.fromkeys(restarts))
1083+ if not stopstart:
1084+ for service_name in services_list:
1085+ service('restart', service_name)
1086+ else:
1087+ for action in ['stop', 'start']:
1088+ for service_name in services_list:
1089+ service(action, service_name)
1090+ return wrapped_f
1091+ return wrap
1092+
1093+
1094+def lsb_release():
1095+ """Return /etc/lsb-release in a dict"""
1096+ d = {}
1097+ with open('/etc/lsb-release', 'r') as lsb:
1098+ for l in lsb:
1099+ k, v = l.split('=')
1100+ d[k.strip()] = v.strip()
1101+ return d
1102+
1103+
1104+def pwgen(length=None):
1105+ """Generate a random pasword."""
1106+ if length is None:
1107+ length = random.choice(range(35, 45))
1108+ alphanumeric_chars = [
1109+ l for l in (string.letters + string.digits)
1110+ if l not in 'l0QD1vAEIOUaeiou']
1111+ random_chars = [
1112+ random.choice(alphanumeric_chars) for _ in range(length)]
1113+ return(''.join(random_chars))
1114+
1115+
1116+def list_nics(nic_type):
1117+ '''Return a list of nics of given type(s)'''
1118+ if isinstance(nic_type, basestring):
1119+ int_types = [nic_type]
1120+ else:
1121+ int_types = nic_type
1122+ interfaces = []
1123+ for int_type in int_types:
1124+ cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
1125+ ip_output = subprocess.check_output(cmd).split('\n')
1126+ ip_output = (line for line in ip_output if line)
1127+ for line in ip_output:
1128+ if line.split()[1].startswith(int_type):
1129+ interfaces.append(line.split()[1].replace(":", ""))
1130+ return interfaces
1131+
1132+
1133+def set_nic_mtu(nic, mtu):
1134+ '''Set MTU on a network interface'''
1135+ cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
1136+ subprocess.check_call(cmd)
1137+
1138+
1139+def get_nic_mtu(nic):
1140+ cmd = ['ip', 'addr', 'show', nic]
1141+ ip_output = subprocess.check_output(cmd).split('\n')
1142+ mtu = ""
1143+ for line in ip_output:
1144+ words = line.split()
1145+ if 'mtu' in words:
1146+ mtu = words[words.index("mtu") + 1]
1147+ return mtu
1148+
1149+
1150+def get_nic_hwaddr(nic):
1151+ cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
1152+ ip_output = subprocess.check_output(cmd)
1153+ hwaddr = ""
1154+ words = ip_output.split()
1155+ if 'link/ether' in words:
1156+ hwaddr = words[words.index('link/ether') + 1]
1157+ return hwaddr
1158
1159=== modified file 'hooks/common.py'
1160--- hooks/common.py 2013-02-12 17:54:29 +0000
1161+++ hooks/common.py 2014-03-05 02:21:45 +0000
1162@@ -1,15 +1,228 @@
1163 # vim: syntax=python
1164
1165+import contextlib
1166+import base64
1167+import itertools
1168+# import json
1169 import os
1170-import sys
1171 import MySQLdb
1172+import _mysql
1173+import socket
1174 import subprocess
1175-import uuid
1176+import sys
1177+
1178+from charmhelpers.core.hookenv import (
1179+ config, relation_get, relation_set, relation_ids, related_units,
1180+ unit_get, log)
1181+
1182+from charmhelpers.contrib.ssl.service import ServiceCA, MYSQL_CERT
1183+
1184
1185 def get_service_user_file(service):
1186 return '/var/lib/mysql/%s.service_user2' % service
1187
1188
1189+def get_service_ca():
1190+ service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
1191+ ca_path = os.path.join(os.environ['CHARM_DIR'], 'ca')
1192+ ca = ServiceCA(service_name, ca_path, MYSQL_CERT)
1193+ ca.init()
1194+ return ca
1195+
1196+
1197+def is_ext_ssl_ca():
1198+ ssl_key = configs.get('ssl_key')
1199+ ssl_cert = configs.get('ssl_cert')
1200+ return all((ssl_key, ssl_cert))
1201+
1202+
1203+def _convert_from_base64(v):
1204+ # Play nice when user hands pem encoded value for cert/key/ca
1205+ # as they are base64 compliant strings.
1206+ if not v:
1207+ return v
1208+ if v.startswith('-----BEGIN'):
1209+ return v
1210+ try:
1211+ return base64.b64decode(v)
1212+ except TypeError:
1213+ return v
1214+
1215+
1216+def configure_service_ssl(configs):
1217+ if not configs.get('ssl', 'off').strip() in ('on', 'only'):
1218+ configs['ssl_cert_marker'] = configs['ssl_ca_marker'] = "#"
1219+ if not slave:
1220+ reconfigure_client_ssl(False)
1221+ return
1222+
1223+ service_name = os.environ.get('JUJU_UNIT_NAME').split('/')[0]
1224+ ssl_cert, ssl_key, ssl_ca = (
1225+ configs.get('ssl_cert'), configs.get('ssl_key'), configs.get('ssl_ca'))
1226+
1227+ if any((ssl_cert, ssl_key, ssl_ca)):
1228+ if not all((ssl_cert, ssl_key)):
1229+ log("ERROR",
1230+ "SSL Key and cert are both required if either specified")
1231+ sys.exit(1)
1232+ ssl_cert = _convert_from_base64(ssl_cert)
1233+ ssl_key = _convert_from_base64(ssl_key)
1234+ if ssl_ca:
1235+ ssl_ca = _convert_from_base64(ssl_ca)
1236+ else:
1237+ ca = get_service_ca()
1238+ ssl_cert, ssl_key = ca.create_certificate(service_name)
1239+ ssl_ca = ca.get_ca_bundle()
1240+
1241+ # Config variables for template
1242+ configs['ssl_cert_marker'] = ''
1243+
1244+ if ssl_ca: # In case a user providers cert but no ca cause its official.
1245+ configs['ssl_ca_marker'] = ''
1246+ with open('/etc/mysql/cacert.pem', 'w') as fh:
1247+ fh.write(ssl_ca)
1248+ with open('/etc/mysql/server-cert.pem', 'w') as fh:
1249+ fh.write(ssl_cert)
1250+ with open('/etc/mysql/server-key.pem', 'w') as fh:
1251+ fh.write(ssl_key)
1252+
1253+ if not slave:
1254+ ssl_only = configs.get('ssl', 'off') == 'only'
1255+ reconfigure_client_ssl(True, ssl_only)
1256+
1257+
1258+def normalize_db_requests(data):
1259+ single_request = set(('username', 'database', 'hostname'))
1260+ if single_request.issubset(data):
1261+ return [data]
1262+
1263+ requests = {}
1264+ for k, v in data.items():
1265+ prefix = k.split('_')[0]
1266+ x = '_'.join(k.split('_')[1:])
1267+ req = requests.setdefault(prefix, {})
1268+ req[x] = v
1269+ req['prefix'] = prefix
1270+ return requests.values()
1271+
1272+
1273+def reconfigure_client_ssl(ssl_enabled, ssl_only=False):
1274+ log("Checking on reconfiguration for ssl clients")
1275+ for rel_type, rid in itertools.chain(
1276+ itertools.izip(
1277+ itertools.repeat('db'), relation_ids('db')),
1278+ itertools.izip(
1279+ itertools.repeat('db-admin'), relation_ids('db-admin')),
1280+ itertools.izip(
1281+ itertools.repeat('shared-db'), relation_ids('shared-db'))):
1282+
1283+ rdata = relation_get(rid=rid, unit=os.environ['JUJU_UNIT_NAME'])
1284+
1285+ # If the relation is already configured for the current settings
1286+ # move on to next.
1287+ if (('ssl_ca' in rdata and ssl_enabled) or
1288+ (not 'ssl_ca' in rdata and not ssl_enabled)):
1289+ continue
1290+
1291+ log("Reconfiguring relation:%s for ssl:%s" % (rid, ssl_enabled))
1292+
1293+ # Update relation data with correct ssl info based on current config.
1294+ if ('ssl_ca' in rdata and not ssl_enabled):
1295+ # Blank any existing ssl config values.
1296+ relation_set(relation_id=rid, ssl_ca="", ssl_cert="", ssl_key="")
1297+ elif is_ext_ssl_ca():
1298+ relation_set(relation_id=rid, ssl_ca=configs.get('ssl_ca', ''))
1299+ else:
1300+ # Setup ssl
1301+ units = related_units(rid)
1302+ if not units:
1303+ # Relation hasn't been initialized yet, skip
1304+ continue
1305+ ssl_data = configure_client_ssl(remote_unit=units[0])
1306+ relation_set(relation_id=rid, **ssl_data)
1307+
1308+ # Update grants based on relation type if we're switching to/from only.
1309+ if rel_type == "shared-db":
1310+ # Update grant for related units
1311+ for u in related_units(rid):
1312+ u_rdata = relation_get(rid=rid, unit=u)
1313+
1314+ # Check if we haven't processed this the relation and
1315+ # its queued for the future.
1316+ if not rdata.get('db_host'):
1317+ continue
1318+
1319+ for req in normalize_db_requests(u_rdata):
1320+ if not all((req.get('username'),
1321+ req.get('database'), req.get('hostname'))):
1322+ # Relation not complete
1323+ continue
1324+
1325+ remote_ip = get_remote_ip(req.get('hostname'))
1326+ requires_ssl = requires_ssl_access(
1327+ req['username'], remote_ip)
1328+
1329+ # Do nothing if we have the right grant for our config
1330+ if ssl_only and requires_ssl:
1331+ continue
1332+ elif not ssl_only and not requires_ssl:
1333+ continue
1334+
1335+ # Else remove and re-grant
1336+ password = rdata.get('password')
1337+ if 'prefix' in req:
1338+ password = rdata['%s_password' % req['prefix']]
1339+
1340+ revoke_grant(
1341+ req['database'], req['username'], remote_ip)
1342+ create_grant(
1343+ req['database'], req['username'], remote_ip, password)
1344+
1345+ elif rel_type in ("db", "db-admin"):
1346+ units = related_units(rid)
1347+ svc = units[0].split("/", 1)[0]
1348+ svc_user, svc_password = get_service_user(svc)
1349+ requires_ssl = requires_ssl_access(svc_user)
1350+
1351+ if rel_type == "db-admin":
1352+ svc = "*"
1353+ # Do nothing if we have the right grant for our config
1354+ if ssl_only and requires_ssl:
1355+ continue
1356+ elif not ssl_only and not requires_ssl:
1357+ continue
1358+ privileges = "all privileges"
1359+ if slave:
1360+ privileges = "select"
1361+ revoke_grant(svc, svc_user)
1362+ create_grant(svc, svc_user, svc_password, privileges=privileges)
1363+
1364+
1365+def configure_client_ssl(remote_unit=None):
1366+ if configs.get('ssl', '') not in ('only', 'on'):
1367+ return dict()
1368+
1369+ if is_ext_ssl_ca():
1370+ if configs.get('ssl_ca'):
1371+ return dict(ssl_ca=configs['ssl_ca'])
1372+ return
1373+
1374+ # self-managed-client-certs
1375+ if remote_unit is None:
1376+ remote_unit = change_unit
1377+
1378+ ca = get_service_ca()
1379+ ca_bundle = ca.get_ca_bundle()
1380+
1381+ service_cert, service_key = ca.get_or_create_cert(
1382+ remote_unit.split('/', 1)[0])
1383+
1384+ return dict(
1385+ ssl_ca=base64.b64encode(ca_bundle),
1386+ ssl_key=base64.b64encode(service_key),
1387+ ssl_cert=base64.b64encode(service_cert))
1388+
1389+
1390 def get_service_user(service):
1391 if service == '':
1392 return (None, None)
1393@@ -17,11 +230,13 @@
1394 if os.path.exists(sfile):
1395 with open(sfile, 'r') as f:
1396 return (f.readline().strip(), f.readline().strip())
1397- (suser, service_password) = subprocess.check_output(['pwgen', '-N 2', '15']).strip().split("\n")
1398+ (suser, service_password) = subprocess.check_output(
1399+ ['pwgen', '-N 2', '15']).strip().split("\n")
1400 with open(sfile, 'w') as f:
1401 f.write("%s\n" % suser)
1402 f.write("%s\n" % service_password)
1403 f.flush()
1404+ os.chmod(f.name, 0600)
1405 return (suser, service_password)
1406
1407
1408@@ -45,16 +260,20 @@
1409 with open(database_name_file, 'r') as dbname:
1410 database_name = dbname.readline().strip()
1411 else:
1412- print 'No established database and no REMOTE_UNIT.'
1413+ log('No established database and no REMOTE_UNIT.')
1414+
1415 # A user per service unit so we can deny access quickly
1416 user, service_password = get_service_user(database_name)
1417 connection = None
1418-lastrun_path = '/var/lib/juju/%s.%s.lastrun' % (database_name,user)
1419+lastrun_path = '/var/lib/juju/%s.%s.lastrun' % (database_name, user)
1420 slave_configured_path = '/var/lib/juju.slave.configured.for.%s' % database_name
1421 slave_configured = os.path.exists(slave_configured_path)
1422 slave = os.path.exists('/var/lib/juju/i.am.a.slave')
1423 broken_path = '/var/lib/juju/%s.mysql.broken' % database_name
1424 broken = os.path.exists(broken_path)
1425+local_hostname = unit_get('private-address')
1426+configs = config()
1427+
1428
1429 def get_db_cursor():
1430 # Connect to mysql
1431@@ -73,47 +292,112 @@
1432 return db_name in databases
1433
1434
1435-def create_database(db_name):
1436- cursor = get_db_cursor()
1437- try:
1438- cursor.execute("CREATE DATABASE {}".format(db_name))
1439- finally:
1440- cursor.close()
1441+def get_remote_ip(hostname):
1442+ if hostname != local_hostname:
1443+ remote_ip = socket.gethostbyname(hostname)
1444+ else:
1445+ remote_ip = '127.0.0.1'
1446+ return remote_ip
1447+
1448+
1449+def create_database(db_name, character_set="utf8"):
1450+ with contextlib.closing(get_db_cursor()) as cursor:
1451+ stmt = "create database {}"
1452+ if character_set:
1453+ stmt += " character set %s" % character_set
1454+ stmt = stmt.format(db_name)
1455+ log("Running SQL %s" % stmt)
1456+ cursor.execute(stmt)
1457
1458
1459 def grant_exists(db_name, db_user, remote_ip):
1460 cursor = get_db_cursor()
1461 try:
1462- cursor.execute("SHOW GRANTS for '{}'@'{}'".format(db_user,
1463- remote_ip))
1464+ stmt = "SHOW GRANTS for '{}'@'{}'".format(db_user, remote_ip)
1465+ print "Running SQL", stmt
1466+ cursor.execute(stmt)
1467 grants = [i[0] for i in cursor.fetchall()]
1468 except MySQLdb.OperationalError:
1469- print "No grants found"
1470- return False
1471- finally:
1472- cursor.close()
1473- return "GRANT ALL PRIVILEGES ON `{}`".format(db_name) in grants
1474-
1475-
1476-def create_grant(db_name, db_user,
1477- remote_ip, password):
1478- cursor = get_db_cursor()
1479- try:
1480- cursor.execute("GRANT ALL PRIVILEGES ON {}.* TO '{}'@'{}' "\
1481- "IDENTIFIED BY '{}'".format(db_name,
1482- db_user,
1483- remote_ip,
1484- password))
1485- finally:
1486- cursor.close()
1487-
1488-
1489-def cleanup_grant(db_user,
1490- remote_ip):
1491- cursor = get_db_cursor()
1492- try:
1493- cursor.execute("DROP FROM mysql.user WHERE user='{}' "\
1494- "AND HOST='{}'".format(db_user,
1495- remote_ip))
1496- finally:
1497- cursor.close()
1498+ log("No grants found")
1499+ return False
1500+ finally:
1501+ cursor.close()
1502+ found = "GRANT ALL PRIVILEGES ON `{}`".format(db_name) in grants
1503+ log('grants found %s %s' % (found, grants))
1504+ return found
1505+
1506+
1507+def requires_ssl_access(db_user, remote_ip=None):
1508+ with contextlib.closing(get_db_cursor()) as cursor:
1509+ stmt = "select User, ssl_type from mysql.user where User='%s'" % (
1510+ db_user)
1511+ if remote_ip:
1512+ stmt + " Host='%s'" % remote_ip
1513+ log("Running SQL %s" % stmt)
1514+ rows = cursor.execute(stmt)
1515+ if not rows:
1516+ log("ssl grant check -> no grants found for %s" % db_user)
1517+ return False
1518+ if rows > 1:
1519+ log("ssl grant check -> multiple grants found for %s and %s" % (
1520+ db_user, remote_ip))
1521+ result = cursor.fetchall()[0]
1522+ if result[1] == "ANY":
1523+ log("ssl grant check -> user %s requires ssl" % db_user)
1524+ return True
1525+ log("ssl grant check -> user %s does not need ssl" % db_user)
1526+ return False
1527+
1528+
1529+def create_grant(db_name, db_user, password, remote_ip=None,
1530+ privileges="all privileges"):
1531+ params = [db_name, db_user, password]
1532+ grant = "GRANT %s ON {}.* TO '{}'" % privileges
1533+
1534+ if remote_ip:
1535+ grant += " @'{}'"
1536+ params.append(remote_ip)
1537+
1538+ if password:
1539+ grant += " IDENTIFIED BY '{}'"
1540+
1541+ # http://dev.mysql.com/doc/refman/5.5/en/grant.html
1542+ if configs.get('ssl') == 'only' and password:
1543+ grant += ' REQUIRE SSL'
1544+
1545+ with contextlib.closing(get_db_cursor()) as cursor:
1546+ output_params = list(params)
1547+ output_params[-1] = "xxx"
1548+ log("Running SQL %s" % (grant.format(*output_params)))
1549+ cursor.execute(grant.format(*params))
1550+
1551+
1552+def revoke_grant(db_name, user, remote_ip=None, privileges="privileges"):
1553+ with contextlib.closing(get_db_cursor()) as cursor:
1554+ # Unclear how to reset the ssl attribute on a user without
1555+ # dropping them. minor mitigation, mysql won't drop existing
1556+ # connections for the user existing sessions are closed.
1557+ stmt = "revoke all %s on %s.* from '%s'" % (
1558+ privileges, db_name, user)
1559+ if remote_ip:
1560+ stmt += "@'%s'" % remote_ip
1561+ log("Running SQL %s" % stmt)
1562+ try:
1563+ cursor.execute(stmt)
1564+ except _mysql.OperationalError, e:
1565+ # http://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html
1566+ # Catch MySQL no such grant error.
1567+ if not e.args[0] == 1141:
1568+ raise
1569+
1570+ stmt = "drop user '%s'" % user
1571+ if remote_ip:
1572+ stmt += "@'%s'" % remote_ip
1573+ log("Running SQL %s" % stmt)
1574+
1575+ try:
1576+ cursor.execute(stmt)
1577+ except _mysql.OperationalError, e:
1578+ # Catch mysql cannot drop user (no such user)
1579+ if not e.args[0] in (1396, 1449):
1580+ raise
1581
1582=== modified file 'hooks/config-changed'
1583--- hooks/config-changed 2014-02-15 21:37:45 +0000
1584+++ hooks/config-changed 2014-03-05 02:21:45 +0000
1585@@ -1,5 +1,6 @@
1586 #!/usr/bin/python
1587
1588+from base64 import b64decode
1589 from subprocess import check_output,check_call, CalledProcessError, Popen, PIPE
1590 import tempfile
1591 import json
1592@@ -10,6 +11,8 @@
1593 import platform
1594 from string import upper
1595
1596+from common import configure_service_ssl, configs
1597+
1598 num_re = re.compile('^[0-9]+$')
1599
1600 # There should be a library for this
1601@@ -48,7 +51,6 @@
1602 if IS_32BIT_SYSTEM:
1603 check_call(['juju-log','-l','INFO','32bit system restrictions in play'])
1604
1605-configs=json.loads(check_output(['config-get','--format=json']))
1606
1607 def get_memtotal():
1608 with open('/proc/meminfo') as meminfo_file:
1609@@ -153,6 +155,10 @@
1610 else:
1611 configs['max-connections'] = 'max_connections = %s' % configs['max-connections']
1612
1613+
1614+configure_service_ssl(configs)
1615+
1616+
1617 template="""
1618 ######################################
1619 #
1620@@ -171,7 +177,7 @@
1621 # You can copy this to one of:
1622 # - "/etc/mysql/my.cnf" to set global options,
1623 # - "~/.my.cnf" to set user-specific options.
1624-#
1625+#
1626 # One can use all long options that the program supports.
1627 # Run program with --help to get a list of available options and with
1628 # --print-defaults to see which it would actually understand and use.
1629@@ -181,7 +187,7 @@
1630
1631 # This will be passed to all mysql clients
1632 # It has been reported that passwords should be enclosed with ticks/quotes
1633-# escpecially if they contain "#" chars...
1634+# escpecially if they contain '#' chars...
1635 # Remember to edit /etc/mysql/debian.cnf when changing the socket location.
1636 [client]
1637 port = 3306
1638@@ -277,9 +283,9 @@
1639 #
1640 # For generating SSL certificates I recommend the OpenSSL GUI "tinyca".
1641 #
1642-# ssl-ca=/etc/mysql/cacert.pem
1643-# ssl-cert=/etc/mysql/server-cert.pem
1644-# ssl-key=/etc/mysql/server-key.pem
1645+%(ssl_ca_marker)s ssl-ca=/etc/mysql/cacert.pem
1646+%(ssl_cert_marker)s ssl-cert=/etc/mysql/server-cert.pem
1647+%(ssl_cert_marker)s ssl-key=/etc/mysql/server-key.pem
1648
1649
1650
1651@@ -326,7 +332,7 @@
1652
1653 need_restart = False
1654 for target,content in targets.iteritems():
1655- tdir = os.path.dirname(target)
1656+ tdir = os.path.dirname(target)
1657 if len(content) == 0 and os.path.exists(target):
1658 os.unlink(target)
1659 need_restart = True
1660
1661=== modified file 'hooks/db-relation-joined'
1662--- hooks/db-relation-joined 2012-11-02 06:41:12 +0000
1663+++ hooks/db-relation-joined 2014-03-05 02:21:45 +0000
1664@@ -1,22 +1,37 @@
1665 #!/usr/bin/env python
1666
1667-from common import *
1668-
1669-import urllib
1670-import subprocess
1671+from common import (
1672+ configure_client_ssl,
1673+ create_database,
1674+ create_grant,
1675+ database_exists,
1676+ get_db_cursor,
1677+ revoke_grant)
1678+
1679+# Module import initialized variables (TODO: Remove?)
1680+from common import (
1681+ broken,
1682+ broken_path,
1683+ database_name,
1684+ database_name_file,
1685+ service_password,
1686+ slave,
1687+ user)
1688+
1689+from charmhelpers.core.hookenv import relation_set, unit_get
1690+
1691+import contextlib
1692 import os
1693 import sys
1694-import string
1695-import random
1696-import pickle
1697+import subprocess
1698
1699-cursor = get_db_cursor()
1700
1701 admin = os.path.basename(sys.argv[0]) == 'db-admin-relation-joined'
1702
1703 def runsql(sql):
1704- print "[%s]" % sql
1705- cursor.execute(sql)
1706+ with contextlib.closing(get_db_cursor()) as cursor:
1707+ print "[%s]" % sql
1708+ cursor.execute(sql)
1709
1710 runsql(
1711 "grant replication client on *.* to `%s` identified by '%s'" % (
1712@@ -25,63 +40,46 @@
1713
1714 if slave and not admin:
1715 try:
1716- runsql(
1717- "revoke all on `%s`.* from `%s`" % (
1718- database_name,
1719- user))
1720+ revoke_grant(database_name, user, privileges="")
1721 except:
1722 print "revoke failed, ignoring error"
1723- runsql(
1724- "grant select on `%s`.* to `%s`" % (
1725- database_name,
1726- user))
1727+ create_grant(database_name, user, None, privileges="select")
1728 else:
1729 if admin:
1730- runsql(
1731- "grant all privileges on *.* to `%s` identified by '%s'" % (
1732- user,
1733- service_password))
1734+ create_grant("*", user, service_password)
1735 else:
1736- runsql(
1737- "grant all on `%s`.* to `%s` identified by '%s'" % (
1738- database_name,
1739- user,
1740- service_password))
1741+ create_grant(database_name, user, service_password)
1742
1743-hostname = subprocess.check_output(['unit-get','private-address']).strip()
1744+hostname = unit_get('private-address')
1745
1746 print str(["relation-set",
1747- "database=%s" % database_name,
1748- "user=%s" % user,
1749- "password=%s" % service_password,
1750- 'host=%s' % hostname,
1751- 'slave=%s' % slave])
1752+ "database=%s" % database_name,
1753+ "user=%s" % user,
1754+ 'host=%s' % hostname,
1755+ 'slave=%s' % slave])
1756
1757 # Create new database or touch slave.configured file
1758 if slave:
1759 open(slave_configured_path,'w').close()
1760 elif not broken and not admin:
1761- # Find existing databases
1762- cursor.execute("show databases")
1763- databases = [i[0] for i in cursor.fetchall()]
1764- if database_name in databases:
1765+ if database_exists(database_name):
1766 print "database exists already"
1767 else:
1768 if not broken and not admin:
1769- runsql("create database `%s` character set utf8" % database_name)
1770+ create_database(database_name)
1771 with open(database_name_file, 'w') as dbname:
1772 dbname.write(database_name)
1773
1774 if broken:
1775 os.unlink(broken_path)
1776
1777-cursor.close()
1778-
1779 # Store new values in relation settings.
1780-subprocess.call(
1781- ["relation-set",
1782- "database=%s" % database_name,
1783- "user=%s" % user,
1784- "password=%s" % service_password,
1785- 'host=%s' % hostname,
1786- 'slave=%s' % slave,])
1787+rdata = dict(
1788+ database=database_name,
1789+ host=hostname,
1790+ user=user,
1791+ password=service_password,
1792+ slave=slave)
1793+rdata.update(configure_client_ssl())
1794+relation_set(**rdata)
1795+
1796
1797=== modified file 'hooks/ha_relations.py'
1798--- hooks/ha_relations.py 2014-02-13 18:51:20 +0000
1799+++ hooks/ha_relations.py 2014-03-05 02:21:45 +0000
1800@@ -92,6 +92,7 @@
1801
1802 def ceph_joined():
1803 utils.juju_log('INFO', 'Start Ceph Relation Joined')
1804+ utils.configure_source()
1805 ceph.install()
1806 utils.juju_log('INFO', 'Finish Ceph Relation Joined')
1807
1808
1809=== modified file 'hooks/lib/ceph_utils.py'
1810--- hooks/lib/ceph_utils.py 2014-02-13 21:34:18 +0000
1811+++ hooks/lib/ceph_utils.py 2014-03-05 02:21:45 +0000
1812@@ -165,8 +165,10 @@
1813 hosts = []
1814 for r_id in utils.relation_ids('ceph'):
1815 for unit in utils.relation_list(r_id):
1816- hosts.append(utils.relation_get('private-address',
1817- unit=unit, rid=r_id))
1818+ ceph_public_addr = utils.relation_get(
1819+ 'ceph_public_addr', unit=unit, rid=r_id) or \
1820+ utils.relation_get('private-address', unit=unit, rid=r_id)
1821+ hosts.append(ceph_public_addr)
1822 return hosts
1823
1824
1825
1826=== modified file 'hooks/lib/utils.py'
1827--- hooks/lib/utils.py 2013-03-18 10:47:05 +0000
1828+++ hooks/lib/utils.py 2014-03-05 02:21:45 +0000
1829@@ -55,13 +55,13 @@
1830
1831 def render_template(template_name, context, template_dir=TEMPLATES_DIR):
1832 templates = jinja2.Environment(
1833- loader=jinja2.FileSystemLoader(template_dir)
1834- )
1835+ loader=jinja2.FileSystemLoader(template_dir))
1836 template = templates.get_template(template_name)
1837 return template.render(context)
1838
1839 CLOUD_ARCHIVE = \
1840-""" # Ubuntu Cloud Archive
1841+"""
1842+# Ubuntu Cloud Archive
1843 deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
1844 """
1845
1846@@ -71,7 +71,13 @@
1847 'folsom/proposed': 'precise-proposed/folsom',
1848 'grizzly': 'precise-updates/grizzly',
1849 'grizzly/updates': 'precise-updates/grizzly',
1850- 'grizzly/proposed': 'precise-proposed/grizzly'
1851+ 'grizzly/proposed': 'precise-proposed/grizzly',
1852+ 'havana': 'precise-updates/havana',
1853+ 'havana/updates': 'precise-updates/havana',
1854+ 'havana/proposed': 'precise-proposed/havana',
1855+ 'icehouse': 'precise-updates/icehouse',
1856+ 'icehouse/updates': 'precise-updates/icehouse',
1857+ 'icehouse/proposed': 'precise-proposed/icehouse',
1858 }
1859
1860
1861@@ -86,8 +92,11 @@
1862 ]
1863 subprocess.check_call(cmd)
1864 if source.startswith('cloud:'):
1865+ # CA values should be formatted as cloud:ubuntu-openstack/pocket, eg:
1866+ # cloud:precise-folsom/updates or cloud:precise-folsom/proposed
1867 install('ubuntu-cloud-keyring')
1868 pocket = source.split(':')[1]
1869+ pocket = pocket.split('-')[1]
1870 with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
1871 apt.write(CLOUD_ARCHIVE.format(CLOUD_ARCHIVE_POCKETS[pocket]))
1872 if source.startswith('deb'):
1873
1874=== modified file 'hooks/shared_db_relations.py'
1875--- hooks/shared_db_relations.py 2013-07-10 16:32:52 +0000
1876+++ hooks/shared_db_relations.py 2014-03-05 02:21:45 +0000
1877@@ -11,15 +11,21 @@
1878 from common import (
1879 database_exists,
1880 create_database,
1881+ configure_client_ssl,
1882 grant_exists,
1883- create_grant
1884- )
1885-import subprocess
1886+ create_grant,
1887+ configs,
1888+ change_unit,
1889+ get_service_ca,
1890+ get_remote_ip)
1891+
1892+import functools
1893 import json
1894-import socket
1895-import os
1896 import lib.utils as utils
1897 import lib.cluster_utils as cluster
1898+import os
1899+import subprocess
1900+
1901
1902 LEADER_RES = 'res_mysql_vip'
1903
1904@@ -29,44 +35,34 @@
1905
1906
1907 def relation_get():
1908- return json.loads(subprocess.check_output(
1909- ['relation-get',
1910- '--format',
1911- 'json']
1912- )
1913- )
1914+ return json.loads(
1915+ subprocess.check_output(['relation-get', '--format', 'json']))
1916+
1917+
1918+local_hostname = utils.unit_get('private-address')
1919+
1920+
1921+def configure_db(hostname, database, username):
1922+ passwd_file = "/var/lib/mysql/mysql-{}.passwd".format(username)
1923+ remote_ip = get_remote_ip(hostname)
1924+
1925+ if not os.path.exists(passwd_file):
1926+ password = pwgen()
1927+ with open(passwd_file, 'w') as pfile:
1928+ pfile.write(password)
1929+ os.chmod(pfile.name, 0600)
1930+ else:
1931+ with open(passwd_file) as pfile:
1932+ password = pfile.read().strip()
1933+
1934+ if not database_exists(database):
1935+ create_database(database, None)
1936+ if not grant_exists(database, username, remote_ip):
1937+ create_grant(database, username, remote_ip, password)
1938+ return password
1939
1940
1941 def shared_db_changed():
1942-
1943- def configure_db(hostname,
1944- database,
1945- username):
1946- passwd_file = "/var/lib/mysql/mysql-{}.passwd"\
1947- .format(username)
1948- if hostname != local_hostname:
1949- remote_ip = socket.gethostbyname(hostname)
1950- else:
1951- remote_ip = '127.0.0.1'
1952-
1953- if not os.path.exists(passwd_file):
1954- password = pwgen()
1955- with open(passwd_file, 'w') as pfile:
1956- pfile.write(password)
1957- else:
1958- with open(passwd_file) as pfile:
1959- password = pfile.read().strip()
1960-
1961- if not database_exists(database):
1962- create_database(database)
1963- if not grant_exists(database,
1964- username,
1965- remote_ip):
1966- create_grant(database,
1967- username,
1968- remote_ip, password)
1969- return password
1970-
1971 if not cluster.eligible_leader(LEADER_RES):
1972 utils.juju_log('INFO',
1973 'MySQL service is peered, bailing shared-db relation'
1974@@ -74,64 +70,62 @@
1975 return
1976
1977 settings = relation_get()
1978- local_hostname = utils.unit_get('private-address')
1979- singleset = set([
1980- 'database',
1981- 'username',
1982- 'hostname'
1983- ])
1984+ singleset = set(['database', 'username', 'hostname'])
1985+
1986+ if configs.get('ssl', '').strip() in ('on', 'only'):
1987+ client_ssl = configure_client_ssl()
1988+ relation_set = functools.partial(utils.relation_set, **client_ssl)
1989+ else:
1990+ relation_set = utils.relation_set
1991
1992 if singleset.issubset(settings):
1993 # Process a single database configuration
1994- password = configure_db(settings['hostname'],
1995- settings['database'],
1996- settings['username'])
1997+ password = configure_db(
1998+ settings['hostname'], settings['database'], settings['username'])
1999 if not cluster.is_clustered():
2000- utils.relation_set(db_host=local_hostname,
2001- password=password)
2002+ relation_set(db_host=local_hostname, password=password)
2003 else:
2004- utils.relation_set(db_host=utils.config_get("vip"),
2005- password=password)
2006+ relation_set(db_host=utils.config_get("vip"), password=password)
2007+ return
2008
2009+ # Process multiple database setup requests.
2010+ # from incoming relation data:
2011+ # nova_database=xxx nova_username=xxx nova_hostname=xxx
2012+ # quantum_database=xxx quantum_username=xxx quantum_hostname=xxx
2013+ # create
2014+ #{
2015+ # "nova": {
2016+ # "username": xxx,
2017+ # "database": xxx,
2018+ # "hostname": xxx
2019+ # },
2020+ # "quantum": {
2021+ # "username": xxx,
2022+ # "database": xxx,
2023+ # "hostname": xxx
2024+ # }
2025+ #}
2026+ #
2027+ databases = {}
2028+ for k, v in settings.iteritems():
2029+ db = k.split('_')[0]
2030+ x = '_'.join(k.split('_')[1:])
2031+ if db not in databases:
2032+ databases[db] = {}
2033+ databases[db][x] = v
2034+ return_data = {}
2035+ for db in databases:
2036+ if singleset.issubset(databases[db]):
2037+ return_data['_'.join([db, 'password'])] = \
2038+ configure_db(databases[db]['hostname'],
2039+ databases[db]['database'],
2040+ databases[db]['username'])
2041+ if len(return_data) > 0:
2042+ relation_set(**return_data)
2043+ if not cluster.is_clustered():
2044+ relation_set(db_host=local_hostname)
2045 else:
2046- # Process multiple database setup requests.
2047- # from incoming relation data:
2048- # nova_database=xxx nova_username=xxx nova_hostname=xxx
2049- # quantum_database=xxx quantum_username=xxx quantum_hostname=xxx
2050- # create
2051- #{
2052- # "nova": {
2053- # "username": xxx,
2054- # "database": xxx,
2055- # "hostname": xxx
2056- # },
2057- # "quantum": {
2058- # "username": xxx,
2059- # "database": xxx,
2060- # "hostname": xxx
2061- # }
2062- #}
2063- #
2064- databases = {}
2065- for k, v in settings.iteritems():
2066- db = k.split('_')[0]
2067- x = '_'.join(k.split('_')[1:])
2068- if db not in databases:
2069- databases[db] = {}
2070- databases[db][x] = v
2071- return_data = {}
2072- for db in databases:
2073- if singleset.issubset(databases[db]):
2074- return_data['_'.join([db, 'password'])] = \
2075- configure_db(databases[db]['hostname'],
2076- databases[db]['database'],
2077- databases[db]['username'])
2078- if len(return_data) > 0:
2079- utils.relation_set(**return_data)
2080- if not cluster.is_clustered():
2081- utils.relation_set(db_host=local_hostname)
2082- else:
2083- utils.relation_set(db_host=utils.config_get("vip"))
2084+ relation_set(db_host=utils.config_get("vip"))
2085
2086 hooks = {
2087 "shared-db-relation-changed": shared_db_changed
2088
2089=== modified file 'hooks/slave-relation-changed'
2090--- hooks/slave-relation-changed 2013-02-12 17:54:29 +0000
2091+++ hooks/slave-relation-changed 2014-03-05 02:21:45 +0000
2092@@ -87,3 +87,4 @@
2093 mysql $ROOTARGS -e "START SLAVE"
2094 mysql $ROOTARGS -e "SHOW SLAVE STATUS"
2095 touch /var/lib/juju/i.am.a.slave
2096+
2097
2098=== modified file 'revision'
2099--- revision 2014-02-13 18:51:20 +0000
2100+++ revision 2014-03-05 02:21:45 +0000
2101@@ -1,1 +1,2 @@
2102-311
2103+321
2104+

Subscribers

People subscribed via source and target branches