diff --git a/ironic/db/sqlalchemy/alembic/versions/163040c5513f_add_firmware_information.py b/ironic/db/sqlalchemy/alembic/versions/163040c5513f_add_firmware_information.py new file mode 100644 index 0000000000..7fcac7e3f8 --- /dev/null +++ b/ironic/db/sqlalchemy/alembic/versions/163040c5513f_add_firmware_information.py @@ -0,0 +1,50 @@ +# 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. + +"""add firmware information + +Revision ID: 163040c5513f +Revises: fe222f476baf +Create Date: 2023-05-11 14:30:46.600582 + +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = '163040c5513f' +down_revision = 'fe222f476baf' + + +def upgrade(): + op.add_column('nodes', sa.Column('firmware_interface', + sa.String(length=255), nullable=True)) + op.create_table( + 'firmware_information', + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('node_id', sa.Integer(), nullable=False), + sa.Column('component', sa.String(length=255), nullable=False), + sa.Column('initial_version', sa.String(length=255), nullable=False), + sa.Column('current_version', sa.String(length=255), nullable=True), + sa.Column('last_version_flashed', sa.String(length=255), + nullable=True), + sa.Column('version', sa.String(length=15), nullable=True), + sa.ForeignKeyConstraint(['node_id'], ['nodes.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('node_id', 'component', + name='uniq_nodecomponent0node_id0component'), + mysql_ENGINE='InnoDB', + mysql_DEFAULT_CHARSET='UTF8MB3' + ) diff --git a/ironic/db/sqlalchemy/models.py b/ironic/db/sqlalchemy/models.py index 62e144d097..d15b06d86a 100644 --- a/ironic/db/sqlalchemy/models.py +++ b/ironic/db/sqlalchemy/models.py @@ -200,6 +200,7 @@ class NodeBase(Base): boot_interface = Column(String(255), nullable=True) console_interface = Column(String(255), nullable=True) deploy_interface = Column(String(255), nullable=True) + firmware_interface = Column(String(255), nullable=True) inspect_interface = Column(String(255), nullable=True) management_interface = Column(String(255), nullable=True) network_interface = Column(String(255), nullable=True) @@ -499,6 +500,22 @@ class NodeInventory(Base): node_id = Column(Integer, ForeignKey('nodes.id'), nullable=True) +class FirmwareInformation(Base): + """Represents the firmware information of a bare metal node.""" + __tablename__ = "firmware_information" + __table_args__ = ( + schema.UniqueConstraint( + 'node_id', 'component', + name='uniq_nodecomponent0node_id0component'), + table_args()) + id = Column(Integer, primary_key=True) + node_id = Column(Integer, ForeignKey('nodes.id'), nullable=False) + component = Column(String(255), nullable=False) + initial_version = Column(String(255), nullable=False) + current_version = Column(String(255), nullable=True) + last_version_flashed = Column(String(255), nullable=True) + + def get_class(model_name): """Returns the model class with the specified name. diff --git a/ironic/tests/unit/common/test_release_mappings.py b/ironic/tests/unit/common/test_release_mappings.py index e6f4479b94..3329949b03 100644 --- a/ironic/tests/unit/common/test_release_mappings.py +++ b/ironic/tests/unit/common/test_release_mappings.py @@ -101,7 +101,7 @@ class ReleaseMappingsTestCase(base.TestCase): # NodeBase is also excluded as it is covered by Node. exceptions = set(['NodeTag', 'ConductorHardwareInterfaces', 'NodeTrait', 'DeployTemplateStep', - 'NodeBase']) + 'NodeBase', 'FirmwareInformation']) model_names -= exceptions # NodeTrait maps to two objects model_names |= set(['Trait', 'TraitList']) diff --git a/ironic/tests/unit/db/sqlalchemy/test_migrations.py b/ironic/tests/unit/db/sqlalchemy/test_migrations.py index 4abd1cbc4a..aaddf1bda6 100644 --- a/ironic/tests/unit/db/sqlalchemy/test_migrations.py +++ b/ironic/tests/unit/db/sqlalchemy/test_migrations.py @@ -1261,6 +1261,68 @@ class MigrationCheckersMixin(object): nodes = db_utils.get_table(engine, 'nodes') self.assertIsInstance(nodes.c.shard.type, sqlalchemy.types.String) + def _pre_upgrade_163040c5513f(self, engine): + # Create a node to which firmware information can be added. + data = {'uuid': uuidutils.generate_uuid()} + nodes = db_utils.get_table(engine, 'nodes') + with engine.begin() as connection: + insert_node = nodes.insert().values(data) + connection.execute(insert_node) + node_stmt = sqlalchemy.select( + models.Node.id + ).where(models.Node.uuid == data['uuid']) + node = connection.execute(node_stmt).first() + data['id'] = node.id + + return data + + def _check_163040c5513f(self, engine, data): + fw_information = db_utils.get_table(engine, 'firmware_information') + col_names = [column.name for column in fw_information.c] + expected_names = ['created_at', 'updated_at', 'id', 'node_id', + 'component', 'initial_version', 'current_version', + 'last_version_flashed', 'version'] + self.assertEqual(sorted(expected_names), sorted(col_names)) + self.assertIsInstance(fw_information.c.created_at.type, + sqlalchemy.types.DateTime) + self.assertIsInstance(fw_information.c.updated_at.type, + sqlalchemy.types.DateTime) + self.assertIsInstance(fw_information.c.id.type, + sqlalchemy.types.Integer) + self.assertIsInstance(fw_information.c.node_id.type, + sqlalchemy.types.Integer) + self.assertIsInstance(fw_information.c.component.type, + sqlalchemy.types.String) + self.assertIsInstance(fw_information.c.initial_version.type, + sqlalchemy.types.String) + self.assertIsInstance(fw_information.c.current_version.type, + sqlalchemy.types.String) + self.assertIsInstance(fw_information.c.last_version_flashed.type, + sqlalchemy.types.String) + self.assertIsInstance(fw_information.c.version.type, + sqlalchemy.types.String) + + nodes = db_utils.get_table(engine, 'nodes') + col_names = [column.name for column in nodes.c] + self.assertIn('firmware_interface', col_names) + self.assertIsInstance(nodes.c.firmware_interface.type, + sqlalchemy.types.String) + + with engine.begin() as connection: + fw_data = {'node_id': data['id'], + 'component': 'bmc', + 'initial_version': 'v1.0.0'} + insert_fw_cmp = fw_information.insert().values(fw_data) + connection.execute(insert_fw_cmp) + fw_cmp_stmt = sqlalchemy.select( + models.FirmwareInformation.initial_version + ).where( + models.FirmwareInformation.node_id == data['id'], + models.FirmwareInformation.component == fw_data['component'] + ) + fw_component = connection.execute(fw_cmp_stmt).first() + self.assertEqual('v1.0.0', fw_component['initial_version']) + def test_upgrade_and_version(self): with patch_with_engine(self.engine): self.migration_api.upgrade('head')