]> scripts.mit.edu Git - wizard.git/blob - doc/repository-conversion.rst
Finish repository conversion docs, move re_define to app.php.
[wizard.git] / doc / repository-conversion.rst
1 Repository conversion
2 =====================
3
4 .. highlight:: sh
5
6 One of Wizard's goals is to replace the previous autoinstaller infrastructure.
7 Pre-wizard autoinstalls live in :file:`/mit/scripts/deploy` and consist of a
8 tarball from upstream, possibly a scripts patch, and possibly some post-install
9 munging (such as the creation of a :file:`php.ini` file and appropriate
10 symlinks).
11
12 Conversion to use Wizard involves placing :term:`pristine` versions of the source
13 code (from the upstream tarballs) and appropriately patched scripts
14 versions into a Git repository, as well as writing a :mod:`wizard.app`
15 module for the application that implements application specific logic, such
16 as how to install, upgrade or backup the installation.
17
18 Here is a tutorial for performing a conversion, using Wordpress as
19 an example.
20
21 Setup
22 -----
23
24 Probably the easiest way to do development is entirely on :term:`AFS`:  all
25 of your source code should live in publically readable (i.e.
26 ``system:anyuser`` as read permissions) directories, so that if you
27 SSH into a scripts server to perform testing, you will be able
28 to invoke your tools and read your development repository.  In order
29 to be able to run the test scripts in the tests directory, this
30 is preferably in :file:`web_scripts`. In that
31 case, setup is as simple as::
32
33     git clone /mit/scripts/git/wizard.git /mit/$USER/web_scripts/wizard
34     athrun consult fsr /mit/$USER/web_scripts/wizard system:anyuser read
35     # for any application you're going to do development on, also:
36     git clone /mit/scripts/git/autoinstalls/$APP.git /mit/$USER/web_scripts/wizard/srv/$APP
37
38 If you'd like to be able to develop offline, just note that you will
39 have to push your changes to AFS once you start doing testing on
40 scripts servers, but before your changes get incorporated into
41 canonical upstream.  Git doesn't exactly make this easy, but you
42 can follow this recipe::
43
44     git clone /mit/scripts/git/wizard.git ~/wizard
45     cd /mit/$USER
46     mkdir wizard.git
47     cd wizard.git
48     git init --bare
49     cd ~/wizard
50     git remote add afs /mit/$USER/wizard.git
51     git push -f afs master
52     git clone /mit/$USER/wizard.git /mit/$USER/wizard
53
54 And then you can perform updates from your local copy with::
55
56     git push afs master
57     cd /mit/$USER/wizard
58     git pull
59
60 If :file:`/mit/$USER/wizard.git` has write permissions for
61 ``system:scripts-security-upd``, this is especially useful if you were hacking
62 on a copy living on ``not-backward.mit.edu``, and now need to transfer the
63 changes back to the canonical repository (please don't give ``not-backward.mit.edu``
64 your root tickets!)  You can also setup a wizard directory similar to the
65 first set of directions for on-server testing.
66
67 From this point on, we will assume you are doing development from an AFS directory
68 named ``$WIZARD``; note that application repositories live in ``$WIZARD/srv``.
69
70 Pristine
71 --------
72
73 This is a tutorial centered around migrating `Wordpress <http://wordpress.org/>`_
74 into our Git repository.  For the sake of demonstration,
75 we shall assume that this repository hasn't been created yet.
76 The repository then doesn't exist, we should create it::
77
78     cd "$WIZARD/srv"
79     mkdir wordpress
80     cd wordpress
81     git init
82
83 We also have to create a module for the application, so we
84 create ``$WIZARD/wizard/app/wordpress.py`` and fill it in with a bare bones template:
85
86 .. code-block:: python
87
88     import os
89     import re
90     import logging
91
92     from wizard import app, install, resolve, sql, util
93     from wizard.app import php
94
95     class Application(app.Application):
96         pass
97
98 Now we are ready to put some code in our repository: the first thing we will
99 add is the :term:`pristine` branch, which contains verbatim the code from upstream.
100 If we were starting a new autoinstaller, we'd pop off and use the latest version,
101 but since we're dealing with legacy we want to start our repository history
102 with the **oldest** version still extant on our servers.  To find this out run::
103
104     wizard summary version APP
105
106 You'll need to be in the ``scripts-team`` list in order to access the data to
107 run this command.
108
109 Try running the following command in :file:`$WIZARD/srv/wordpress`::
110
111     wizard prepare-pristine wordpress-2.0.2
112
113 You should get an error complaining about :meth:`wizard.app.Application.download`
114 not being implemented yet. Let's fix that:
115
116 .. code-block:: python
117
118     class Application(app.Application):
119         def download(self, version):
120             return "http://wordpress.org/wordpress-%s.tar.gz" % version
121
122 We determined this by finding `Wordpress's Release Archive <http://wordpress.org/download/release-archive/>`_
123 and inferring the naming scheme by inspecting various links.  You should now
124 be able to run the prepare-pristine command successfully: when it is
125 done, you'll now have a bunch of files in your repository, and they
126 will be ready to be committed.  Inspect the files and commit (note that the
127 format of the commit message is a plain Appname Version.Number)::
128
129     git status
130     git commit -asm "Wordpress 2.0.2"
131     git tag wordpress-2.0.2
132
133 .. note::
134
135     Sometimes, ``http://wordpress.org/wordpress-2.0.2.tar.gz`` won't
136     actually exist anymore (it didn't exist when we did it).  In this case,
137     you'll probably be able to find the original tarball in
138     :file:`/mit/scripts/deploy/wordpress-2.0.2`, and you can feed it
139     manually to prepare pristine with
140     ``wizard prepare-pristine /mit/scripts/deploy/wordpress-2.0.2/wordpress-2.0.2.tar.gz``
141
142 Some last house-keeping bits:  now that you have a commit in a repository, you
143 can also create a pristine branch::
144
145     git branch pristine
146
147 From the point of view of the pristine branch pointer, history should be a
148 straight-forward progression of versions.  Once you have more versions,
149 it will look something like:
150
151 .. digraph:: pristine_dag
152
153     rankdir=LR
154     node [shape=square]
155     subgraph cluster_pristine {
156         a -> b -> c
157         a [label="1.0"]
158         b [label="1.1"]
159         c [label="2.0"]
160         label = "pristine"
161         color = white
162         labeljust = r
163     }
164
165 Scriptsify
166 ----------
167
168 In a perfect world, the pristine version would be equivalent to the scriptsified
169 version that would actually get deployed.  However, we have historically needed
170 to apply patches and add extra configuration files to get applications to
171 work correctly.  Due to the way Git's merge algorithm works, the closer we are
172 able to reconstruct a version of the application that was actually used, the
173 better off we will be when we try to subsequently upgrade those applications.
174
175 To give you an idea of what the Git history graph will look like when we
176 are done, here is the graph from before, but augmented with the scripts versions:
177
178 .. digraph:: master_dag
179
180     rankdir=LR
181     node [shape=square]
182     subgraph cluster_pristine {
183         a -> b -> c
184         a [label="1.0"]
185         b [label="1.1"]
186         c [label="2.0"]
187         label = "pristine"
188         color = white
189         labeljust = r
190     }
191     subgraph cluster_master {
192         as -> bs -> cs
193         as [label="1.0-scripts"]
194         bs [label="1.1-scripts"]
195         cs [label="2.0-scripts"]
196         label = "master"
197         color = white
198         labeljust = r
199     }
200     a -> as
201     b -> bs
202     c -> cs
203     a []
204
205 First things first: verify that we are on the master branch::
206
207     git checkout master
208
209 Check for pre-existing patches in the old application directory,
210 :file:`/mit/scripts/deploy/wordpress-2.0.2` in the case of Wordpress,
211 and apply them::
212
213     patch -n0 < /mit/scripts/deploy/wordpress-2.0.2/wordpress.patch
214
215 Then, run the following command to setup a :file:`.scripts` directory::
216
217     wizard prepare-new
218
219 This directory holds Wizard related files, and is also used by
220 :command:`parallel-find.pl` to determine if a directory is an autoinstall.
221
222 Finally, if you are running a PHP application, you'll need to setup
223 a :file:`php.ini` and symlinks to it in all subdirectories::
224
225     cp /mit/scripts/deploy/php.ini/wordpress php.ini
226     athrun scripts fix-php-ini
227
228 .. note::
229
230     As of November 2009, all PHP applications load the same :file:`php.ini` file.
231
232 Now commit, but don't get too attached to your commit; we're going
233 to be heavily modifying it soon::
234
235     git commit -asm "Wordpress 2.0.2-scripts"
236
237 Installation
238 ------------
239
240 We now need to make it possible for a user to install the application.
241 Most web applications have a number of web scripts for generating a
242 configuration file, so creating the install script involves:
243
244     1. Determining what input values you will need from the user, such
245        as a title for the new application or database credentials; more
246        on this shortly.
247
248     2. Determining what POST values need to be sent to what URLs.
249        Since you're converting a repository, this job is even simpler: you just
250        need to port the Perl script that was originally used into Python.
251
252 There's an in-depth explanation of named input values in
253 :mod:`wizard.install`.  The short version is that your application
254 contains a class-wide :data:`~wizard.app.Application.install_schema`
255 attribute that encodes this information.  Instantiate it with
256 :class:`wizard.install.ArgSchema` (passing in arguments to get
257 some pre-canned input values), and then add application specific
258 arguments by passing instances of :class:`wizard.install.Arg`
259 to the method :meth:`~wizard.install.ArgSchema.add`.  Usually you should
260 be able to get away with pre-canned attributes.  You can access
261 these arguments inside :meth:`~wizard.app.Application.install` via
262 the ``options`` value.
263
264 Some tips and tricks for writing :meth:`wizard.app.Application.install`:
265
266     * Some configuration file generators will get unhappy if the
267       target directory is not chmod'ed to be writable; dropping
268       in a ``os.chmod(dir, 0777)`` and then undoing the chmod
269       when you're done is a decent workaround.
270
271     * :func:`wizard.install.fetch` is the standard workhorse for making
272       requests to applications.  It accepts three parameters; the first
273       is ``options`` (which was the third argument to ``install`` itself),
274       the second is the page to query, relative to the installation's
275       web root, and ``post`` is a dictionary of keys to values to POST.
276
277     * You should log any web page output using :func:`logging.debug`.
278
279     * If you need to manually manipulate the database afterwards, you
280       can use :func:`wizard.sql.mysql_connect` (passing it ``options``)
281       to get a `SQLAlchemy metadata object
282       <http://www.sqlalchemy.org/docs/05/sqlexpression.html>`_, which can
283       consequently be queried.  For convenience, we've bound metadata
284       to the connection, you can perform implicit execution.
285
286 .. todo::
287
288     Our installer needs to also parametrize :file:`php.ini`, which we haven't
289     done yet.
290
291 To test if your installation function works, it's probably convenient to
292 create a test script in :file:`tests`; :file:`tests/test-install-wordpress.sh`
293 in the case of Wordpress.  It will look something like::
294
295     #!/bin/bash -e
296
297     DEFAULT_HEAD=1
298     TESTNAME="install_wordpress"
299     source ./setup
300
301     wizard install "wordpress-$VERSION-scripts" "$TESTDIR" --non-interactive -- --title="My Blog"
302
303 ``DEFAULT_HEAD=1`` indicates that this script can perform a reasonable
304 operation without any version specified (since we haven't tagged any of our
305 commits yet, we can't use the specific version functionality; not that we'd want
306 to, though).  ``TESTNAME`` is simply the name of the file with the leading
307 ``test-`` stripped and dashes converted to underscores.  Run the script with
308 verbose debugging information by using::
309
310     env WIZARD_DEBUG=1 ./test-install-wordpress.sh
311
312 Versioning config
313 -----------------
314
315 A design decision that was made early on during Wizard's development was that
316 the scriptsified versions would contain generic copies of the configuration
317 files.  You're going to generate this generic copy now and in doing so,
318 overload your previous scripts commit.   Because some installers
319 exhibit different behavior depending on server configuration, you should run
320 the installation on a Scripts server::
321
322     wizard install wordpress --no-commit
323
324 The installer will interactively prompt you for some values, and then conclude
325 the install.  ``--no-commit`` prevents the installer from generating a Git
326 commit after the install, and will make it easier for us to propagate the
327 change back to the parent repository.
328
329 Look at the changes the installer made::
330
331     git status
332
333 There are probably now a few unversioned files lounging around; these are probably
334 the configuration files that the installer generated.
335
336 You will now need to implement the following data attributes and methods in your
337 :class:`~wizard.app.Application` class: :attr:`~wizard.app.Application.extractors`,
338 :attr:`~wizard.app.Application.substitutions`, :attr:`~wizard.app.Application.parametrized_files`,
339 :meth:`~wizard.app.Application.checkConfig` and :meth:`~wizard.app.Application.detectVersion`.
340 These are all closely related to the configuration files that the installer generated.
341
342 :meth:`~wizard.app.Application.checkConfig` is the most straightforward method to
343 write: you will usually only need to test for the existence of the configuration file.
344 Note that this function will always be called with the current working directory being
345 the deployment, so you can simplify your code accordingly:
346
347 .. code-block:: python
348
349     class Application(app.Application):
350         # ...
351         def checkConfig(self, deployment):
352             return os.path.isfile("wp-config.php")
353
354 :meth:`~wizard.app.Application.detectVersion` should detect the version of the application
355 by regexing it out of a source file.  We first have to figure out where the version number
356 is stored: a quick grep tells us that it's in :file:`wp-includes/version.php`:
357
358 .. code-block:: php
359
360     <?php
361
362     // This just holds the version number, in a separate file so we can bump it without cluttering the SVN
363
364     $wp_version = '2.0.4';
365     $wp_db_version = 3440;
366
367     ?>
368
369 We could now grab the :mod:`re` module and start constructing a regex to grab ``2.0.4``, but it
370 turns out this isn't necessary: :meth:`wizard.app.php.re_var` does this for us already!
371 If such a module doesn't exist, you should create it and place an equivalent ``re_var()``
372 function for that language there.
373
374 With this function in hand, writing a version detection function is pretty straightforward.
375 There is one gotcha: the value that ``re_var`` returns as the second subpattern is quoted (the reasons for this
376 will become clear shortly), so you will need to trim off the last and first characters or
377 use :mod:`shlex`.  In the case of version numbers, there are probably no escape characters
378 in the string, so the former is relatively safe.
379
380 .. code-block:: python
381
382     class Application(app.Application):
383         # ...
384         def detectVersion(self, deployment):
385             contents = open("wp-includes/version.php").read()
386             match = php.re_var("wp_version").search(contents)
387             if not match: return None
388             return distutils.version.LooseVersion(match.group(2)[1:-1])
389
390 :attr:`~wizard.app.Application.parametrized_files` is a simple list of files that the
391 program's installer wrote or touched during the installation process.
392
393 .. code-block:: python
394
395     class Application(app.Application):
396         # ...
397         parametrized_files = ['wp-config.php']
398
399 This is actually is a lie: we also need to include changes to :file:`php.ini` that
400 we made:
401
402 .. code-block:: python
403
404     class Application(app.Application):
405         # ...
406         parametrized_files = ['wp-config.php'] + php.parametrized_files
407
408 And finally, we have :attr:`~wizard.app.Application.extractors` and
409 :attr:`~wizard.app.Application.substitutions`.  At the bare metal, these
410 are simply dictionaries of variable names to functions: when you call the
411 function, it performs either an extraction or a substitution.  However, we can
412 use higher-level constructs to generate these functions for us.
413
414 The magic sauce is a data structure we'll refer to as ``seed``.  Its form is
415 a dictionary of variable names to a tuple ``(filename, regular expression)``.
416 If we manually coded one out, it might look like:
417
418 .. code-block:: python
419
420     seed = {
421         'WIZARD_DBSERVER': ('wp-config.php', re.compile(r'''^(define\('DB_HOST', )(.*)(\))''', re.M)),
422         'WIZARD_DBNAME': ('wp-config.php', re.compile(r'''^(define\('DB_NAME', )(.*)(\)), re.M)),
423     }
424
425 There's a lot of duplication, though.  For one thing, the regular expressions are almost
426 identical, safe for a single substitution within the string.  We have a function
427 :meth:`wizard.app.php.re_define` that does this for us:
428
429 .. code-block:: python
430
431     seed = {
432         'WIZARD_DBSERVER': ('wp-config.php', php.re_define('DB_HOST')),
433         'WIZARD_DBNAME': ('wp-config.php', php.re_define('DB_NAME')),
434     }
435
436 .. note::
437
438     If you find yourself needing to define a custom regular expression generation function,
439     be sure to use :func:`wizard.app.expand_re`, which will escape an incoming variable
440     to be safe for inclusion in a regular expression, and also let you pass a list,
441     and have correct behavior.  Check out :mod:`wizard.app.php` for some examples.
442
443 We can shorten this even further: in most cases, all of the configuration values live in
444 one file, so let's make ourselves a function that generates the whole tuple:
445
446 .. code-block:: python
447
448     def make_filename_regex_define(var):
449         return 'wp-config.php', php.re_define(var)
450
451 Then we can use :func:`util.dictmap` to apply this:
452
453 .. code-block:: python
454
455     seed = util.dictmap(make_filename_regex_define, {
456         'WIZARD_DBSERVER': 'DB_HOST',
457         'WIZARD_DBNAME': 'DB_NAME',
458         'WIZARD_DBUSER': 'DB_USER',
459         'WIZARD_DBPASSWORD': 'DB_PASSWORD',
460     })
461
462 Short and sweet.  From there, setting up :attr:`~wizard.app.Application.extractors` and
463 :attr:`~wizard.app.Application.substitutions` is easy:
464
465 .. code-block:: python
466
467     class Application(app.Application):
468         # ...
469         extractors = app.make_extractors(seed)
470         extractors.update(php.extractors)
471         substitutions = app.make_substitutions(seed)
472         substitutions.update(php.substitutions)
473
474 Note how we combine our own dictionaries with the dictionaries of :mod:`wizard.app.php`, much like
475 we did for :attr:`~wizard.app.Application.parametrized_files`.
476
477 With all of these pieces in place, run the following command::
478
479     wizard prepare-config
480
481 If everything is working, when you open up the configuration files, any user-specific
482 variables should have been replaced by ``WIZARD_FOOBAR`` variables.  If not, check
483 your regular expressions, and then try running the command again.
484
485 When you are satisfied with your changes, add your files, amend your previous
486 commit with these changes and force them back into the public repository::
487
488     git status
489     git add wp-config.php
490     git commit --amend -a
491     git push --force
492
493 Ending ceremonies
494 -----------------
495
496 Congratulations!  You have just implemented the installation code for a new install.
497 If you have other copies of the application checked out, you can pull the forced
498 change by doing::
499
500     git reset --hard HEAD~
501     git pull
502
503 One last thing to do: after you are sure that your commit is correct, tag the new
504 commit as ``appname-x.y.z-scripts``, or in this specific case::
505
506     git tag wordpress-2.0.4-scripts
507     git push --tags