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