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