View Issue Details
ID | Project | Category | View Status | Date Submitted | Last Update |
---|---|---|---|---|---|
556 | RackTables | default | public | 2012-05-14 09:09 | 2012-07-22 01:24 |
Reporter | Assigned To | adoom42 | |||
Priority | normal | Severity | feature | Reproducibility | N/A |
Status | closed | Resolution | fixed | ||
Product Version | 0.19.12 | ||||
Target Version | 0.20.0 | Fixed in Version | 0.20.0 | ||
Summary | 556: Console based database upgrade | ||||
Description | Hi, 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 | ||||
Tags | No tags attached. | ||||
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>"; |
I patched 0.20 and added the other files to the contrib repo. | |
Date Modified | Username | Field | Change |
---|---|---|---|
2012-05-14 09:09 |
|
New Issue | |
2012-05-14 09:09 |
|
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 |