Merge lp:~openstack-charmers/charms/precise/mysql/ssl-everywhere into lp:charms/mysql
- Precise Pangolin (12.04)
- ssl-everywhere
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Marco Ceppi (community) | Needs Resubmitting | ||
Review via email:
|
This proposal supersedes a proposal from 2014-02-24.
Commit message
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).
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Kapil Thangavelu (hazmat) wrote : Posted in a previous version of this proposal | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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:/
File config.yaml (right):
https:/
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:/
File hooks/db-
https:/
hooks/db-
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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:/
> File config.yaml (right):
>
> https:/
> 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:/
> File hooks/db-
>
>
> https:/
> hooks/db-
> 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:/
>
> --
>
> https:/
> You are the owner of lp:~hazmat/charms/precise/mysql/ssl-everywhere.
>
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Kapil Thangavelu (hazmat) wrote : Posted in a previous version of this proposal | # |
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:/
>> File config.yaml (right):
>>
>> https:/
>> 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:/
>> File hooks/db-
>>
>>
>> https:/
>> hooks/db-
>> 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:/
>>
>> --
>>
>> https:/
>> You are the owner of lp:~hazmat/charms/precise/mysql/ssl-everywhere....
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
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
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 | + |
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): changed relation- joined db_relations. py relation- changed
A [revision details]
M config.yaml
M hooks/common.py
M hooks/config-
M hooks/db-
M hooks/shared_
M hooks/slave-
A hooks/sslca.py
M revision