View Issue Details

IDProjectCategoryView StatusLast Update
556RackTablesdefaultpublic2012-07-22 01:24
Reporteruser292Assigned Toadoom42  
PrioritynormalSeverityfeatureReproducibilityN/A
Status closedResolutionfixed 
Product Version0.19.12 
Target Version0.20.0Fixed in Version0.20.0 
Summary556: Console based database upgrade
DescriptionHi,

we're currently building a capistrano based deployment process for racktables.

To upgrade the database from the console, i've developed a small php console application which can do the upgrade from the console. The application uses Composer and the Symfony2 console.

To use it:

  Install composer:
    $> curl -s http://getcomposer.org/installer | php
  Install dependencies:
    $> php composer.phar install
  Run the console:
    $> php console.php

Greets
Hannes
TagsNo tags attached.

Activities

2012-05-14 09:09

 

console_upgrade.patch (9,221 bytes)   
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..11490ce
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,6 @@
+{
+    "name": "racktables/racktables",
+    "require": {
+        "symfony/console": "v2.0.12"
+    }
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..6de8d48
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,13 @@
+{
+    "hash": "ea0ca025f3f4d8d690f0b385f2ca9bd5",
+    "packages": [
+        {
+            "package": "symfony/console",
+            "version": "v2.0.12"
+        }
+    ],
+    "packages-dev": null,
+    "aliases": [
+
+    ]
+}
diff --git a/console.php b/console.php
new file mode 100644
index 0000000..8b3299d
--- /dev/null
+++ b/console.php
@@ -0,0 +1,245 @@
+#!/usr/bin/env php
+<?php
+
+define( 'REAL_PATH', realpath(dirname(__FILE__)) );
+
+require_once REAL_PATH.'/vendor/autoload.php';
+require_once REAL_PATH.'/wwwroot/inc/pre-init.php';
+require_once REAL_PATH.'/wwwroot/inc/config.php';
+require_once REAL_PATH.'/wwwroot/inc/dictionary.php';
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class RacktablesDatabaseCommand extends Command
+{
+
+	protected function ensureDatabase(OutputInterface $output)
+	{
+		global $dbxlink;
+		try{
+			if( !$dbxlink ) connectDB();
+			return $dbxlink;
+		}catch (RackTablesError $e)
+		{
+			$output->write("<error>Database connection failed:\n\n" . $e->getMessage().'</error>');
+			exit();
+		}
+	}
+
+	protected function getDatabaseVersion(OutputInterface $output)
+	{
+		$dbxlink = $this->ensureDatabase($output);
+		$prepared = $dbxlink->prepare ('SELECT varvalue FROM Config WHERE varname = "DB_VERSION" and vartype = "string"');
+		if (! $prepared->execute())
+		{
+			$errorInfo = $dbxlink->errorInfo();
+			$output->write('<error>SQL query failed with error ' . $errorInfo[2].'</error>');
+			exit();
+		}
+		$rows = $prepared->fetchAll (PDO::FETCH_NUM);
+		unset ($result);
+		if (count ($rows) != 1 || !strlen ($rows[0][0])){
+			$output->write('<error>Cannot guess database version. Config table is present, but DB_VERSION is missing or invalid. Giving up.</error>');
+			exit();
+		}
+		$ret = $rows[0][0];
+		return $ret;
+	} 
+
+}
+
+class RacktablesBackupCommand extends RacktablesDatabaseCommand
+{
+
+	protected function configure()
+	{
+		$this
+			->setName('backup:db')
+			->setDescription('Backups the db')
+			->addOption('dry-run', null, InputOption::VALUE_NONE, 'just say what would be done')
+			->addArgument('outputfile', InputArgument::OPTIONAL, 'Where to write the backup file');
+	}
+
+	protected function execute(InputInterface $input, OutputInterface $output)
+	{
+		global $pdo_dsn, $db_username, $db_password;
+
+		$dsn = $this->parseDSN($pdo_dsn);
+		$dry_run = $input->getOption('dry-run');
+		$cmd = 'mysqldump --host='.escapeshellarg($dsn['host']).' --user='.escapeshellarg($db_username).' --password='.escapeshellarg($db_password).' '.escapeshellarg($dsn['dbname']).' ';
+
+		if( $input->getArgument('outputfile') ){
+			$file = $input->getArgument('outputfile');
+		}else{
+			if( !is_dir(REAL_PATH.'/../backups/') ) mkdir( REAL_PATH.'/../backups/' );
+			$file = REAL_PATH.'/../backups/'.strftime('%F-%T.sql');
+		}
+
+		$output->writeln( "<info>Running: $cmd</info>" );
+		$output->writeln( "<info>Output to: $file</info>" );
+		if( !$dry_run ){
+
+			$proc = proc_open($cmd, array( 0 =>array('pipe', 'r') , 1 => array('file', $file, 'w' ), 2 => array('pipe', 'w') ), $pipes, null,array());
+
+			$status = proc_get_status($proc);
+
+			while( $status['running'] ){
+				sleep(1);
+				$output->write('.');
+				$status = proc_get_status($proc);
+			}
+			if( $status['exitcode'] ){
+				$output->writeln('<error>'.stream_get_contents($pipes[2]).'</error>');
+				return $status['exitcode'];
+			}
+
+			$output->writeln( "\n<info>Done</info>" );
+		}
+	}
+
+	private function parseDSN($dsn)
+	{
+		if( preg_match( '#^mysql:#', $dsn) ){
+			$args = explode(';',substr($dsn,6));
+			$params = array();
+			foreach( $args as $arg ){
+				list($key,$value) = explode('=',$arg,2);
+				$params[$key] = $value;
+			}
+			return $params;
+		}else{
+			throw "Expected a DSN, but got: $dsn";
+		}
+	}
+
+}
+
+class RacktablesUpgradeCheckCommand extends RacktablesDatabaseCommand
+{
+	protected function configure()
+	{
+		$this
+			->setName('upgrade:check')
+			->setDescription('Checks if upgrades are necessary. Exit code is zero if no upgrade is needed and non-zero otherwise.');
+	}
+
+	protected function execute(InputInterface $input, OutputInterface $output)
+	{
+		return ( $this->getDatabaseVersion($output) == CODE_VERSION ) ? 0 : 1;
+	}
+
+}
+
+class RacktablesUpgradeDoCommand extends RacktablesDatabaseCommand
+{
+	protected function configure()
+	{
+		$this
+			->setName('upgrade:do')
+			->setDescription('Actually upgrades racktables WITHOUT BACKUP. If the take the backup yourself, this is fine. Otherwise use "upgrade".')
+			->addOption('dry-run', null, InputOption::VALUE_NONE, 'just say what would be done');
+	}
+
+	protected function execute(InputInterface $input, OutputInterface $output)
+	{
+		require './wwwroot/inc/upgrade.php';
+		$this->ensureDatabase($output);
+		return $this->upgrade($this->getDatabaseVersion($output), CODE_VERSION, $input, $output);
+	}
+
+	protected function upgrade($from, $to, InputInterface $input, OutputInterface $output )
+	{
+		global $dbxlink;
+		$dry = $input->getOption('dry-run');
+		if( $from == $to ){
+			$output->writeln('<info>Database is up-to-date. Nothing to do here.</info>');
+			return 0;
+		}
+		$failures = 0;
+		$path = getDBUpgradePath ($from, $to);
+		$output->writeln('<comment>Upgrading database ['.$from.'] -> ' . join(' -> ',$path).'</comment>' );
+		$path[]='dictionary';
+		foreach( $path as $version ){
+			$batch = getUpgradeBatch($version);
+			if( $version == 'dictionary' ){
+				$output->writeln( '<info>Updating Dictionary</info>');
+			}else{
+				$output->writeln( '<info>Upgrading to Version '.$version.'</info>');
+			}
+			$output->writeln('');
+			foreach( $batch as $query )
+			{
+				try
+				{
+					if( $output->getVerbosity() == OutputInterface::VERBOSITY_VERBOSE ){
+						$output->writeln("<comment>  $query</comment>");
+					}else{
+						$output->write(".");
+					}
+					if( !$dry ) $dbxlink->query ($query);
+				}
+				catch (PDOException $e)
+				{
+					$errorInfo = $dbxlink->errorInfo();
+					if( $output->getVerbosity() != OutputInterface::VERBOSITY_VERBOSE ){
+						$output->writeln('');
+						$output->writeln("<error>QUERY FAILED:	$query</error>");
+					}
+					$output->writeln("<error>REASON: {$errorInfo[2]}</error>");
+					$failures++;
+				}
+			}
+			$output->writeln("");
+		}
+		return $failures;
+	}
+}
+
+class RacktablesUpgradeCommand extends Command
+{
+
+	protected function configure()
+	{
+		$this
+				->setName('upgrade')
+				->setDescription('Upgrades racktables with backup.');
+	}
+
+	protected function execute(InputInterface $input, OutputInterface $output)
+	{
+		$check_command = $this->getApplication()->find('upgrade:check');
+
+		$returnCode = $check_command->run(new Symfony\Component\Console\Input\ArrayInput(array('upgrade:check')), $output);
+
+		if( !$returnCode )
+		{
+			$output->writeln('<info>Database is up-to-date.</info>');
+			return 0;
+		}
+
+		$backup_command = $this->getApplication()->find('backup:db');
+		if( !$backup_command->run(new Symfony\Component\Console\Input\ArrayInput(array('backup:db')), $output) ){
+			return 1;
+		}
+
+		$upgrade_command = $this->getApplication()->find('upgrade:do');
+		return $upgrade_command->run(new Symfony\Component\Console\Input\ArrayInput(array('upgrade:do')), $output);
+
+	}
+
+}
+
+
+use Symfony\Component\Console\Application;
+
+$application = new Application();
+$application->add(new RacktablesBackupCommand);
+$application->add(new RacktablesUpgradeCheckCommand);
+$application->add(new RacktablesUpgradeDoCommand);
+$application->add(new RacktablesUpgradeCommand);
+$application->run();
+
diff --git a/wwwroot/inc/upgrade.php b/wwwroot/inc/upgrade.php
index a923e7e..a0d2e8c 100644
--- a/wwwroot/inc/upgrade.php
+++ b/wwwroot/inc/upgrade.php
@@ -168,10 +168,10 @@ function getDBUpgradePath ($v1, $v2)
 
 // Upgrade batches are named exactly as the release where they first appear.
 // That is simple, but seems sufficient for beginning.
-function executeUpgradeBatch ($batchid)
+function getUpgradeBatch ($batchid)
 {
 	$query = array();
-	global $dbxlink;
+
 	switch ($batchid)
 	{
 		case '0.16.5':
@@ -1253,9 +1253,18 @@ CREATE TABLE `CactiGraph` (
 			$query = reloadDictionary();
 			break;
 		default:
-			showError ("unknown batch '${batchid}'", __FUNCTION__);
-			die;
-			break;
+			return null;
+	}
+	return $query;
+}
+
+function executeUpgradeBatch ($batchid)
+{
+	global $dbxlink;
+	$query = getUpgradeBatch($batchid);
+	if( null === $query ){
+		showError ("unknown batch '${batchid}'", __FUNCTION__);
+		die;
 	}
 	$failures = array();
 	echo "<tr><th>Executing batch '${batchid}'</th><td>";
console_upgrade.patch (9,221 bytes)   
adoom42

adoom42

2012-07-22 01:24

administrator   ~0000711

I patched 0.20 and added the other files to the contrib repo.

Issue History

Date Modified Username Field Change
2012-05-14 09:09 user292 New Issue
2012-05-14 09:09 user292 File Added: console_upgrade.patch
2012-07-22 01:24 adoom42 Note Added: 0000711
2012-07-22 01:24 adoom42 Assigned To => adoom42
2012-07-22 01:24 adoom42 Status new => closed
2012-07-22 01:24 adoom42 Resolution open => fixed
2012-07-22 01:24 adoom42 Fixed in Version => 0.20.0
2012-07-22 01:24 adoom42 Target Version => 0.20.0