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