예제 #1
0
 protected function init()
 {
     $this->setDescription('Create a new TestSwarm project. Returns the auth token (can be re-created with refreshProjectToken.php).');
     $this->registerOption('create', 'boolean', 'Pass this to the create if it doesn\'t exist.');
     $this->registerOption('id', 'value', 'ID of project (must be in format: "' . LoginAction::getNameValidationRegex() . '").');
     $this->registerOption('display-title', 'value', 'Display title (free form text, max: 255 chars)');
     $this->registerOption('password', 'value', 'Password for this project (omit to enter in interactive mode)');
     $this->registerOption('site-url', 'value', 'URL for this project (optional)');
 }
예제 #2
0
 protected function init()
 {
     $this->setDescription('Create or update a TestSwarm project.');
     $this->registerOption('create', 'boolean', 'Pass this to the create if it doesn\'t exist.');
     $this->registerOption('delete', 'boolean', 'Pass this to remove a project and recursively delete all its jobs and runs.');
     $this->registerOption('id', 'value', 'ID of project (must be in format: "' . LoginAction::getNameValidationRegex() . '").');
     $this->registerOption('display-title', 'value', 'Display title (free form text, max: 255 chars)');
     $this->registerOption('password', 'value', 'Password for this project (omit to enter in interactive mode)');
     $this->registerOption('site-url', 'value', 'URL for this project (optional)');
 }
예제 #3
0
    /**
     * The actual database updates
     * Friendly reminder from http://dev.mysql.com/doc/refman/5.1/en/alter-table.html
     * - Column name must be mentioned twice in ALTER TABLE CHANGE
     * - Definition must be complete
     *   So 'CHANGE foo BIGINT' on a 'foo INT UNSIGNED DEFAULT 1' will remove
     *   the default and unsigned property.
     *   Except for PRIMARY KEY or UNIQUE properties, those must never be
     *   part of a CHANGE clause.
     */
    protected function doDatabaseUpdates()
    {
        if ($this->getContext()->dbLock()) {
            $this->error('Database is currently locked, please remove ./cache/database.lock before updating.');
        }
        $db = $this->getContext()->getDB();
        $this->out('Setting database.lock, other requests may not access the database during the update.');
        $this->getContext()->dbLock(true);
        $this->out('Running tests on the database to detect which updates are needed.');
        /**
         * 0.2.0 -> 1.0.0-alpha (patch-new-ua-runresults.sql)
         * useragents and run_client table removed, many column changes, new runresults table.
         */
        // If the previous version was before 1.0.0 we won't offer an update, because most
        // changes in 1.0.0 can't be simulated without human intervention. The changes are not
        // backwards compatible. Instead do a few quick checks to verify this is in fact a
        // pre-1.0.0 database, then ask the user for a re-install from scratch
        // (except for the users table).
        $has_run_client = $db->tableExists('run_client');
        $has_users_request = $db->fieldExists('users', 'request');
        $clients_useragent_id = $db->fieldInfo('clients', 'useragent_id');
        if (!$clients_useragent_id) {
            $this->unknownDatabaseState('clients.useragent_id not found');
            return;
        }
        if (!$has_run_client && !$has_users_request && !$clients_useragent_id->numeric && $clients_useragent_id->type === 'string') {
            $this->out('... run_client table already dropped');
            $this->out('... users.request already dropped');
            $this->out('... client.useragent_id is up to date');
        } else {
            $this->out("\n" . "It appears this database is from before 1.0.0. No update exists for those versions.\n" . "The updater could re-install TestSwarm (optionally preserving user accounts)\n" . "THIS WILL DELETE ALL DATA.\nContinue? (Y/N)");
            $reinstall = $this->cliInput();
            if ($reinstall !== 'Y') {
                // Nothing left to do. Remove database.lock and abort the script
                $this->getContext()->dbLock(false);
                return;
            }
            $this->out("Import user names and tokens from the old database after re-installing?\n" . "(Note: password and seed cannot be restored due to incompatibility in the database.\n" . ' Instead the auth token will be used as the the new password) (Y/N)');
            $reimportUsers = $this->cliInput();
            // Drop all known TestSwarm tables in the database
            // (except users, handled separately)
            foreach (array('run_client', 'clients', 'run_useragent', 'useragents', 'runs', 'jobs') as $dropTable) {
                $this->outRaw("Dropping {$dropTable} table...");
                $exists = $db->tableExists($dropTable);
                if ($exists) {
                    $dropped = $db->query('DROP TABLE ' . $db->addIdentifierQuotes($dropTable));
                    $this->out(' ' . ($dropped ? 'OK' : 'FAILED'));
                } else {
                    $this->out('SKIPPED (didn\'t exist)');
                }
            }
            // Handle users table (reimport or drop as well)
            $userRows = array();
            if ($reimportUsers === 'Y') {
                $this->out('Upgrading users table');
                $this->outRaw('Fetching current users...');
                $has_users = $db->tableExists('users');
                if (!$has_users) {
                    $this->out('SKIPPED (users table didn\'t exist)');
                } else {
                    $userRows = $db->getRows('SELECT * FROM users');
                    $this->out('OK');
                }
            }
            $this->outRaw('Dropping users table...');
            $dropped = $db->query('DROP TABLE users');
            $this->out(' ' . ($dropped ? 'OK' : 'FAILED'));
            // Create new tables
            $this->outRaw('Creating new tables... (this may take a few minutes)');
            global $swarmInstallDir;
            $fullSchemaFile = "{$swarmInstallDir}/config/tables.sql";
            if (!is_readable($fullSchemaFile)) {
                $this->error('Can\'t read schema file');
            }
            $fullSchemaSql = file_get_contents($fullSchemaFile);
            $executed = $db->batchQueryFromFile($fullSchemaSql);
            if (!$executed) {
                $this->error('Creating new tables failed');
            }
            $this->out('OK');
            if ($reimportUsers === 'Y') {
                $this->out('Re-importing ' . count($userRows) . ' users...');
                foreach ($userRows as $userRow) {
                    $this->outRaw('- creating user "' . $userRow->name . '"... ');
                    if (empty($userRow->password) || empty($userRow->seed) || empty($userRow->auth)) {
                        $this->out('SKIPPED: Not a project account but a swarm client.');
                        continue;
                    }
                    try {
                        $signupAction = SignupAction::newFromContext($this->getContext());
                        // Password stored in the old database is a hash of the old seed (of type 'double'
                        // and the actual password. We can't create this user with the same password because
                        // sha1 is not supposed to be decodable.
                        // I tried overriding the created row after the creation with the old seed and password,
                        // but that didn't work because the old seed doesn't fit in the new seed field (of binary(40)).
                        // When inserted, mysql transforms it into something else and sha1(seed + password) will no
                        // longer match the hash. So instead create the new user with the auth token as password.
                        $signupAction->doCreateUser($userRow->name, $userRow->auth);
                        $err = $signupAction->getError();
                        if (!$err) {
                            $this->outRaw('OK. Restoring auth token... ');
                            $data = $signupAction->getData();
                            $updated = $db->query(str_queryf('UPDATE users
								SET
									auth = %s
								WHERE id = %u', $userRow->auth, $data['userID']));
                            $this->out($updated ? 'OK.' : 'FAILED.');
                        } else {
                            $this->out("FAILED. SignupAction error. {$err['info']}");
                        }
                    } catch (Exception $e) {
                        $this->out("FAILED. Unexpected exception thrown while creating account. {$e->getMessage()}");
                    }
                }
            }
            // End of users re-import
        }
        // End of patch-new-ua-runresults.sql
        /**
         * 1.0.0-alpha (patch-users-projects-conversion.sql)
         * users table removed, new projects table, various column changes.
         */
        $has_users = $db->tableExists('users');
        $has_clients_user_id = $db->fieldInfo('clients', 'user_id');
        $has_jobs_user_id = $db->fieldInfo('jobs', 'user_id');
        $has_projects = $db->tableExists('projects');
        $has_clients_name = $db->fieldInfo('clients', 'name');
        $has_jobs_project_id = $db->fieldInfo('jobs', 'project_id');
        $has_clients_useragent_id = $db->fieldInfo('clients', 'useragent_id');
        if (!$has_users && !$has_clients_user_id && !$has_jobs_user_id) {
            $this->out('... users table already dropped');
            $this->out('... clients.user_id already dropped');
            $this->out('... jobs.user_id already dropped');
        } else {
            // Verify that the entire database is in the 1.0.0-alpha2012 state,
            // not just part of it.
            foreach (array('users table' => $has_users, 'clients.user_id' => $has_clients_user_id, 'jobs.user_id' => $has_jobs_user_id, 'projects table' => !$has_projects, 'clients.name' => !$has_clients_name, 'jobs.project_id' => !$has_jobs_project_id, 'clients.useragent_id' => $has_clients_useragent_id) as $label => $isAsExpected) {
                if (!$isAsExpected) {
                    $this->unknownDatabaseState($label . ' not found');
                    return;
                }
            }
            $this->out('Schema changes before users-projects-conversion migration...');
            $this->out('... creating projects table');
            $db->query("CREATE TABLE `projects` (\n  `id` varchar(255) binary NOT NULL PRIMARY KEY,\n  `display_title` varchar(255) binary NOT NULL,\n  `site_url` blob NOT NULL default '',\n  `password` tinyblob NOT NULL,\n  `auth_token` tinyblob NOT NULL,\n  `updated` binary(14) NOT NULL,\n  `created` binary(14) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;");
            $this->out('... adding clients.name');
            $db->query("ALTER TABLE clients\n  ADD `name` varchar(255) binary NOT NULL AFTER `id`");
            $this->out('... adding jobs.project_id');
            $db->query("ALTER TABLE jobs\n  ADD `project_id` varchar(255) binary NOT NULL AFTER `name`");
            $this->out('... dropping constraint fk_clients_user_id');
            $db->query("ALTER TABLE clients\n  DROP FOREIGN KEY fk_clients_user_id");
            $this->out('... dropping constraint fk_jobs_user_id');
            $db->query("ALTER TABLE jobs\n  DROP FOREIGN KEY fk_jobs_user_id");
            $this->out('... dropping constraint fk_runs_job_id');
            $db->query("ALTER TABLE runs\n  DROP FOREIGN KEY fk_runs_job_id");
            $this->out('... dropping constraint fk_run_useragent_run_id');
            $db->query("ALTER TABLE run_useragent\n  DROP FOREIGN KEY fk_run_useragent_run_id");
            $this->out('... dropping constraint fk_runresults_client_id');
            $db->query("ALTER TABLE runresults\n  DROP FOREIGN KEY fk_runresults_client_id");
            $this->out('... dropping index idx_users_name');
            $db->query("ALTER TABLE users\n  DROP INDEX idx_users_name");
            $this->out('... dropping index idx_clients_user_useragent_updated');
            $db->query("ALTER TABLE clients\n  DROP INDEX idx_clients_user_useragent_updated");
            $this->out('... dropping index idx_jobs_user');
            $db->query("ALTER TABLE jobs\n  DROP INDEX idx_jobs_user");
            $this->out('Migrating old content into new schema...');
            $this->out('... fetching users table');
            $userRows = $db->getRows('SELECT * FROM users') ?: array();
            $this->out('... found ' . count($userRows) . ' users');
            foreach ($userRows as $userRow) {
                $this->out('... creating project "' . $userRow->name . '"');
                if (!trim($userRow->seed) || !trim($userRow->password) || !trim($userRow->auth)) {
                    // Client.php used to create rows in the users table with blanks
                    // in these "required" fields. MySQL expands the emptyness to the full
                    // 40-width of the column. Hence the trim().
                    $this->out('    SKIPPED: Not a project account but a swarm client.');
                    continue;
                }
                // Validate project id
                if (!LoginAction::isValidName($userRow->name)) {
                    $this->out('    SKIPPED: User name not a valid project id. Must match: ' . LoginAction::getNameValidationRegex());
                    continue;
                }
                if (!$db->getOne(str_queryf('SELECT 1 FROM jobs WHERE user_id=%u', $userRow->id))) {
                    $this->out('    SKIPPED: Account has 0 jobs');
                    continue;
                }
                $isInserted = $db->query(str_queryf('INSERT INTO projects
					(id, display_title, site_url, password, auth_token, updated, created)
					VALUES(%s, %s, %s, %s, %s, %s, %s);', $userRow->name, $userRow->name, '', LoginAction::generatePasswordHashForUserrow($userRow), sha1($userRow->auth), swarmdb_dateformat(SWARM_NOW), $userRow->created));
                if (!$isInserted) {
                    $this->out('    FAILED: Failed to insert row into projects table.');
                    continue;
                }
                $this->out('... updating references for project "' . $userRow->name . '"');
                $isUpdated = $db->query(str_queryf('UPDATE clients
					SET name=%s
					WHERE user_id=%u', $userRow->name, $userRow->id));
                if (!$isUpdated) {
                    $this->out('    FAILED: Failed to update rows in clients table.');
                    continue;
                }
                $isUpdated = $db->query(str_queryf('UPDATE jobs
					SET project_id=%s
					WHERE user_id=%u', $userRow->name, $userRow->id));
                if (!$isUpdated) {
                    $this->out('    FAILED: Failed to update rows in jobs table.');
                    continue;
                }
            }
            $this->out('Schema changes after users-projects-conversion migration...');
            $this->out('... changing clients.useragent_id');
            $db->query("ALTER TABLE clients\n  CHANGE COLUMN `useragent_id` `useragent_id` varchar(255) NOT NULL");
            $this->out('... dropping clients.user_id');
            $db->query("ALTER TABLE clients\n  DROP COLUMN `user_id`");
            $this->out('... dropping jobs.user_id');
            $db->query("ALTER TABLE jobs\n  DROP COLUMN `user_id`");
            $this->out('... dropping users table');
            $db->query("DROP TABLE users");
            $this->out('... adding index idx_clients_name_ua_created');
            $db->query("ALTER TABLE clients\n  ADD INDEX idx_clients_name_ua_created (name, useragent_id, created);");
            $this->out('... adding index idx_jobs_project_created');
            $db->query("ALTER TABLE jobs\n  ADD INDEX idx_jobs_project_created (project_id, created);");
        }
        // End of patch-users-projects-conversion.sql
        $this->getContext()->dbLock(false);
        $this->out("Removed database.lock.\nNo more updates.");
    }
예제 #4
0
    /**
     * @param string $id
     * @param array $options
     * @return array Exposes the new auth token
     */
    public function create($id, array $options = null)
    {
        $db = $this->getContext()->getDB();
        $password = isset($options['password']) ? $options['password'] : null;
        $displayTitle = isset($options['displayTitle']) ? $options['displayTitle'] : null;
        $siteUrl = isset($options['siteUrl']) ? $options['siteUrl'] : '';
        if (!$id || !$displayTitle || !$password) {
            $this->setError('missing-parameters');
            return;
        }
        // Check if a project by this id doesn't exist already
        $row = $db->getOne(str_queryf('SELECT id FROM projects WHERE id = %s;', $id));
        if ($row) {
            $this->setError('invalid-input', 'Unable to create project, a project by that name exists already.');
            return;
        }
        // Validate project id
        if (!LoginAction::isValidName($id)) {
            $this->setError('invalid-input', 'Project ids must be in format: "' . LoginAction::getNameValidationRegex() . '".');
            return;
        }
        // maxlength (otherwise MySQL will crop it)
        if (strlen($displayTitle) > 255) {
            $this->setError('Display title has to be no longer than 255 characters.');
            return;
        }
        // Create the project
        $authToken = LoginAction::generateRandomHash(40);
        $authTokenHash = sha1($authToken);
        $isInserted = $db->query(str_queryf('INSERT INTO projects
			(id, display_title, site_url, password, auth_token, updated, created)
			VALUES(%s, %s, %s, %s, %s, %s, %s);', $id, $displayTitle, $siteUrl, LoginAction::generatePasswordHash($password), $authTokenHash, swarmdb_dateformat(SWARM_NOW), swarmdb_dateformat(SWARM_NOW)));
        if (!$isInserted) {
            $this->setError('internal-error', 'Insertion of row into database failed.');
            return;
        }
        return array('authToken' => $authToken);
    }