provision.drush.inc 18.8 KB
Newer Older
1 2
<?php
/**
3
 * @file
4
 * Provision Drush commands.
5 6 7
 *
 *
 * This module provides a framework for a Drupal site to manage and install new Drupal sites, using the command line
8
 * Drush utility.
9 10 11 12 13
 *
 * It allows for pluggable 'provisioning modules' that can extend and modify the tasks that are taken during installation.
 *
 * Each site has the following commands that can be run on it.
 *
14
 * Implemented :
15 16
 *   install - Install a new Drupal site. The install command uses 3 separate hooks to do its job,
 *             namely hook_pre_provision_install(), hook_provision_install() and hook_post_provision_install().
17 18 19 20 21 22 23 24 25
 *   verify  - Recreate all configuration files, to be in synch with changes in the front end. And test that they are correct.
 *   stats   - Return an associated array of site statistics. (implemented in provision_stats module, is thus optional)
 *   import  - Import the details of an already existing site into the provisioning framework.
 *             This command inspects the settings.php and generates the site.php file that the framework uses for configuration.
 *   backup  - Generates a tarball containing the sites directory, the site data configuration and the database dump.
 *             This allows the tarball to act as a 'site package', which can be redeployed on other installations,
 *             or used for an upgrade.
 *   disable - Disable an installed Drupal site. Changes the virtual host config file so that it redirects to provision_disabled_site_redirect_url
 *   enable  - Re-enable a site that has already been disabled. Recreates the virtual host file.
26 27
 *   delete  - In a site context: generates a back up of the site, and then removes all references to it.
 *             In a platform context: removes the platform and its vhost config from the server if no sites are currently running on it
28 29
 *   restore - Revert to a previous backup of the site.
 *
30
 *   deploy  - Accepts a site package (backup) as argument, and redeploys it, running the upgrade processes on it.
31 32 33
 *             Uses hook_provision_pre_upgrade(), hook_provision_upgrade() and hook_provision_post_upgrade() hooks,
 *             and allows clean roll back if any errors occur. Will include stringent checking of module versions,
 *             and allow unit tests to be run.
34 35 36 37
 *   lock    - Lock a platform so that sites cannot be provisioned on it. This does not disable or delete the platform
 *             nor any sites currently provisioned on it.
 *   unlock  - Unlock a platform so that sites can be provisioned on it.
 *
38
 *   login-reset - Generate a one-time login reset URL.
39 40
 */

41

42 43 44 45
/**
 * @defgroup provisiondrush Command line interface for Provision.
 * @{
 */
46
include_once('provision.inc');
47

48
/**
49
 * Implements hook_drush_init().
50 51
 */
function provision_drush_init() {
52 53 54 55 56
  // try to load the drush siterecoard (the "alias") into d
  // if a name is provided, it's because we're in provision-save so we
  // need to specify it because it's not loaded by drush
  $hash_name = drush_get_option('#name') ? '#name' : 'name';
  d(drush_get_option($hash_name, '@self', 'alias'), TRUE);
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
  // Make sure that the provision command is not being run as the root user.
  _provision_drush_check_user();
  // Abort the process if the load is too high.
  _provision_drush_check_load();
}

/**
 * This will abort any process running drush provision commands if the
 * user running the command is root.
 */
function _provision_drush_check_user() {
  $command = drush_get_command();
  $name = posix_getpwuid(posix_geteuid());
  if (preg_match("/^provision-\b/", $command['command']) && $name['name'] == 'root') {
    return drush_set_error('PROVISION_IS_ROOT', dt('You are running the provision script as the root user. Exiting'));
  }
}

/**
76 77 78 79
 * This will abort any process running drush if the load is critical.
 *
 * @see provision_load_critical()
 */
80
function _provision_drush_check_load() {
81 82
  $load = sys_getloadavg();
  if (provision_load_critical($load)) {
83
    drush_set_error('PROVISION_OVERLOAD', dt("load on system too heavy (@load), aborting", array('@load' => join(" ", $load))));
84 85 86 87
    exit(1);
  }
}

88 89 90 91
/**
 * Implementation of hook_drush_command().
 */
function provision_drush_command() {
92
  $items['provision-save'] = array(
drumm's avatar
drumm committed
93 94 95 96 97
    'description' => dt('Save Drush alias'),
    'arguments' => array(
      '@context_name' => 'Context to save',
    ),
    'options' => array_merge(array(
98
      'context_type' => 'server, platform, or site; default server',
ergonlogic's avatar
ergonlogic committed
99 100 101 102
      'delete' => 'Remove the alias.'),
      Provision_Context_server::option_documentation(),
      Provision_Context_platform::option_documentation(),
      Provision_Context_site::option_documentation()),
103 104 105
    // we should populate from all known contexts, unfortunately we don't
    // enumerate them yet... see https://drupal.org/node/1972286
    'allow-additional-options' => TRUE,
106 107
    'bootstrap' => DRUSH_BOOTSTRAP_DRUSH,
  );
108
  $items['provision-install'] = array(
109
    'description' => dt('Provision a new site using the provided data.'),
110 111 112
    'examples' => array(
      'drush @site provision-install' => 'Install the site as defined by the site Drush alias generated with provision-save.',
    ),
113 114
    'options' => array(
      'client_email' => dt('The email address of the client to use.'),
115
      'profile' => dt('The profile to use when installing the site.'),
116
    ),
117
    'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_ROOT
118 119
  );

120 121
  $items['provision-install-backend'] = array(
    'description' => dt('Provision a new site using the provided data.'),
122 123 124
    'options' => array(
      'client_email' => dt('The email address of the client to use.'),
    ),
125 126 127 128
    'hidden' => TRUE,
    'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_SITE
  );

129
  $items['provision-import'] = array(
130
    'description' => dt('Turn an already running site into a provisioned site.'),
131 132 133
    'options' => array(
      'client_email' => dt('The email address of the client to use.'),
    ),
134 135 136
    'examples' => array(
      'drush @site provision-import' => 'Import the site as defined by the site Drush alias generated with provision-save.',
    ),
137
    'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_ROOT
138 139
  );

140
  $items['provision-backup'] = array(
141
    'optional arguments' => array('backup-file' => dt('The file to save the backup to. This will be a gzipped tarball.')),
142
    'description' => dt('Generate a back up for the site.'),
143 144
    'options' => array(
      'profile' => dt('The Drupal profile to use.')),
145 146 147
    'examples' => array(
      'drush @site provision-backup' => 'Back up the site as defined by the site Drush alias generated with provision-save.',
    ),
148
    'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_ROOT
149
  );
150

151
  $items['provision-enable'] = array(
152
    'description' => 'Enable a disabled site.',
153 154 155
    'examples' => array(
      'drush @site provision-enable' => 'Enable the site as defined by the site Drush alias generated with provision-save.',
    ),
156
    'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_ROOT
157
  );
158
  $items['provision-disable'] = array(
159
    'arguments' => array('domain.com' => dt('The domain of the site to disable (only if disabled).')),
160
    'description' => 'Disable a site.',
161 162 163
    'examples' => array(
      'drush @site provision-disable' => 'Disable the site as defined by the site Drush alias generated with provision-save.',
    ),
164
    'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_ROOT
165 166
  );

167 168
  $items['provision-lock'] = array(
    'description' => 'Lock a platform from having any other sites provisioned on it.',
169 170 171
    'examples' => array(
      'drush @platform provision-lock' => 'Lock the platform as defined by the platform Drush alias generated with provision-save.',
    ),
172 173 174 175 176
    'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_ROOT
  );

  $items['provision-unlock'] = array(
    'description' => 'Unlock a platform so that sites can be provisioned on it.',
177 178 179
    'examples' => array(
      'drush @platform provision-unlock' => 'Unlock the platform as defined by the platform Drush alias generated with provision-save.',
    ),
180 181 182
    'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_ROOT
  );

183
  $items['provision-verify'] = array(
184
    'arguments' => array('domain.com' => dt('The domain of the site to verify).')),
185
    'description' => 'Verify that the provisioning framework is correctly installed.',
186 187 188 189
    'examples' => array(
      'drush @site provision-verify' => 'Verify the site as defined by the site Drush alias generated with provision-save.',
      'drush @platform provision-verify' => 'Verify the platform as defined by the platform Drush alias generated with provision-save.',
    ),
190 191 192 193 194 195
    'options' => array(
      'working-copy' => array(
        'description' => dt('Keep VCS files when building the a platform using Drush make.'),
        'hidden' => TRUE,
      ),
    ),
196
    'bootstrap' => DRUSH_BOOTSTRAP_DRUSH 
197
  );
198

199
  $items['provision-restore'] = array(
200
    'description' => 'Restore the site to a previous backup. This will also generate a backup of the site as it was.',
201
    'arguments' => array(
202
      'site_backup.tar.gz' => dt('The backup to restore the site to.')),
203 204 205
    'examples' => array(
      'drush @site provision-restore ~/backups/some_site.tar.gz' => 'Restore the site to the backup in ~/backups/some_site.tar.gz.',
    ),
206
    'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_ROOT
207 208
  );

209
  $items['provision-deploy'] = array(
210
    'description' => 'Deploy an existing backup to a new url.',
211
    'arguments' => array(
212
      'site_backup.tar.gz' => dt('The backup to deploy.')),
213 214 215
    'options' => array(
      'old_uri' => dt('Old site uri to replace in references to sites/example.com/files/ in the database content.)'),
    ),
216 217 218
    'examples' => array(
      'drush @site provision-deploy ~/backups/some_site.tar.gz' => 'Deploy the site as defined by the site Drush alias, from the backup in ~/backups/some_site.tar.gz.',
    ),
219
    'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_ROOT
220
  );
221

222
  $items['provision-migrate'] = array(
223
    'description' => 'Migrate a site between platforms.',
224 225
    'arguments' => array(
      '@platform_name' => dt('The Drush alias of the platform.')),
226 227
    'options' => array(
      'profile' => dt('The Drupal profile to use.')),
228 229 230
    'examples' => array(
      'drush @site provision-migrate @platform_name' => 'Migrate the site as defined by the Drush alias, to the platform as defined by the platform\'s Drush alias',
    ),
231
    'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_ROOT
232
  );
233

234
  $items['provision-clone'] = array(
235
    'description' => 'Clone a site between platforms.',
236 237 238
    'arguments' => array(
      '@new_site' => dt('The Drush alias of the new site as generated by provision-save.'),
      '@platform_name' => dt('The Drush alias of the platform to clone the site onto.')),
239 240
    'options' => array(
      'profile' => dt('The Drupal profile to use.')),
241 242 243
    'examples' => array(
      'drush @site provision-clone @new_site @platform_name' => 'Clone the original site to the new site on a platform',
    ),
244 245
    'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_ROOT
  );
246

247
  $items['provision-delete'] = array(
248
    'description' => 'Delete a site.',
249 250 251
    'examples' => array(
      'drush @site provision-delete' => 'Delete the site as defined by the site Drush alias generated with provision-save.',
    ),
252
    'bootstrap' => DRUSH_BOOTSTRAP_DRUSH
253 254
  );

255
  $items['provision-login-reset'] = array(
256
    'description' => 'Generate a one-time login reset URL.',
257
    'examples' => array(
258
      'drush @site provision-login-reset' => 'Generate a one-time login reset URL for the site as defined by the site Drush alias generated with provision-save.',
259
    ),
260 261
    'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_ROOT
  );
262

263
  $items['provision-backup-delete'] = array(
264 265
    'description' => 'Delete a backup file.',
    'arguments' => array('backup-file' => dt('The backup file to delete. This will be a gzipped tarball.')),
266
    'examples' => array(
267
      'drush @site provision-backup-delete /path/to/site_backup.tgz' => 'Delete a backup of this site as defined by the site Drush alias generated with provision-save.',
268
    ),
269
    'bootstrap' => DRUSH_BOOTSTRAP_DRUSH
270

271
  );
272

273
  $items['hostmaster-migrate'] = array(
274 275 276 277
    'description' => dt('Migrate an instance of the Hostmaster front end to a new platform'),
    'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_ROOT,
    'arguments' => array(
      'example.com' => dt('The name of the site to migrate'),
278
      '/path/to/platform' => dt('The platform to migrate the site to.'),
279 280
    ),
    'options' => array(
281
      'http_service_type' => dt('Webserver type to configure (default: %webserver)', array('%webserver' => 'apache')),
282
      'makefile' => dt('The makefile used to create the hostmaster platform (default: %makefile)', array('%makefile' => dirname(__FILE__). '/aegir.make')),
283 284 285
    ),
  );

286 287 288 289
  $items['hostmaster-install'] = array(
    'description' => dt('Install and verify the Hostmaster frontend.'),
    'bootstrap' => DRUSH_BOOTSTRAP_DRUSH,
    'arguments' => array(
anarcat's avatar
anarcat committed
290
      'example.com' => dt('The URL of the site to install, optional (default: %host).', array('%host' => provision_fqdn())),
291
    ),
anarcat's avatar
anarcat committed
292 293 294 295 296 297
    'options' => array
    (
     'http_service_type' => dt('Webserver type to configure (default: %webserver)', array('%webserver' => 'apache')),
     'aegir_db_host' => dt('Database host to connect to (default: %host)', array('%host' => 'localhost')),
     'aegir_db_user' => dt('Database user to connect as (default: %user)', array('%user' => 'root')),
     'aegir_db_pass' => dt('Database password to use'),
298
     'aegir_db_port' => dt('Database port to use (default: %port)', array('%port' => '3306')),
anarcat's avatar
anarcat committed
299 300 301 302 303 304
     'client_email' => dt('Email of the first client to create in the frontend'),
     'client_name' => dt('Name of the first client to create in the frontend (default: %user)', array('%user' => 'admin')),
     'makefile' => dt('The makefile used to create the hostmaster platform (default: %makefile)', array('%makefile' => dirname(__FILE__). '/aegir.make')),
     'aegir_host' => dt('Fully qualified domain name of the local server (default: %fqdn)', array('%fqdn' => provision_fqdn())),
     'script_user' => dt('User to run the backend as (default: %user)', array('%user' => provision_current_user())),
     'web_group' => dt('Group the webserver is running as (default: %group)', array('%group' => _provision_default_web_group())),
305
     'http_port' => dt('Port the webserver is running on (default: %port)', array('%port' => '80')),
anarcat's avatar
anarcat committed
306 307
     'version' => dt('The version of this released. (default: %version)', array('%version' => provision_version())),
     'aegir_root' => dt('Install aegir in this home directory (default: %home). Do not change unless you know what you are doing.', array('%home' => drush_server_home())),
308
     'root' => dt('Install the frontend in this directory (default: %home/hostmaster-%version).', array('%home' => drush_server_home(), '%version' => provision_version())),
309 310
     'backend-only' => dt('Install just the backend, and not the frontend UI.'),
     'working-copy' => dt('Keep VCS files when building the hostmaster platform using Drush make.')
anarcat's avatar
anarcat committed
311
     ),
312
  );
313

anarcat's avatar
anarcat committed
314 315 316 317 318 319 320 321 322
  $items['hostmaster-uninstall'] = array(
    'description' => dt('Uninstall the Hostmaster frontend.'),
    'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_SITE,
    'options' => array
    (
     'all' => dt('Destroy *ALL* sites managed by the Aegir frontend'),
     ),
  );

323 324 325 326 327
  $items['backend-parse'] = array(
    'description' => dt('Parse the output of --backend commands to a human readable form'),
    'bootstrap' => DRUSH_BOOTSTRAP_DRUSH,
  );

328 329 330
  return $items;
}

331 332 333
function drush_provision_save($alias = NULL) {
  if (drush_get_option('delete', FALSE)) {
    // remove an existing alias
334
    $config = new Provision_Config_Drushrc_Alias($alias);
335 336 337
    $config->unlink();
  }
  else {
338
    // trigger additional logic that should happen only on save.
339
    d($alias)->type_invoke('save');
340
    // create or update the record
341
    d($alias)->write_alias();
342
  }
drumm's avatar
drumm committed
343 344
}

345
function drush_provision_verify() {
346
  d()->command_invoke('verify');
347 348
}

349
function _provision_default_web_group() {
350
  $info = posix_getgrgid(posix_getgid());
351
  $common_groups = array(
352
    'www',
353
    'httpd',
354
    'www-data',
355
    'apache',
drumm's avatar
drumm committed
356
    'webservd',
357 358
    'nogroup',
    'nobody',
359
    $info['name']);
360 361 362 363 364 365 366

  foreach ($common_groups as $group) {
    if (provision_posix_groupname($group)) {
      return $group;
      break;
    }
  }
367
  return NULL;
368 369
}

370 371 372 373 374 375 376
/**
 * determine the number of CPU on the machine
 *
 * This tries a best guess at the number of CPUs running on the system. This is
 * useful for calculating sane load threshold.
 *
 * On Linux, this parses /proc/cpuinfo and looks for lines like this:
377
 *
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393
 * processor	: 0
 * ...
 * processor	: 1
 * processor	: n
 *
 * The number of CPUs on the system is n+1, we just count the number of lines.
 *
 * Other systems remain to be implemented, and would be best implemetend
 * through a PECL (or similar) extension that would use the POSIX sysconf
 * interface, as such:
 *
 * ncpus = sysconf(_SC_NPROCESSORS_ONLN);
 *
 * If no method can be found to figure out the number of CPUs, this will return
 * FALSE.
 *
394 395 396 397 398
 * People wishing to extend this to other platforms should look at
 * suggestions at:
 *
 * http://groups.google.com/group/sage-devel/browse_thread/thread/d65209f7ad6057fc
 *
399 400 401 402 403
 * @see provision_load_critical()
 * @todo implement for other systems than Linux
 */
function provision_count_cpus() {
  $ncpus = FALSE;
404 405 406 407 408 409 410
  if (file_exists("/proc/cpuinfo")) {
    # this should work on Linux with a /proc filesystem
    $cpuinfo = file_get_contents("/proc/cpuinfo");
    if ($cpuinfo !== FALSE) {
      if (preg_match_all("/^processor.*:.*[0-9]+$/m", $cpuinfo, $matches)) {
        $ncpus = count(array_pop($matches));
      }
411 412 413 414 415
    }
  }
  return $ncpus;
}

416 417 418
define('CRITICAL_LOAD_MULTIPLIER', 5);
define('CRITICAL_LOAD_THRESHOLD', 10);

419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
/**
 * determine if overall load of the machine is critical
 *
 * We use the "average system load" of the system as a metric, as available
 * through 'uptime' or in PHP sys_getloadavg() since 5.1. The load is usually
 * defined as "the number of processes in the system run queue"
 *
 * It's not a really reliable metric, but it's the best shot we've got without
 * getting into real specific details about I/O, CPU or memory load that are
 * going to be even tougher to evaluate.
 *
 * We base our evaluation on the number of CPUs on the servers. If there are
 * more than 5 processes waiting per CPU, we abort completely. If we ignore the
 * number of available CPUs, we assume a critical limit is a load of 10.
 *
 * @see sys_getloadavg()
 */
436
function provision_load_critical($load = NULL, $threshold = NULL) {
437 438 439 440 441
  if (is_null($load)) {
    $load = sys_getloadavg();
  }
  if (is_null($threshold)) {
    if ($ncpus = provision_count_cpus()) {
442
      $threshold = $ncpus * drush_get_option('critical_load_multiplier', CRITICAL_LOAD_MULTIPLIER);
443 444
    }
    else {
445
      // can't determine the number of CPU, we hardcode at load 10
446
      $threshold = drush_get_option('critical_load_threshold', CRITICAL_LOAD_THRESHOLD);
447 448 449 450
    }
  }
  return ($load[0] > $threshold);
}
451 452 453 454 455 456 457 458 459

/**
 * Check whether a Hosting feature is enabled.
 */
function provision_hosting_feature_enabled($feature) {
  $features = drush_get_option('hosting_features', array());
  return array_key_exists($feature, $features) && $features[$feature];
}