Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 package database | 1 package database |
| 2 | 2 |
| 3 import ( | 3 import ( |
| 4 "bufio" | |
| 4 "database/sql" | 5 "database/sql" |
| 6 "flag" | |
| 5 "fmt" | 7 "fmt" |
| 8 "os" | |
| 9 "strings" | |
| 6 "time" | 10 "time" |
| 7 | 11 |
| 8 _ "github.com/go-sql-driver/mysql" | 12 _ "github.com/go-sql-driver/mysql" |
| 9 "github.com/golang/glog" | 13 "github.com/golang/glog" |
| 10 » _ "github.com/mattn/go-sqlite3" | 14 » "skia.googlesource.com/buildbot.git/go/metadata" |
| 11 "skia.googlesource.com/buildbot.git/go/util" | 15 "skia.googlesource.com/buildbot.git/go/util" |
| 12 ) | 16 ) |
| 13 | 17 |
| 18 const ( | |
| 19 // Template for DB connection strings. | |
| 20 DB_CONN_TMPL = "%s:%s@tcp(%s:%d)/%s?parseTime=true" | |
| 21 | |
| 22 // Name of the root user. | |
| 23 USER_ROOT = "root" | |
| 24 | |
| 25 // Name of the readwrite user. | |
| 26 USER_RW = "readwrite" | |
| 27 | |
| 28 // Key of the password for the readwrite user. | |
| 29 RW_METADATA_KEY = "readwrite" | |
| 30 | |
| 31 // Key of the password for the root user. | |
| 32 ROOT_METADATA_KEY = "root" | |
| 33 ) | |
| 34 | |
| 35 var ( | |
| 36 // Flags | |
| 37 dbHost *string | |
| 38 dbPort *int | |
| 39 dbUser *string | |
| 40 dbName *string | |
| 41 ) | |
| 42 | |
| 43 // SetupFlags adds command-line flags for the database. | |
| 44 func SetupFlags(defaultHost string, defaultPort int, defaultUser, defaultDatabas e string) { | |
| 45 dbHost = flag.String("db_host", defaultHost, "Hostname of the MySQL data base server.") | |
| 46 dbPort = flag.Int("db_port", defaultPort, "Port number of the MySQL data base.") | |
| 47 dbUser = flag.String("db_user", defaultUser, "MySQL user name.") | |
| 48 dbName = flag.String("db_name", defaultDatabase, "Name of the MySQL data base.") | |
| 49 } | |
| 50 | |
| 51 // checkFlags returns an error if the command-line flags have not been set. | |
| 52 func checkFlags() error { | |
| 53 if dbHost == nil || dbPort == nil || dbUser == nil || dbName == nil { | |
| 54 return fmt.Errorf( | |
| 55 "One or more of the required command-line flags was not set. " + | |
| 56 "Did you call forget to call database.SetupFlags ?") | |
| 57 } | |
| 58 return nil | |
| 59 } | |
| 60 | |
| 61 // ConfigFromFlags obtains a DatabaseConfig based on parsed command-line flags. | |
| 62 // If local is true, the DB host is overridden. | |
| 63 func ConfigFromFlags(password string, local bool, m []MigrationStep) (*DatabaseC onfig, error) { | |
| 64 if err := checkFlags(); err != nil { | |
| 65 return nil, err | |
| 66 } | |
| 67 // Override the DB host in local mode. | |
| 68 useHost := *dbHost | |
| 69 if local { | |
| 70 useHost = "localhost" | |
| 71 } | |
| 72 | |
| 73 usePassword := password | |
| 74 // Prompt for password if necessary. | |
| 75 if usePassword == "" { | |
| 76 reader := bufio.NewReader(os.Stdin) | |
| 77 fmt.Printf("Enter password for MySQL user %s at %s:%d: ", *dbUse r, useHost, *dbPort) | |
|
jcgregorio
2015/01/05 18:33:18
Can we skip the prompt if we are local and presume
borenet
2015/01/05 19:03:19
That's fine with me - the caller can always prompt
| |
| 78 var err error | |
| 79 usePassword, err = reader.ReadString('\n') | |
| 80 if err != nil { | |
| 81 return nil, fmt.Errorf("Failed to get password: %v", err ) | |
| 82 } | |
| 83 usePassword = strings.Trim(usePassword, "\n") | |
| 84 } | |
| 85 return NewDatabaseConfig(*dbUser, usePassword, useHost, *dbPort, *dbName , m), nil | |
| 86 } | |
| 87 | |
| 88 // ConfigFromFlagsAndMetadata obtains a DatabaseConfig based on a combination | |
| 89 // of parsed command-line flags and metadata when not running in local mode. | |
| 90 func ConfigFromFlagsAndMetadata(local bool, m []MigrationStep) (*DatabaseConfig, error) { | |
| 91 if err := checkFlags(); err != nil { | |
| 92 return nil, err | |
| 93 } | |
| 94 // If not in local mode, get the password from metadata. | |
| 95 password := "" | |
| 96 if !local { | |
| 97 key := "" | |
| 98 if *dbUser == USER_RW { | |
| 99 key = RW_METADATA_KEY | |
| 100 } else if *dbUser == USER_ROOT { | |
| 101 key = ROOT_METADATA_KEY | |
| 102 } | |
| 103 if key == "" { | |
| 104 return nil, fmt.Errorf("Unknown user %s; could not obtai n password from metadata.", *dbUser) | |
| 105 } | |
| 106 var err error | |
| 107 password, err = metadata.Get(key) | |
| 108 if err != nil { | |
| 109 return nil, fmt.Errorf("Failed to find metadata. Use 'lo cal' flag when running locally.") | |
| 110 } | |
| 111 } | |
| 112 return ConfigFromFlags(password, local, m) | |
| 113 } | |
| 114 | |
| 14 // Config information to create a database connection. | 115 // Config information to create a database connection. |
| 15 type DatabaseConfig struct { | 116 type DatabaseConfig struct { |
| 16 MySQLString string | 117 MySQLString string |
| 17 SQLiteFilePath string | |
| 18 MigrationSteps []MigrationStep | 118 MigrationSteps []MigrationStep |
| 19 } | 119 } |
| 20 | 120 |
| 121 // NewDatabaseConfig constructs a DatabaseConfig from the given options. | |
| 122 func NewDatabaseConfig(user, password, host string, port int, database string, m []MigrationStep) *DatabaseConfig { | |
| 123 return &DatabaseConfig{ | |
| 124 MySQLString: fmt.Sprintf(DB_CONN_TMPL, user, password, host, port, database), | |
| 125 MigrationSteps: m, | |
| 126 } | |
| 127 } | |
| 128 | |
| 21 // Single step to migrated from one database version to the next and back. | 129 // Single step to migrated from one database version to the next and back. |
| 22 type MigrationStep struct { | 130 type MigrationStep struct { |
| 23 » MySQLUp []string | 131 » MySQLUp []string |
| 24 » MySQLDown []string | 132 » MySQLDown []string |
| 25 » SQLiteUp []string | |
| 26 » SQLiteDown []string | |
| 27 } | 133 } |
| 28 | 134 |
| 29 // Database handle to send queries to the underlying database. | 135 // Database handle to send queries to the underlying database. |
| 30 type VersionedDB struct { | 136 type VersionedDB struct { |
| 31 » // Database intance that is either backed by SQLite or MySQl. | 137 » // Database intance that is backed by MySQL. |
| 32 DB *sql.DB | 138 DB *sql.DB |
| 33 | 139 |
| 34 // Keeps track if we are connected to MySQL or SQLite | |
| 35 IsMySQL bool | |
| 36 | |
| 37 // List of migration steps for this database. | 140 // List of migration steps for this database. |
| 38 migrationSteps []MigrationStep | 141 migrationSteps []MigrationStep |
| 39 } | 142 } |
| 40 | 143 |
| 41 // Init must be called once before DB is used. | 144 // Init must be called once before DB is used. |
| 42 // | 145 // |
| 43 // Since it used glog, make sure it is also called after flag.Parse is called. | 146 // Since it used glog, make sure it is also called after flag.Parse is called. |
| 44 func NewVersionedDB(conf *DatabaseConfig) *VersionedDB { | 147 func NewVersionedDB(conf *DatabaseConfig) *VersionedDB { |
| 45 // If there is a connection string then connect to the MySQL server. | 148 // If there is a connection string then connect to the MySQL server. |
| 46 // This is for testing only. In production we get the relevant informati on | 149 // This is for testing only. In production we get the relevant informati on |
| 47 // from the metadata server. | 150 // from the metadata server. |
| 48 var err error | 151 var err error |
| 49 var isMySQL = true | |
| 50 var DB *sql.DB = nil | 152 var DB *sql.DB = nil |
| 51 | 153 |
| 52 » if conf.MySQLString != "" { | 154 » glog.Infoln("Opening SQL database.") |
| 53 » » glog.Infoln("Opening SQL database.") | 155 » if DB, err = sql.Open("mysql", conf.MySQLString); err == nil { |
| 54 » » if DB, err = sql.Open("mysql", conf.MySQLString); err == nil { | 156 » » glog.Infoln("Sending Ping.") |
| 55 » » » glog.Infoln("Sending Ping.") | 157 » » err = DB.Ping() |
| 56 » » » err = DB.Ping() | 158 » } |
| 57 » » } | |
| 58 | 159 |
| 59 » » if err != nil { | 160 » if err != nil { |
| 60 » » » glog.Fatalln("Failed to open connection to SQL server:", err) | 161 » » glog.Fatalln("Failed to open connection to SQL server:", err) |
| 61 » » } | |
| 62 » } else { | |
| 63 » » // Open a local SQLite database instead. | |
| 64 » » glog.Infof("Opening local sqlite database at: %s", conf.SQLiteFi lePath) | |
| 65 » » // Fallback to sqlite for local use. | |
| 66 » » DB, err = sql.Open("sqlite3", conf.SQLiteFilePath) | |
| 67 » » if err != nil { | |
| 68 » » » glog.Fatalln("Failed to open:", err) | |
| 69 » » } | |
| 70 » » isMySQL = false | |
| 71 } | 162 } |
| 72 | 163 |
| 73 result := &VersionedDB{ | 164 result := &VersionedDB{ |
| 74 DB: DB, | 165 DB: DB, |
| 75 IsMySQL: isMySQL, | |
| 76 migrationSteps: conf.MigrationSteps, | 166 migrationSteps: conf.MigrationSteps, |
| 77 } | 167 } |
| 78 | 168 |
| 79 // Make sure the migration table exists. | 169 // Make sure the migration table exists. |
| 80 if err := result.checkVersionTable(); err != nil { | 170 if err := result.checkVersionTable(); err != nil { |
| 81 // We are using panic() instead of Fataln() to be able to trap t his | 171 // We are using panic() instead of Fataln() to be able to trap t his |
| 82 // in tests and make sure it fails when no version table exists. | 172 // in tests and make sure it fails when no version table exists. |
| 83 glog.Errorln("Unable to create version table.") | 173 glog.Errorln("Unable to create version table.") |
| 84 panic("Attempt to create version table returned: " + err.Error() ) | 174 panic("Attempt to create version table returned: " + err.Error() ) |
| 85 } | 175 } |
| 86 glog.Infoln("Version table OK.") | 176 glog.Infoln("Version table OK.") |
| 87 | 177 |
| 88 // Migrate to the latest version if we are using SQLite, so we don't hav e | |
| 89 // to run the *_migratdb command for a local database. | |
| 90 if !result.IsMySQL { | |
| 91 result.Migrate(result.MaxDBVersion()) | |
| 92 } | |
| 93 | |
| 94 // Ping the database occasionally to keep the connection fresh. | 178 // Ping the database occasionally to keep the connection fresh. |
| 95 go func() { | 179 go func() { |
| 96 c := time.Tick(1 * time.Minute) | 180 c := time.Tick(1 * time.Minute) |
| 97 for _ = range c { | 181 for _ = range c { |
| 98 if err := result.DB.Ping(); err != nil { | 182 if err := result.DB.Ping(); err != nil { |
| 99 glog.Warningln("Database failed to respond:", er r) | 183 glog.Warningln("Database failed to respond:", er r) |
| 100 } | 184 } |
| 101 glog.Infof("db: Successful ping") | 185 glog.Infof("db: Successful ping") |
| 102 } | 186 } |
| 103 }() | 187 }() |
| (...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 178 return version, err | 262 return version, err |
| 179 } | 263 } |
| 180 | 264 |
| 181 // Returns the highest version currently available. | 265 // Returns the highest version currently available. |
| 182 func (vdb *VersionedDB) MaxDBVersion() int { | 266 func (vdb *VersionedDB) MaxDBVersion() int { |
| 183 return len(vdb.migrationSteps) | 267 return len(vdb.migrationSteps) |
| 184 } | 268 } |
| 185 | 269 |
| 186 // Returns an error if the version table does not exist. | 270 // Returns an error if the version table does not exist. |
| 187 func (vdb *VersionedDB) checkVersionTable() error { | 271 func (vdb *VersionedDB) checkVersionTable() error { |
| 188 » // Check if the table exists in MySQL or SQLite. | 272 » // Check if the table exists in MySQL. |
| 189 stmt := "SHOW TABLES LIKE 'sk_db_version'" | 273 stmt := "SHOW TABLES LIKE 'sk_db_version'" |
| 190 if !vdb.IsMySQL { | |
| 191 stmt = "SELECT name FROM sqlite_master WHERE type='table' AND na me='sk_db_version';" | |
| 192 } | |
| 193 | 274 |
| 194 var temp string | 275 var temp string |
| 195 err := vdb.DB.QueryRow(stmt).Scan(&temp) | 276 err := vdb.DB.QueryRow(stmt).Scan(&temp) |
| 196 if err != nil { | 277 if err != nil { |
| 197 // See if we can create the version table. | 278 // See if we can create the version table. |
| 198 return vdb.ensureVersionTable() | 279 return vdb.ensureVersionTable() |
| 199 } | 280 } |
| 200 | 281 |
| 201 return nil | 282 return nil |
| 202 } | 283 } |
| (...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 256 if inc < 0 { | 337 if inc < 0 { |
| 257 idx = currentVersion - 1 | 338 idx = currentVersion - 1 |
| 258 } | 339 } |
| 259 delta := util.AbsInt(targetVersion - currentVersion) | 340 delta := util.AbsInt(targetVersion - currentVersion) |
| 260 result := make([][]string, 0, delta) | 341 result := make([][]string, 0, delta) |
| 261 | 342 |
| 262 for i := 0; i < delta; i++ { | 343 for i := 0; i < delta; i++ { |
| 263 var temp []string | 344 var temp []string |
| 264 switch { | 345 switch { |
| 265 // using mysqlp | 346 // using mysqlp |
| 266 » » case (inc > 0) && vdb.IsMySQL: | 347 » » case (inc > 0): |
| 267 temp = vdb.migrationSteps[idx].MySQLUp | 348 temp = vdb.migrationSteps[idx].MySQLUp |
| 268 » » case (inc < 0) && vdb.IsMySQL: | 349 » » case (inc < 0): |
| 269 temp = vdb.migrationSteps[idx].MySQLDown | 350 temp = vdb.migrationSteps[idx].MySQLDown |
| 270 // using sqlite | |
| 271 case (inc > 0): | |
| 272 temp = vdb.migrationSteps[idx].SQLiteUp | |
| 273 case (inc < 0): | |
| 274 temp = vdb.migrationSteps[idx].SQLiteDown | |
| 275 } | 351 } |
| 276 result = append(result, temp) | 352 result = append(result, temp) |
| 277 idx += inc | 353 idx += inc |
| 278 } | 354 } |
| 279 return result | 355 return result |
| 280 } | 356 } |
| OLD | NEW |