Merge lp:~james-page/charms/precise/mysql/shared-db-multi into lp:charms/mysql

Proposed by James Page
Status: Merged
Approved by: Clint Byrum
Approved revision: 91
Merged at revision: 91
Proposed branch: lp:~james-page/charms/precise/mysql/shared-db-multi
Merge into: lp:charms/mysql
Diff against target: 323 lines (+185/-105)
4 files modified
hooks/common.py (+55/-2)
hooks/shared-db-common (+0/-41)
hooks/shared-db-relations (+129/-61)
revision (+1/-1)
To merge this branch: bzr merge lp:~james-page/charms/precise/mysql/shared-db-multi
Reviewer Review Type Date Requested Status
Adam Gandelman (community) Approve
Clint Byrum (community) Approve
Review via email: mp+137593@code.launchpad.net

Description of the change

This MP adds a new feature to the shared-db relation; specifically it
allow multiple databases/users to be created across the interface in
a single transaction.

This was required to support deploying the Quantum API server as part
of the nova-cloud-controller charm; Quantum requires its own database
as it namespace conflicts with Nova.

The hook was originally written in bash; as this is a bit more complex
I took the opportunity to rewrite it in python to make handling the
multiple database creation a bit easier.

The existing charm interface remains intact.

Credit to Adam G for this approach; its the same as used in Keystone.

To post a comment you must log in.
Revision history for this message
Benjamin Kerensa (bkerensa) wrote :

Looks like a clean change to the hook to me... Good work

Revision history for this message
Clint Byrum (clint-fewbar) wrote :

Looks good. +1

review: Approve
Revision history for this message
Adam Gandelman (gandelman-a) wrote :

Thanks James. This works really well. I thought I was half-insane when I came up with this strategy for multi-endpoints in Keystone. Glad to see its working well elsewhere.

Merged with some whitespace cleanup.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/common.py'
2--- hooks/common.py 2012-11-02 06:41:12 +0000
3+++ hooks/common.py 2012-12-03 15:24:32 +0000
4@@ -59,8 +59,61 @@
5 def get_db_cursor():
6 # Connect to mysql
7 passwd = open("/var/lib/juju/mysql.passwd").read().strip()
8- print passwd
9 connection = MySQLdb.connect(user="root", host="localhost", passwd=passwd)
10-
11 return connection.cursor()
12
13+
14+def database_exists(db_name):
15+ cursor = get_db_cursor()
16+ try:
17+ cursor.execute("SHOW DATABASES")
18+ databases = [i[0] for i in cursor.fetchall()]
19+ finally:
20+ cursor.close()
21+ return db_name in databases
22+
23+
24+def create_database(db_name):
25+ cursor = get_db_cursor()
26+ try:
27+ cursor.execute("CREATE DATABASE {}".format(db_name))
28+ finally:
29+ cursor.close()
30+
31+
32+def grant_exists(db_name, db_user, remote_ip):
33+ cursor = get_db_cursor()
34+ try:
35+ cursor.execute("SHOW GRANTS for '{}'@'{}'".format(db_user,
36+ remote_ip))
37+ grants = [i[0] for i in cursor.fetchall()]
38+ except MySQLdb.OperationalError:
39+ print "No grants found"
40+ return False
41+ finally:
42+ cursor.close()
43+ return "GRANT ALL PRIVILEGES ON `{}`".format(db_name) in grants
44+
45+
46+def create_grant(db_name, db_user,
47+ remote_ip, password):
48+ cursor = get_db_cursor()
49+ try:
50+ cursor.execute("GRANT ALL PRIVILEGES ON {}.* TO '{}'@'{}' "\
51+ "IDENTIFIED BY '{}'".format(db_name,
52+ db_user,
53+ remote_ip,
54+ password))
55+ finally:
56+ cursor.close()
57+
58+
59+def cleanup_grant(db_user,
60+ remote_ip):
61+ cursor = get_db_cursor()
62+ try:
63+ cursor.execute("DROP FROM mysql.user WHERE user='{}' "\
64+ "AND HOST='{}'".format(db_user,
65+ remote_ip))
66+ finally:
67+ cursor.close()
68
69=== removed file 'hooks/shared-db-common'
70--- hooks/shared-db-common 2011-11-09 19:43:42 +0000
71+++ hooks/shared-db-common 1970-01-01 00:00:00 +0000
72@@ -1,41 +0,0 @@
73-#!/bin/bash
74-#
75-# Common utility functions used for hooks in shared-db-relations
76-#
77-# Notes:
78-# - /var/lib/juju/mysql.passwd is created during install hook
79-#
80-# Author: Adam Gandelman <adam.gandelman@canonical.com>
81-
82-DEFAULT_ETH=$(ip route | grep default | awk '{ print $5 }')
83-
84-database_exists() {
85- mysql -u root -p$(cat /var/lib/juju/mysql.passwd) \
86- -NBe "SHOW DATABASES" | grep $DATABASE >/dev/null
87-}
88-
89-create_database() {
90- juju-log "mysql - shared-db: Creating database $DATABASE"
91- mysql -u root -p$(cat /var/lib/juju/mysql.passwd) \
92- -NBe "CREATE DATABASE $DATABASE" >/dev/null
93-}
94-
95-grant_exists() {
96- [[ $(mysql -u root -p$(cat /var/lib/juju/mysql.passwd) \
97- -NBe "SELECT User, Host FROM user \
98- WHERE User='$DB_USER' AND Host='$REMOTE_IP'" mysql | wc -l) -gt 0 ]]
99-}
100-
101-create_grant() {
102- juju-log "mysql - shared-db: Creating grant for $DB_USER@$REMOTE_IP"
103- mysql -u root -p$(cat /var/lib/juju/mysql.passwd) \
104- -NBe "GRANT ALL PRIVILEGES ON $DATABASE.* TO '$DB_USER'@'$REMOTE_IP' \
105- IDENTIFIED BY '$PASSWORD'"
106-}
107-
108-cleanup_grant() {
109- juju-log "mysql - shared-db: Cleaning up grant for $DB_USER@$REMOTE_IP"
110- mysql -u root -p$(cat /var/lib/juju/mysql.passwd) \
111- -e "DROP FROM user WHERE user='$DB_USER' AND HOST='$REMOTE_IP'" mysql
112-}
113-
114
115=== modified file 'hooks/shared-db-relations'
116--- hooks/shared-db-relations 2012-10-29 19:35:34 +0000
117+++ hooks/shared-db-relations 2012-12-03 15:24:32 +0000
118@@ -1,4 +1,4 @@
119-#!/bin/bash
120+#!/usr/bin/python
121 #
122 # Create relations between a shared database to many peers.
123 # Join does nothing. Peer requests access to $DATABASE from $REMOTE_HOST.
124@@ -7,63 +7,131 @@
125 #
126 # Author: Adam Gandelman <adam.gandelman@canonical.com>
127
128-set -ue
129-
130-FORMULA_DIR=$(dirname $0)
131-
132-if [[ -e $FORMULA_DIR/shared-db-common ]] ; then
133- . $FORMULA_DIR/shared-db-common
134-else
135- juju-log "mysql - shared-db: ERROR Could not load $FORMULA_DIR/shared-db-common"
136-fi
137-
138-shared-db_changed() {
139- juju-log "mysql - shared-db: Relation CHANGED"
140-
141- DATABASE=`relation-get database`
142- DB_USER=`relation-get username`
143- REMOTE_HOST=`relation-get hostname`
144- PASSWD_FILE="/var/lib/juju/mysql-$DB_USER.passwd"
145-
146- # peer not ready, exit 0 and wait.
147- [[ -z $DATABASE ]] || [[ -z $REMOTE_HOST ]] || [[ -z $DB_USER ]] \
148- && echo "DATABASE||REMOTE_HOST||DB_USER not set. Peer not ready?" && exit 0
149-
150- # to be used for GRANTs
151- # if the remote unit is actually placed on the same system,
152- # it'll need a grant for a user connecting from 127.0.0.1
153- local_hostname=$(hostname -f)
154- if [[ $REMOTE_HOST != $local_hostname ]] ; then
155- REMOTE_IP=$(python -c "import socket ; print socket.gethostbyname('$REMOTE_HOST')")
156- else
157- REMOTE_IP="127.0.0.1"
158- fi
159-
160- if [[ ! -e $PASSWD_FILE ]] ; then
161- PASSWORD=$(pwgen -s 16)
162- echo $PASSWORD >$PASSWD_FILE
163- else
164- PASSWORD=$(cat $PASSWD_FILE)
165- fi
166-
167- if ! database_exists ; then
168- juju-log "mysql - shared db: Creating database $DATABASE"
169- create_database || exit 1
170- echo "Done"
171- fi
172- if ! grant_exists ; then
173- juju-log "mysql - shared-db: Granting $DB_USER@$REMOTE_HOST access to $DATABASE"
174- create_grant || exit 1
175- fi
176- relation-set db_host=$(hostname -f) password=$PASSWORD
177- juju-log "mysql - shared-db: Returning access to $DATABASE to $DB_USER@$REMOTE_HOST."
178-}
179-
180-RELATION_ACTION=${0##*/shared-db-relation-}
181-juju-log "HOOK: shared-db-relation-$RELATION_ACTION fired."
182-
183-case $RELATION_ACTION in
184- "joined") exit 0 ;; # do nothing on join, wait for peer to request access
185- "changed") shared-db_changed ;;
186- *) exit 0 ;;
187-esac
188+
189+from common import *
190+import sys
191+import subprocess
192+import json
193+import socket
194+import os
195+
196+
197+def pwgen():
198+ return subprocess.check_output(['pwgen', '-s', '16']).strip()
199+
200+
201+def relation_get():
202+ return json.loads(subprocess.check_output(
203+ ['relation-get',
204+ '--format',
205+ 'json']
206+ )
207+ )
208+
209+
210+def relation_set(**kwargs):
211+ cmd = [ 'relation-set' ]
212+ args = []
213+ for k, v in kwargs.items():
214+ if k == 'rid':
215+ cmd.append('-r')
216+ cmd.append(v)
217+ else:
218+ args.append('{}={}'.format(k, v))
219+ cmd += args
220+ subprocess.check_call(cmd)
221+
222+
223+def shared_db_changed():
224+
225+ def configure_db(hostname,
226+ database,
227+ username):
228+ passwd_file = "/var/lib/juju/mysql-{}.passwd"\
229+ .format(username)
230+ if hostname != local_hostname:
231+ remote_ip = socket.gethostbyname(hostname)
232+ else:
233+ remote_ip = '127.0.0.1'
234+
235+ if not os.path.exists(passwd_file):
236+ password = pwgen()
237+ with open(passwd_file, 'w') as pfile:
238+ pfile.write(password)
239+ else:
240+ with open(passwd_file) as pfile:
241+ password = pfile.read().strip()
242+
243+ if not database_exists(database):
244+ create_database(database)
245+ if not grant_exists(database,
246+ username,
247+ remote_ip):
248+ create_grant(database,
249+ username,
250+ remote_ip, password)
251+ return password
252+
253+ settings = relation_get()
254+ local_hostname = socket.getfqdn()
255+ singleset = set([
256+ 'database',
257+ 'username',
258+ 'hostname'
259+ ])
260+
261+ if singleset.issubset(settings):
262+ # Process a single database configuration
263+ password = configure_db(settings['hostname'],
264+ settings['database'],
265+ settings['username'])
266+ relation_set(db_host=local_hostname,
267+ password=password)
268+ else:
269+ # Process multiple database setup requests.
270+ # from incoming relation data:
271+ # nova_database=xxx nova_username=xxx nova_hostname=xxx
272+ # quantum_database=xxx quantum_username=xxx quantum_hostname=xxx
273+ # create
274+ #{
275+ # "nova": {
276+ # "username": xxx,
277+ # "database": xxx,
278+ # "hostname": xxx
279+ # },
280+ # "quantum": {
281+ # "username": xxx,
282+ # "database": xxx,
283+ # "hostname": xxx
284+ # }
285+ #}
286+ #
287+ databases = {}
288+ for k, v in settings.iteritems():
289+ db = k.split('_')[0]
290+ x = '_'.join(k.split('_')[1:])
291+ if db not in databases:
292+ databases[db] = {}
293+ databases[db][x] = v
294+ return_data = {}
295+ for db in databases:
296+ if singleset.issubset(databases[db]):
297+ return_data['_'.join([ db, 'password' ])] = \
298+ configure_db(databases[db]['hostname'],
299+ databases[db]['database'],
300+ databases[db]['username'])
301+ relation_set(**return_data)
302+ relation_set(db_host=local_hostname)
303+
304+hook = os.path.basename(sys.argv[0])
305+hooks = {
306+ "shared-db-relation-changed": shared_db_changed
307+ }
308+try:
309+ hook_func = hooks[hook]
310+except KeyError:
311+ pass
312+else:
313+ hook_func()
314+
315+sys.exit(0)
316\ No newline at end of file
317
318=== modified file 'revision'
319--- revision 2012-11-02 06:41:12 +0000
320+++ revision 2012-12-03 15:24:32 +0000
321@@ -1,1 +1,1 @@
322-164
323+165

Subscribers

People subscribed via source and target branches

to all changes: