Managing Drupal site updates with hook_update_N()

November 4, 2010

One of the often discussed topics in the Drupal community is the site deployment process, and best practices for managing development, staging, and production environments. In this post, I am going to explain part of our approach for a particular project and how hook_update_N() made our lives a bit easier.

Background

One of the projects we recently completed for a client involved creating a Drupal distribution that could be used to deploy many similar sites. The distribution consists of a drush make file, an install profile, six Features, another half dozen custom modules, and many contrib modules. We use the Aegir hosting system to manage site deployments.

Platform deployments

Aegir uses the concept of Platforms to organize deployments of sites. mig5 has written a very nice post about this so, if you are interested to learn more about this, read mig5's write-up on deployments with Aegir. The gist of working with Aegir is that you create a Platform which contains the codebase for your site, then you create a new Site in Aegir which is an instance of that Platform.

In our case, we have a Platform that 10 sites use as their common codebase. When a new version of Drupal core, or security updates for contrib modules are released, we can simply run the command drush make ourdistro.build. This will grab the contrib modules, install profile, and custom code that we have in our SVN repository and create a new Platform. We then tell Aegir that the Platform exists, and migrate our sites to it.

Making changes to live sites

All this works fine when you are creating new Sites based on a new Platform. But what happens when you want to update some settings across all the live sites?

We could login in to each site and make the settings changes we wanted manually. But in addition to being a tedious process, it is also error-prone and not sustainable or scalable.

Using Features for updates

Another option is to use Features to manage updates to sites.

One of the Features in our distribution is a core Feature called ourdistro_core which contains a number of settings, variables, and configuration that should not change across sites.

The ourdistro_core Feature contained some permissions settings related to viewing user profiles. Originally, anonymous users and authenticated users had permissions to access user profiles. We had to change this to allow only the “administrator” role to access profiles.

The first thing we had to do was update the ourdistro_core Feature to take this change into account. Easy enough. Then we built a new Platform. New sites built on that Platform have the new settings in place, great! But what happens when we migrate an existing site on an earlier version of the platform to the new one? The changes won’t take effect, because the Feature will be overridden.

This is actually one of the best parts of Features. You can update your Feature code and push it to a site, but the changes won’t take effect immediately. Rather, you can see which parts are overridden and can optionally decide which components to revert. This is a good thing.

Our problem was that ourdistro_core had some permissions that had been modified on various sites that we did not want to revert. For example, ourdistro_core Feature included permission settings for "posting comments without approval" that had been overridden on a number of sites. If we wanted to revert the permissions in the Feature to take our new changes into account, we would lose all other customizations for the permissions.

(As a side note: one of the lessons learned from this project is that it is really important to keep your Features manageable and discreet pieces of functionality, for exactly this reason. Kit is helpful in defining the scope of a Feature. If a Feature becomes too large, updates or feature changes become difficult. If you use the same Feature across 10 sites, the problem becomes 10 times worse.)

Using hook_update_N() to update the database

The solution is to include an implementation of hook_update_N() in the ourdistro_core Feature that will update the settings for us. When we migrate a Site to a new Platform, hook_update_N() will be invoked in the ourdistro_core Feature, which will take care of making the necessary database changes for us.

Here's how we did it:

Revoke “access user profiles” permissions for some user roles

This example uses the excellent Permissions API module

function ourdistro_core_update_6113(&$sandbox) {

    $user_profiles = array('access user profiles');

    permissions_grant_permissions('administrator', $user_profiles);
    permissions_revoke_permissions('anonymous user', $user_profiles);
    permissions_revoke_permissions('authenticated user', $user_profiles);

    return array();
}

Here are some more examples:

Changing the WYSIWYG Editor from TinyMCE to CKEditor

function ourdistro_core_update_6100(&$sandbox) {
    $ret = array();

    $object->editor = 'ckeditor';
  $object->format = 1;
  $object->settings =  'a:20:{s:7:"default";i:1;s:11:"user_choose";i:0;s:11:"show_toggle";i:1;s:5:"theme";s:8:"advanced";s:8:"language";s:2:"en";s:7:"buttons";a:2:{s:7:"default";a:18:{s:4:"Bold";i:1;s:6:"Italic";i:1;s:9:"Underline";i:1;s:11:"JustifyLeft";i:1;s:13:"JustifyCenter";i:1;s:12:"JustifyRight";i:1;s:12:"BulletedList";i:1;s:12:"NumberedList";i:1;s:7:"Outdent";i:1;s:6:"Indent";i:1;s:4:"Link";i:1;s:5:"Image";i:1;s:11:"Superscript";i:1;s:9:"Subscript";i:1;s:14:"HorizontalRule";i:1;s:13:"PasteFromWord";i:1;s:6:"Format";i:1;s:5:"Table";i:1;}s:6:"drupal";a:1:{s:18:"wysiwyg_imagefield";i:1;}}s:11:"toolbar_loc";s:3:"top";s:13:"toolbar_align";s:4:"left";s:8:"path_loc";s:6:"bottom";s:8:"resizing";i:1;s:11:"verify_html";i:1;s:12:"preformatted";i:0;s:22:"convert_fonts_to_spans";i:1;s:17:"remove_linebreaks";i:1;s:23:"apply_source_formatting";i:1;s:27:"paste_auto_cleanup_on_paste";i:0;s:13:"block_formats";s:32:"p,address,pre,h2,h3,h4,h5,h6,div";s:11:"css_setting";s:4:"none";s:8:"css_path";s:0:"";s:11:"css_classes";s:0:"";}';
  drupal_write_record('wysiwyg', $object, 'format');
  $object->format = 2;
  drupal_write_record('wysiwyg', $object, 'format');

    return $ret;
}

Provide a default backup schedule for each site

function ourdistro_core_update_6101(&$sandbox) {
    $ret = array();
    $modules = module_rebuild_cache();
      if (isset($modules['backup_migrate'])) {
        if (!$modules['backup_migrate']->status) {
          drupal_install_modules(array('backup_migrate'));
                drupal_install_modules(array('backup_migrate'));
                // Create default backup schedule for backup_migrate
                $schedule->name = 'Daily backup';
                $schedule->source_id = 'db';
                $schedule->destination_id = 'scheduled';
                $schedule->profile_id = 'default';
                $schedule->keep = 7;
                $schedule->period = 86400;
                $schedule->last_run = 0;
                $schedule->enabled = 1;
                $schedule->cron = 0;
                drupal_write_record('backup_migrate_schedules', $schedule);
            }
        }
    return $ret;
}

And so on.

Conclusions

Whether you are managing a single site or a grouping of sites, hook_update_N() is invaluable in the deployment process. There are many great examples out there, so have a look and start using it, it is well worth the time and effort to learn.


Comments

hook_update_N is a very handy way to manage automated deployments. I use it for deployment on all my sites now. If you are still using this method, this module can be a real boost.
https://www.drupal.org/project/hook_update_deploy_tools
It is a framework to make the cycle more complete by adding
a) verification of what you asked it to do
b) reporting and logging of what was actually done
c) Update Exception thrown if the result was not what was expected so that the hook_update_N was not counted as run.