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. | ||||
| Attached Files | 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>";
| ||||
| 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 |