Migration in Drupal - Part 1

Migration modules in Drupal 8 are still designated as experimental. Changes continue and documentation is confusing and contradictory. One aspect of configuring a migration is considered, simplified and security improved.

Migration configuration

Configuring the migration begins with

drush migrate-upgrade --legacy-db-url=mysql://user:password@server/db --legacy-root=http://mydrupal7site.com --configure-only

Passwords appearing on the command line are a poor security practice. Anything on the command line appears in history, system tools (e.g. top) and system logs. For instance it is better security practice to use

mysql -u user -p

and then enter the password when prompted than to put the password in the command line. After having to type this command many times when trying to work through bugs in migration I ran across instructions for using settings.php which seems like a much better approach. These instructions suggest adding the following to settings.php

// Database entry for `drush migrate-upgrade --configure-only`
$databases['upgrade']['default'] = array (
  'database' => 'dbname',
  'username' => 'dbuser',
  'password' => 'dbpass',
  'prefix' => '',
  'host' => 'localhost',
  'port' => '3306',
  'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
  'driver' => 'mysql',
);
// Database entry for `drush migrate-import --all`
$databases['migrate']['default'] = array (
  'database' => 'dbname',
  'username' => 'dbuser',
  'password' => 'dbpass',
  'prefix' => '',
  'host' => 'localhost',
  'port' => '3306',
  'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
  'driver' => 'mysql',
);

But when running "drush migrate-status" the following error message appears.

Driver not specified for this database connection: drupal_7

I tried adding drupal_7 to settings.php

$databases['drupal_7']['default'] = array (
  'database' => 'dbname',
  'username' => 'dbuser',
  'password' => 'dbpass',
  'prefix' => '',
  'host' => 'localhost',
  'port' => '3306',
  'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
  'driver' => 'mysql',
);

Sadly, this does not fix the error. Debugging this I found

Array
(
    [key] => drupal_7
    [database] => Array
        (
            [driver] => 
            [username] => 
            [password] => 
            [host] => 
            [port] => 
            [database] => 
            [prefix] => 
        )

    ...
)

Mike Ryan's comment about not using settings.php suggests this approach does not work anymore. Looking in MigrateUpgradeDrushRunner.php the problem is located.

  public function configure() {
    $db_url = drush_get_option('legacy-db-url');
    $db_spec = drush_convert_db_from_db_url($db_url);
    $db_prefix = drush_get_option('legacy-db-prefix');
    $db_spec['prefix'] = $db_prefix;

    $connection = $this->getConnection($db_spec);

The code gets the connection parameters from the command line and any support for getting it from settings.php is no longer there.

Securing migration

It only takes a few lines of code to fix the problem and avoid having credentials appear on the command line. Create a new custom_migration module. The module information file, custom_migration.info.yml, is just a few lines.

name: Custom Migration
type: module
description: Customizations for migration.
package: Migration
core: 8.x

The Drush command will need to be modified to call our code. Create the file custom_migration.drush.inc.


/**
 * @file
 * Customization of command line tools for migrations.
 */

use Drupal\Core\Database\Database;
use Drupal\custom_migration\CustomMigrationDrushRunner;

/**
 * Implements hook_drush_command_alter().
 */
function custom_migration_drush_command_alter(&$command) {
  if (($command['command'] == 'migrate-upgrade') && (drush_drupal_major_version() >= 8)) {
    $command['callback'] = 'custom_migration_migrate_upgrade';
  }

}

/**
 * Migrate upgrade callback.
 */
function custom_migration_migrate_upgrade() {
  $connection = Database::getConnectionInfoAsUrl('drupal_7');
  $runner = new CustomMigrationDrushRunner();

  try {
    $runner->configure('drupal_7', $connection);
    if (drush_get_option('configure-only')) {
      $runner->export();
    }
    else {
      $runner->import();
      \Drupal::state()->set('migrate_drupal_ui.performed', REQUEST_TIME);
    }
    // Remove the global database state.
    \Drupal::state()->delete('migrate.fallback_state_key');
  }
  catch (\Exception $e) {
    drush_log($e->getMessage(), 'error');
  }
}

The Drush command is altered to call the new function that will call our code. Here a callback is used instead of a hook because the hook was not getting called for some reason. But callbacks are deprecated for most situations. The key from the error statement is used but it may be different for other migrations. The key also matches the code added to the settings.php. The class CustomMigrationDrushRunner is located in the file src/CustomMigrationDrushRunner.php. Only the configure member function needs to change so we extend the original class to get the rest of the functionality.

namespace Drupal\custom_migration;

use Drupal\migrate_upgrade\MigrateUpgradeDrushRunner;

/**
 * Migrate upgrade Drush funtionality.
 */
class CustomMigrationDrushRunner extends MigrateUpgradeDrushRunner {

  /**
   * Instantiate the appropriate migrations in the active configuration.
   *
   * @throws \Exception
   */
  public function configure($db_prefix = NULL, $db_url = NULL) {
    if (is_null($db_prefix)) {
      $db_prefix = drush_get_option('legacy-db-prefix');
    }
    if (is_null($db_url)) {
      $db_url = drush_get_option('legacy-db-url');
    }

    $db_spec = drush_convert_db_from_db_url($db_url);
    $db_spec['prefix'] = $db_prefix;

    $connection = $this->getConnection($db_spec);
    $this->version = $this->getLegacyDrupalVersion($connection);
    $this->createDatabaseStateSettings($db_spec, $this->version);
    $this->databaseStateKey = 'migrate_drupal_' . $this->version;
    $migrations = $this->getMigrations($this->databaseStateKey, $this->version);
    $this->migrationList = [];
    foreach ($migrations as $migration) {
      $destination = $migration->get('destination');
      if ($destination['plugin'] === 'entity:file') {
        // Make sure we have a single trailing slash.
        $source_base_path = rtrim(drush_get_option('legacy-root'), '/') . '/';
        $destination['source_base_path'] = $source_base_path;
        $migration->set('destination', $destination);
      }
      $this->migrationList[$migration->id()] = $migration;
    }
  }

}

Two arguments are added to the configure function for the database credentials. If they are not supplied then we try to get the information from the command line. Now we can use the following commands to configure the migration and get the initial status.

drush migrate-upgrade --legacy-root=http://mydrupal7site.com --configure-only
drush migrate-status

In part 2 we look at how to handle legacy root.

Categories