Fixes migration where password created_at is nullable

The 105 migration added a password created_at column that was left as
nullable. This patch sets a default value for password created_at and
makes this column not nullable.

Closes-Bug: #1596500
Change-Id: I394467d554c786ecd9bf55367435c856c6723042
This commit is contained in:
Ronald De Rose 2016-08-30 00:35:21 +00:00 committed by David Stanek
parent 0061419170
commit 2b70175282
9 changed files with 130 additions and 2 deletions

View File

@ -0,0 +1,39 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import sqlalchemy as sql
def upgrade(migrate_engine):
meta = sql.MetaData()
meta.bind = migrate_engine
password = sql.Table('password', meta, autoload=True)
# Because it's difficult to get a timestamp server default working amoung
# all of the supported datbases and versions, I'm choosing to drop and then
# recreate the column as I think this is a more cleaner option. This will
# only impact operators that have already deployed the 105 migration;
# resetting the password created_at for security compliannce features, if
# enabled.
password.c.created_at.drop()
# sqlite doesn't support server_default=sql.func.now(), so skipping.
if migrate_engine.name == 'sqlite':
created_at = sql.Column('created_at', sql.TIMESTAMP, nullable=True)
else:
# Changing type to timestamp as mysql 5.5 and older doesn't support
# datetime defaults.
created_at = sql.Column('created_at', sql.TIMESTAMP, nullable=False,
default=datetime.datetime.utcnow,
server_default=sql.func.now())
password.create_column(created_at)

View File

@ -54,6 +54,7 @@ Enum = sql.Enum
ForeignKey = sql.ForeignKey
DateTime = sql.DateTime
Date = sql.Date
TIMESTAMP = sql.TIMESTAMP
IntegrityError = sql.exc.IntegrityError
DBDuplicateEntry = db_exception.DBDuplicateEntry
OperationalError = sql.exc.OperationalError

View File

@ -0,0 +1,18 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# A null initial migration to open this repo. Do not re-use replace this with
# a real migration, add additional ones in subsequent version scripts.
def upgrade(migrate_engine):
pass

View File

@ -0,0 +1,18 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# A null initial migration to open this repo. Do not re-use replace this with
# a real migration, add additional ones in subsequent version scripts.
def upgrade(migrate_engine):
pass

View File

@ -234,7 +234,7 @@ class Password(sql.ModelBase, sql.DictBase):
ondelete='CASCADE'))
password = sql.Column(sql.String(128), nullable=True)
# created_at default set here to safe guard in case it gets missed
created_at = sql.Column(sql.DateTime, nullable=False,
created_at = sql.Column(sql.TIMESTAMP, nullable=False,
default=datetime.datetime.utcnow)
expires_at = sql.Column(sql.DateTime, nullable=True)
self_service = sql.Column(sql.Boolean, default=False, nullable=False,

View File

@ -150,7 +150,7 @@ class SqlModels(SqlTests):
cols = (('id', sql.Integer, None),
('local_user_id', sql.Integer, None),
('password', sql.String, 128),
('created_at', sql.DateTime, None),
('created_at', sql.TIMESTAMP, None),
('expires_at', sql.DateTime, None),
('self_service', sql.Boolean, False))
self.assertExpectedSchema('password', cols)

View File

@ -227,6 +227,11 @@ class TestKeystoneExpandSchemaMigrations(
# NOTE(xek, henry-nash): Reviewers: DO NOT ALLOW THINGS TO BE ADDED
# HERE UNLESS JUSTIFICATION CAN BE PROVIDED AS TO WHY THIS WILL NOT
# CAUSE PROBLEMS FOR ROLLING UPGRADES.
# Migration 002 changes the column type, from datetime to timestamp in
# the contract phase. Adding exception here to pass expand banned
# tests, otherwise fails.
2
]
def setUp(self):
@ -257,6 +262,11 @@ class TestKeystoneDataMigrations(
# NOTE(xek, henry-nash): Reviewers: DO NOT ALLOW THINGS TO BE ADDED
# HERE UNLESS JUSTIFICATION CAN BE PROVIDED AS TO WHY THIS WILL NOT
# CAUSE PROBLEMS FOR ROLLING UPGRADES.
# Migration 002 changes the column type, from datetime to timestamp in
# the contract phase. Adding exception here to pass banned data
# migration tests. Fails otherwise.
2
]
def setUp(self):
@ -296,6 +306,11 @@ class TestKeystoneContractSchemaMigrations(
# NOTE(xek, henry-nash): Reviewers: DO NOT ALLOW THINGS TO BE ADDED
# HERE UNLESS JUSTIFICATION CAN BE PROVIDED AS TO WHY THIS WILL NOT
# CAUSE PROBLEMS FOR ROLLING UPGRADES.
# Migration 002 changes the column type, from datetime to timestamp.
# To do this, the column is first dropped and recreated. This should
# not have any negative impact on a rolling upgrade deployment.
2
]
def setUp(self):

View File

@ -1625,3 +1625,35 @@ class VersionTests(SqlMigrateBase):
"""
# Note to reviewers: this version number should never change.
self.assertEqual(109, self.repos[LEGACY_REPO].max_version)
class FullMigration(SqlMigrateBase, unit.TestCase):
"""Test complete orchestration between all database phases."""
def setUp(self):
super(FullMigration, self).setUp()
# Upgrade the legacy repository
self.upgrade()
def test_migration_002_password_created_at_not_nullable(self):
# upgrade each repository to 001
self.expand(1)
self.migrate(1)
self.contract(1)
password = sqlalchemy.Table('password', self.metadata, autoload=True)
self.assertTrue(password.c.created_at.nullable)
# upgrade each repository to 002
self.expand(2)
self.migrate(2)
self.contract(2)
password = sqlalchemy.Table('password', self.metadata, autoload=True)
if self.engine.name != 'sqlite':
self.assertFalse(password.c.created_at.nullable)
class MySQLOpportunisticFullMigration(FullMigration):
FIXTURE = test_base.MySQLOpportunisticFixture
class PostgreSQLOpportunisticFullMigration(FullMigration):
FIXTURE = test_base.PostgreSQLOpportunisticFixture

View File

@ -0,0 +1,5 @@
---
upgrade:
- Fixes a bug related to the password create date. If you deployed master
during Newton development, the password create date may be reset. This
would only be apparent if you have security compliance features enabled.