Merge lp:~nick-moffitt/charms/precise/keystone/migrate-sqlite-to-mysql into lp:~openstack-charmers-archive/charms/precise/keystone/trunk

Proposed by Marco Ceppi
Status: Work in progress
Proposed branch: lp:~nick-moffitt/charms/precise/keystone/migrate-sqlite-to-mysql
Merge into: lp:~openstack-charmers-archive/charms/precise/keystone/trunk
Diff against target: 150 lines (+98/-0) (has conflicts)
1 file modified
hooks/keystone_hooks.py (+98/-0)
Text conflict in hooks/keystone_hooks.py
Conflict adding file hooks/upgrade-charm.  Moved existing file to hooks/upgrade-charm.moved.
To merge this branch: bzr merge lp:~nick-moffitt/charms/precise/keystone/migrate-sqlite-to-mysql
Reviewer Review Type Date Requested Status
Adam Gandelman Pending
OpenStack Charmers Pending
Review via email: mp+207162@code.launchpad.net

This proposal supersedes a proposal from 2013-02-19.

Description of the change

This change uses sqlfairy (http://packages.ubuntu.com/precise/libsql-translator-perl) to migrate the schema and then all rows from the default sqlite database over to the new database when the mysql relation fires for the first time. The rationale is that users may not realize that they're using sqlite until it's too late, and adding the relation caused a loss of all authentication data.

The SQL generated from sqlfairy needed some manipulation in order to work.
In particular:

    * "UNIQUE `` (`name`)" commands needed a UID of some sort between the ``
      for mysql to accept them
    * As we're running these mysql 'LOAD DATA' commands from a remote
      client, 'LOAD DATA INFILE' needed to become 'LOAD DATA LOCAL INFILE',
      and the mysql client needed to be explicitly told to allow this
      syntax.

The install hook edits configuration entries, risking loss of idempotence. Instead of just re-running it in the upgrade hook, I've replicated the apt-get commands.

To post a comment you must log in.
Revision history for this message
Adam Gandelman (gandelman-a) wrote : Posted in a previous version of this proposal

Hey Nick-

Thanks for submitting. This is quite useful. Tested using a sqlite database /w a few thousand users and the migration worked great. I'm just a bit confused about the last bit WRT upgrade. The added 'apt-get' commands you added to the install hook, which currently isn't called on charm upgrade. Did you mean to add a new upgrade hook that re-runs apt-get update only?

review: Needs Information
Revision history for this message
Nick Moffitt (nick-moffitt) wrote : Posted in a previous version of this proposal

Adam, thanks for helping to test this!

In my initial use case, we had a system that we realized with some shock had been running with sqlite, and we were about to see an increase in the volume of the database. Since this charm was already running on a production system, I needed to make sure that the new package dependencies (sqlfairy itself, for instance) were installed before the relation hook fired.

In my understanding, no configuration steps should be taken in install hooks, since a fresh install will get a config-changed after the install hook fires anyway. Unfortunately this charm seemed to have done some config steps in the install hook, so I was nervous about trusting its idempotence and calling it from my upgrade hook.

Since this was meant to roll out a change to our production service, I took the simplest step forward that would obviously work correctly. So that's why there's duplication of the apt-get steps in the upgrade hook function.

I would love to see that bit detangled somewhat, but it seemed out of scope for this merge.

Revision history for this message
Nick Moffitt (nick-moffitt) wrote : Posted in a previous version of this proposal

Oh, I see now that I may have mispasted during the merge! I'll tidy that up.

Revision history for this message
Adam Gandelman (gandelman-a) wrote : Posted in a previous version of this proposal

Nick-

Yes, you're right about the config options being set in the install hook being a bug. They were originally there to work on old deficiencies in packaging that are fixed now, and could probably be dropped from there / moved to config_changed.

That said, I'm not sure about duplicating the apt-get commands wholesale in upgrade-charm. As its proposed here, if there has been a newer package published to the archive for Keystone (or any other package that may have charm modified configs) since the last charm upgrade, apt-get will block waiting for user guidance on how to handle the config changes (merge, overwrite, shell, etc) and the upgrade hook will never complete. I see a couple of options:

- Call apt-get /w "--option Dpkg::Options::=--force-confnew -n" to ensure we keep our juju modified configs in place. This could be problematic, though, if the updated packages contain critical config file changes, too.

- Avoid calling apt-get in the upgrade hook and instead late install the packages required for migration as needed in migrate_db(), similar to how subprocess is being imported late there.

The latter seems safest to me, since we're only acting on the migration related packages, and ideally only gets run once upon relating to a new mysql server. What do you think?

review: Needs Fixing
Revision history for this message
Nick Moffitt (nick-moffitt) wrote : Posted in a previous version of this proposal

So it sounds like even the apt commands in the install hook are not idempotent! If that's the case, how do we solve this more generally?

Revision history for this message
Nick Moffitt (nick-moffitt) wrote : Posted in a previous version of this proposal

And I think you're right about the apt commands being best suited to the particular hook. I'll have a go at that next.

Revision history for this message
Adam Gandelman (gandelman-a) wrote : Posted in a previous version of this proposal

Marking 'Rejected' to get it out of the charm review queue, feel free to mark as 'Needs Review' when ready.

Revision history for this message
Nick Moffitt (nick-moffitt) wrote : Posted in a previous version of this proposal

I've updated my branch, but this MP seems to still have the old diff. I've marked it back to "needs review" in the hopes that it will catch up as a result.

Revision history for this message
Juan L. Negron (negronjl) wrote : Posted in a previous version of this proposal

I'm deferring this to Adam.

-Juan

Revision history for this message
Adam Gandelman (gandelman-a) wrote : Posted in a previous version of this proposal

Hey Nick-- Sorry for the delay on this. We're in the process of testing and merging back 6 mo. worth of charm work to the main cs:precise/keystone branch. I'm hesitant to merge this until thats done, but the changes you've committed in response to my comments look good. We will hopefully have the main branch updated before next week, and I'll merge this in as soon as we do.

Revision history for this message
Adam Gandelman (gandelman-a) wrote : Posted in a previous version of this proposal

Hey Nick-

We've landed all of the pending keystone stuff. Any chance you can rebase this against current state of lp:charms/keystone? We'll be busy for a while migrating existing shell-based openstack stuff to python, so the keystone charm should be relatively quite in the meantime. Thanks

Unmerged revisions

42. By Nick Moffitt

Moved sqlfairy installation insurance down to the db_changed hook, so that it happens only when it's needed.
This is in response to https://code.launchpad.net/~nick-moffitt/charms/precise/keystone/migrate-sqlite-to-mysql/+merge/149334/comments/327313

41. By Nick Moffitt

Fix the half-finished upgrade-charm work.

40. By Nick Moffitt

Automatically migrate the database schema and contents when moving from sqlite to mysql.

This change uses sqlfairy (http://packages.ubuntu.com/precise/libsql-translator-perl) to migrate the schema and then all rows from the default sqlite database over to the new database when the mysql relation fires for the first time. The rationale is that users may not realize that they're using sqlite until it's too late, and adding the relation caused a loss of all authentication data.

The SQL generated from sqlfairy needed some manipulation in order to work.
In particular:

    * "UNIQUE `` (`name`)" commands needed a UID of some sort between the ``
      for mysql to accept them
    * As we're running these mysql 'LOAD DATA' commands from a remote
      client, 'LOAD DATA INFILE' needed to become 'LOAD DATA LOCAL INFILE',
      and the mysql client needed to be explicitly told to allow this
      syntax.

The install hook edits configuration entries, risking loss of idempotence. Instead of just re-running it in the upgrade hook, I've replicated the apt-get commands.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/keystone_hooks.py'
2--- hooks/keystone_hooks.py 2014-02-03 11:58:43 +0000
3+++ hooks/keystone_hooks.py 2014-02-19 12:35:28 +0000
4@@ -45,11 +45,16 @@
5
6 config = config_get()
7
8+<<<<<<< TREE
9 packages = [
10 "keystone", "python-mysqldb", "pwgen",
11 "haproxy", "python-jinja2", "openssl", "unison",
12 "python-sqlalchemy"
13 ]
14+=======
15+packages = "keystone python-mysqldb pwgen"
16+sql_migration_packages = "sqlfairy libdbd-sqlite3-perl mysql-client-core-5.5"
17+>>>>>>> MERGE-SOURCE
18 service = "keystone"
19
20 # used to verify joined services are valid openstack components.
21@@ -153,18 +158,88 @@
22
23
24 def db_changed():
25+<<<<<<< TREE
26 relation_data = utils.relation_get_dict()
27+=======
28+ def migrate_db(db_uri, relation_data):
29+ import subprocess
30+ schemacmd = ['sqlt',
31+ '-f', 'DBI', '--dsn', 'dbi:SQLite:{}'.format(db_uri.path),
32+ '-t', 'MySQL',
33+ '--add-drop-table']
34+ p = subprocess.Popen(schemacmd, stdout=subprocess.PIPE)
35+ idx = 0
36+ schema = ''
37+ for line in p.stdout:
38+ if 'UNIQUE ``' in line:
39+ # Anonymous indexes collide, so we give a random name
40+ line = line.replace('``', '`idx{}`'.format(idx))
41+ idx += 1
42+ schema += line
43+ mysqlcmd = ['mysql', '--local-infile',
44+ '--host={private-address}'.format(**relation_data),
45+ '--user={database-user}'.format(**config),
46+ '--password={password}'.format(**relation_data),
47+ '--database={database}'.format(**config)]
48+ p = subprocess.Popen(mysqlcmd, stdout=subprocess.PIPE,
49+ stdin=subprocess.PIPE)
50+ print schema
51+ p.communicate(schema)
52+ if p.returncode:
53+ juju_log("Schema failed to update.")
54+ raise subprocess.CalledProcessError(p.returncode, mysqlcmd)
55+ # This next step can be confusing: sqlt generates a perl script we
56+ # then run to do the actual data migration.
57+ make_dumpercmd = ['sqlt',
58+ '-f', 'DBI', '--dsn', 'dbi:SQLite:{}'.format(db_uri.path),
59+ '-t', 'Dumper',
60+ '--use-same-auth']
61+ p = subprocess.check_call(make_dumpercmd,
62+ stdout=open('db_migrate.pl', 'w'))
63+ run_dumpercmd = ['perl', 'db_migrate.pl', '--mysql-loadfile']
64+ out = subprocess.check_output(run_dumpercmd)
65+ # We need to load data from this system, not the one running mysql
66+ loadfiledata = out.replace('LOAD DATA INFILE',
67+ 'LOAD DATA LOCAL INFILE')
68+ p = subprocess.Popen(mysqlcmd, stdout=subprocess.PIPE,
69+ stdin=subprocess.PIPE)
70+ p.communicate(loadfiledata)
71+ if p.returncode:
72+ juju_log("Data failed to update.")
73+ raise subprocess.CalledProcessError(p.returncode, mysqlcmd)
74+
75+ from urlparse import urlparse
76+ from ConfigParser import ConfigParser
77+
78+ relation_data = relation_get_dict()
79+>>>>>>> MERGE-SOURCE
80 if ('password' not in relation_data or
81+<<<<<<< TREE
82 'db_host' not in relation_data):
83 utils.juju_log('INFO',
84 "db_host or password not set. Peer not ready, exit 0")
85 return
86
87+=======
88+ 'private-address' not in relation_data):
89+ juju_log("private-address or password not set. Peer not ready, exit 0")
90+ exit(0)
91+
92+ keyconf = ConfigParser()
93+ keyconf.read(keystone_conf)
94+ db_uri = urlparse(keyconf.get('sql','connection'))
95+
96+ execute("apt-get update", die=True)
97+ execute("apt-get -y install %s" % sql_migration_packages, die=True,
98+ echo=True)
99+
100+>>>>>>> MERGE-SOURCE
101 update_config_block('sql', connection="mysql://%s:%s@%s/%s" %
102 (config["database-user"],
103 relation_data["password"],
104 relation_data["db_host"],
105 config["database"]))
106+<<<<<<< TREE
107
108 if cluster.eligible_leader(CLUSTER_RES):
109 utils.juju_log('INFO',
110@@ -207,6 +282,28 @@
111 adminurl=adminurl,
112 internalurl=internalurl)
113
114+=======
115+ execute("service keystone stop", echo=True)
116+
117+ if db_uri.scheme == 'sqlite':
118+ juju_log("Migrating schema and data from sqlite to mysql")
119+ migrate_db(db_uri, relation_data)
120+ execute("service keystone start")
121+ else:
122+ execute("keystone-manage db_sync", echo=True)
123+ execute("service keystone start")
124+ time.sleep(5)
125+ ensure_initial_admin(config)
126+ # If the backend database has been switched to something new and
127+ # there are existing identity-service relations,, service entries
128+ # need to be recreated in the new database. Re-executing
129+ # identity-service-changed will do this.
130+ for id in relation_ids(relation_name='identity-service'):
131+ for unit in relation_list(relation_id=id):
132+ juju_log("Re-exec'ing identity-service-changed for: %s - %s" %
133+ (id, unit))
134+ identity_changed(relation_id=id, remote_unit=unit)
135+>>>>>>> MERGE-SOURCE
136
137 def identity_joined():
138 """ Do nothing until we get information about requested service """
139@@ -552,6 +649,7 @@
140
141 hooks = {
142 "install": install_hook,
143+ "upgrade-charm": upgrade_charm,
144 "shared-db-relation-joined": db_joined,
145 "shared-db-relation-changed": db_changed,
146 "identity-service-relation-joined": identity_joined,
147
148=== added symlink 'hooks/upgrade-charm'
149=== target is u'keystone-hooks'
150=== renamed symlink 'hooks/upgrade-charm' => 'hooks/upgrade-charm.moved'

Subscribers

People subscribed via source and target branches