Merge lp:~gandelman-a/charms/precise/keystone/cluster_leader into lp:~openstack-charmers/charms/precise/keystone/ha-support

Proposed by Adam Gandelman
Status: Merged
Merged at revision: 50
Proposed branch: lp:~gandelman-a/charms/precise/keystone/cluster_leader
Merge into: lp:~openstack-charmers/charms/precise/keystone/ha-support
Diff against target: 227 lines (+99/-25)
3 files modified
hooks/keystone-hooks (+21/-22)
hooks/utils.py (+77/-2)
revision (+1/-1)
To merge this branch: bzr merge lp:~gandelman-a/charms/precise/keystone/cluster_leader
Reviewer Review Type Date Requested Status
James Page Approve
Review via email: mp+145979@code.launchpad.net

Description of the change

Adds better support for service leaders.

* The service leader is determined depending on how keystone is currently clustered. If there are multiple units, but no hacluster subordinate, the oldest service unit is elected leader (lowest unit number). If hacluster exists and the service is clustered, the CRM is consulted and the node hosting the resources is designated the leader.

* Only the leader may initialize or touch the database (create users, endpoints, etc)

* The leader is responsible for synchronizing a list of service credentials to all peers. The list is stored on disk and resolves the issue of the passwd dump files in /var/lib/keystone/ being out-of-sync among peers.

We can use the same approach in the rabbitmq-server charm if it works out here.

To post a comment you must log in.
51. By Adam Gandelman

Little cleanup.

Revision history for this message
James Page (james-page) wrote :

Tested OK and generally looks sound

+1 merged.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/keystone-hooks'
2--- hooks/keystone-hooks 2013-01-21 15:05:12 +0000
3+++ hooks/keystone-hooks 2013-01-31 21:34:20 +0000
4@@ -73,12 +73,14 @@
5 driver='keystone.token.backends.sql.Token')
6 update_config_block('ec2',
7 driver='keystone.contrib.ec2.backends.sql.Ec2')
8+
9 execute("service keystone stop", echo=True)
10 execute("keystone-manage db_sync")
11 execute("service keystone start", echo=True)
12 time.sleep(5)
13 ensure_initial_admin(config)
14
15+
16 def db_joined():
17 relation_data = { "database": config["database"],
18 "username": config["database-user"],
19@@ -96,7 +98,14 @@
20 relation_data["password"],
21 relation_data["private-address"],
22 config["database"]))
23+
24 execute("service keystone stop", echo=True)
25+
26+ if not eligible_leader():
27+ juju_log('Deferring DB initialization to service leader.')
28+ execute("service keystone start")
29+ return
30+
31 execute("keystone-manage db_sync", echo=True)
32 execute("service keystone start")
33 time.sleep(5)
34@@ -137,8 +146,8 @@
35 adminurl=adminurl,
36 internalurl=internalurl)
37
38- if is_clustered() and not is_leader():
39- # Only respond if service unit is the leader
40+ if not eligible_leader():
41+ juju_log('Deferring identity_changed() to service leader.')
42 return
43
44 settings = relation_get_dict(relation_id=relation_id,
45@@ -217,16 +226,7 @@
46 token = get_admin_token()
47 juju_log("Creating service credentials for '%s'" % service_username)
48
49- # TODO: This needs to be changed as it won't work with ha keystone
50- stored_passwd = '/var/lib/keystone/%s.passwd' % service_username
51- if os.path.isfile(stored_passwd):
52- juju_log("Loading stored service passwd from %s" % stored_passwd)
53- service_password = open(stored_passwd, 'r').readline().strip('\n')
54- else:
55- juju_log("Generating a new service password for %s" % service_username)
56- service_password = execute('pwgen -c 32 1', die=True)[0].strip()
57- open(stored_passwd, 'w+').writelines("%s\n" % service_password)
58-
59+ service_password = get_service_password(service_username)
60 create_user(service_username, service_password, config['service-tenant'])
61 grant_role(service_username, config['admin-role'], config['service-tenant'])
62
63@@ -254,6 +254,7 @@
64 relation_data["service_port"] = SERVICE_PORTS['keystone_service']
65
66 relation_set(relation_data)
67+ synchronize_service_credentials()
68
69 def config_changed():
70
71@@ -268,11 +269,9 @@
72
73 set_admin_token(config['admin-token'])
74
75- if is_clustered() and is_leader():
76+ if eligible_leader():
77 juju_log('Cluster leader - ensuring endpoint configuration is up to date')
78 ensure_initial_admin(config)
79- elif not is_clustered():
80- ensure_initial_admin(config)
81
82 update_config_block('logger_root', level=config['log-level'],
83 file='/etc/keystone/logging.conf')
84@@ -286,11 +285,9 @@
85
86 def upgrade_charm():
87 cluster_changed()
88- if is_clustered() and is_leader():
89+ if eligible_leader():
90 juju_log('Cluster leader - ensuring endpoint configuration is up to date')
91 ensure_initial_admin(config)
92- elif not is_clustered():
93- ensure_initial_admin(config)
94
95
96 SERVICE_PORTS = {
97@@ -310,6 +307,8 @@
98 configure_haproxy(cluster_hosts,
99 SERVICE_PORTS)
100
101+ synchronize_service_credentials()
102+
103
104 def ha_relation_changed():
105 relation_data = relation_get_dict()
106@@ -387,7 +386,7 @@
107
108 # keystone-hooks gets called by symlink corresponding to the requested relation
109 # hook.
110-arg0 = sys.argv[0].split("/").pop()
111-if arg0 not in hooks.keys():
112- error_out("Unsupported hook: %s" % arg0)
113-hooks[arg0]()
114+hook = os.path.basename(sys.argv[0])
115+if hook not in hooks.keys():
116+ error_out("Unsupported hook: %s" % hook)
117+hooks[hook]()
118
119=== modified file 'hooks/utils.py'
120--- hooks/utils.py 2013-01-22 17:49:36 +0000
121+++ hooks/utils.py 2013-01-31 21:34:20 +0000
122@@ -11,10 +11,10 @@
123 keystone_conf = "/etc/keystone/keystone.conf"
124 stored_passwd = "/var/lib/keystone/keystone.passwd"
125 stored_token = "/var/lib/keystone/keystone.token"
126-
127+SERVICE_PASSWD_PATH = '/var/lib/keystone/services.passwd'
128
129 def execute(cmd, die=False, echo=False):
130- """ Executes a command
131+ """ Executes a command
132
133 if die=True, script will exit(1) if command does not return 0
134 if echo=True, output of command will be printed to stdout
135@@ -398,6 +398,31 @@
136 manager.api.users.update_password(user=user_id, password=password)
137 juju_log("Successfully updated password for user '%s'" % username)
138
139+def load_stored_passwords(path=SERVICE_PASSWD_PATH):
140+ creds = {}
141+ if not os.path.isfile(path):
142+ return creds
143+
144+ stored_passwd = open(path, 'r')
145+ for l in stored_passwd.readlines():
146+ user, passwd = l.strip().split(':')
147+ creds[user] = passwd
148+ return creds
149+
150+def save_stored_passwords(path=SERVICE_PASSWD_PATH, **creds):
151+ with open(path, 'wb') as stored_passwd:
152+ [stored_passwd.write('%s:%s\n' % (u, p)) for u, p in creds.iteritems()]
153+
154+def get_service_password(service_username):
155+ creds = load_stored_passwords()
156+ if service_username in creds:
157+ return creds[service_username]
158+
159+ passwd = subprocess.check_output(['pwgen', '-c', '32', '1']).strip()
160+ creds[service_username] = passwd
161+ save_stored_passwords(**creds)
162+
163+ return passwd
164
165 def configure_pki_tokens(config):
166 '''Configure PKI token signing, if enabled.'''
167@@ -482,3 +507,53 @@
168 return True
169 else:
170 return False
171+
172+
173+def peer_units():
174+ peers = []
175+ for r_id in (relation_ids('cluster') or []):
176+ for unit in (relation_list(r_id) or []):
177+ peers.append(unit)
178+ return peers
179+
180+def oldest_peer(peers):
181+ local_unit_no = os.getenv('JUJU_UNIT_NAME').split('/')[1]
182+ for peer in peers:
183+ remote_unit_no = peer.split('/')[1]
184+ if remote_unit_no < local_unit_no:
185+ return False
186+ return True
187+
188+
189+def eligible_leader():
190+ if is_clustered():
191+ if not is_leader():
192+ juju_log('Deferring action to CRM leader.')
193+ return False
194+ else:
195+ peers = peer_units()
196+ if peers and not oldest_peer(peers):
197+ juju_log('Deferring action to oldest service unit.')
198+ return False
199+ return True
200+
201+
202+def synchronize_service_credentials():
203+ '''
204+ Broadcast service credentials to peers or consume those that have been
205+ broadcasted by peer, depending on hook context.
206+ '''
207+ if os.path.basename(sys.argv[0]) == 'cluster-relation-changed':
208+ r_data = relation_get_dict()
209+ if 'service_credentials' in r_data:
210+ juju_log('Saving service passwords from peer.')
211+ save_stored_passwords(**json.loads(r_data['service_credentials']))
212+ return
213+
214+ creds = load_stored_passwords()
215+ if not creds:
216+ return
217+ juju_log('Synchronizing service passwords to all peers.')
218+ creds = json.dumps(creds)
219+ for r_id in (relation_ids('cluster') or []):
220+ relation_set_2(rid=r_id, service_credentials=creds)
221
222=== modified file 'revision'
223--- revision 2013-01-22 17:49:36 +0000
224+++ revision 2013-01-31 21:34:20 +0000
225@@ -1,1 +1,1 @@
226-195
227+196

Subscribers

People subscribed via source and target branches