diff --git a/app/commands.py b/app/commands.py index 6b8b5e6ce3ee8f2521b0e01a77d0e88259267bf5..fd1c33f5ea9acc350b8edda6152938dc5c45b041 100644 --- a/app/commands.py +++ b/app/commands.py @@ -16,9 +16,8 @@ import sqlalchemy as sa from flask import current_app from .extensions import db, ldap_manager from .defaults import defaults -from .models import User from .tasks import TaskWorker -from . import utils, tokens +from . import models, utils, tokens def sync_user(connection, user): @@ -64,7 +63,7 @@ def sync_users(): except ldap3.core.exceptions.LDAPException as e: current_app.logger.warning(f"Failed to connect to the LDAP server: {e}") return - for user in User.query.all(): + for user in models.User.query.all(): sync_user(connection, user) db.session.commit() diff --git a/app/models.py b/app/models.py index e91ebcd089a1deb6b4e0f2c298aa496461310bef..bfa25575d33e78a33d78c837712d4c7809e66d1f 100644 --- a/app/models.py +++ b/app/models.py @@ -798,6 +798,7 @@ class AnsibleGroupType(Enum): class AnsibleGroup(CreatedMixin, db.Model): + __versioned__ = {} __tablename__ = "ansible_group" # Define id here so that it can be used in the primary and secondary join id = db.Column(db.Integer, primary_key=True) @@ -875,6 +876,10 @@ class AnsibleGroup(CreatedMixin, db.Model): class Host(CreatedMixin, db.Model): + __versioned__ = {} + + # id shall be defined here to be used by SQLAlchemy-Continuum + id = db.Column(db.Integer, primary_key=True) name = db.Column(db.Text, nullable=False, unique=True) description = db.Column(db.Text) device_type_id = db.Column( diff --git a/app/templates/network/view_group.html b/app/templates/network/view_group.html index 31e73e7a624260a4dfac80dcc80ff9318cdefb52..1e41b74518a69645997a7a7f2fb82ff67cd47ed6 100644 --- a/app/templates/network/view_group.html +++ b/app/templates/network/view_group.html @@ -41,4 +41,31 @@ </dl> </div> </div> + <h3>History</h3> + <table id="group_version_table" class="table table-hover table-sm"> + <thead> + <tr> + <th>Updated at</th> + <th>Updated by</th> + <th>Name</th> + <th>Variables</th> + <th>Type</th> + <th>Children</th> + <th>Hosts</th> + </tr> + </thead> + <tbody> + {% for version in group.versions | reverse %} + <tr> + <td>{{ version.transaction.issued_at | datetimeformat }}</td> + <td>{{ version.transaction.user }}</td> + <td>{{ version.name }}</td> + <td><pre>{{ version.vars | toyaml }}</pre></td> + <td>{{ version.type }}</td> + <td>{{ link_to_ansible_groups(version.children) }}</td> + <td>{{ link_to_hosts(version._hosts) }}</td> + </tr> + {% endfor %} + </tbody> + </table> {%- endblock %} diff --git a/app/templates/network/view_host.html b/app/templates/network/view_host.html index 589e9247f72d313ac0918856dd4df43c8684a113..908d2af140c50c0ad86bab085349cb2613d25054 100644 --- a/app/templates/network/view_host.html +++ b/app/templates/network/view_host.html @@ -108,4 +108,29 @@ {% endfor %} </tbody> </table> + <h3>History</h3> + <table id="host_version_table" class="table table-hover table-sm"> + <thead> + <tr> + <th>Updated at</th> + <th>Updated by</th> + <th>Name</th> + <th>Device Type</th> + <th>Ansible variables</th> + <th>Ansible groups</th> + </tr> + </thead> + <tbody> + {% for version in host.versions | reverse %} + <tr> + <td>{{ version.transaction.issued_at | datetimeformat }}</td> + <td>{{ version.transaction.user }}</td> + <td>{{ version.name }}</td> + <td>{{ version.device_type }}</td> + <td><pre>{{ version.ansible_vars | toyaml }}</pre></td> + <td>{{ link_to_ansible_groups(version.ansible_groups) }}</td> + </tr> + {% endfor %} + </tbody> + </table> {%- endblock %} diff --git a/migrations/versions/8f9b5c5ed49f_add_versioning_on_host_and_ansiblegroup.py b/migrations/versions/8f9b5c5ed49f_add_versioning_on_host_and_ansiblegroup.py new file mode 100644 index 0000000000000000000000000000000000000000..fc74a8bac90b69d57a17c43073f2c2881807bddd --- /dev/null +++ b/migrations/versions/8f9b5c5ed49f_add_versioning_on_host_and_ansiblegroup.py @@ -0,0 +1,252 @@ +"""Add versioning on Host and AnsibleGroup + +Revision ID: 8f9b5c5ed49f +Revises: 5698c505d70e +Create Date: 2018-08-31 14:45:59.768159 + +""" +import citext +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "8f9b5c5ed49f" +down_revision = "5698c505d70e" +branch_labels = None +depends_on = None + + +def create_transaction(): + """Create a new transaction and return the id""" + conn = op.get_bind() + conn.execute("INSERT INTO transaction (issued_at) values (now())") + result = conn.execute("SELECT id FROM transaction ORDER BY issued_at DESC LIMIT 1") + return result.fetchone()[0] + + +def create_initial_version(version_table, transaction_id): + conn = op.get_bind() + # Get all values from the source table + source_table = version_table.name.replace("_version", "") + result = conn.execute(f"SELECT * from {source_table}") + values = [{key: value for key, value in row.items()} for row in result] + # Add the transaction_id and operation_type + for d in values: + d["transaction_id"] = transaction_id + # INSERT = 0 (sqlalchemy_continuum/operation.py) + d["operation_type"] = 0 + op.bulk_insert(version_table, values) + + +def upgrade(): + ansible_group_type = postgresql.ENUM( + "STATIC", + "NETWORK_SCOPE", + "NETWORK", + "DEVICE_TYPE", + name="ansible_group_type", + create_type=False, + ) + ansible_group_version = op.create_table( + "ansible_group_version", + sa.Column("created_at", sa.DateTime(), autoincrement=False, nullable=True), + sa.Column("updated_at", sa.DateTime(), autoincrement=False, nullable=True), + sa.Column("id", sa.Integer(), autoincrement=False, nullable=False), + sa.Column("name", citext.CIText(), autoincrement=False, nullable=True), + sa.Column( + "vars", + postgresql.JSONB(astext_type=sa.Text()), + autoincrement=False, + nullable=True, + ), + sa.Column("type", ansible_group_type, autoincrement=False, nullable=True), + sa.Column("user_id", sa.Integer(), autoincrement=False, nullable=True), + sa.Column( + "transaction_id", sa.BigInteger(), autoincrement=False, nullable=False + ), + sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), + sa.Column("operation_type", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint( + "id", "transaction_id", name=op.f("pk_ansible_group_version") + ), + ) + op.create_index( + op.f("ix_ansible_group_version_end_transaction_id"), + "ansible_group_version", + ["end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_ansible_group_version_operation_type"), + "ansible_group_version", + ["operation_type"], + unique=False, + ) + op.create_index( + op.f("ix_ansible_group_version_transaction_id"), + "ansible_group_version", + ["transaction_id"], + unique=False, + ) + ansible_groups_hosts_version = op.create_table( + "ansible_groups_hosts_version", + sa.Column( + "ansible_group_id", sa.Integer(), autoincrement=False, nullable=False + ), + sa.Column("host_id", sa.Integer(), autoincrement=False, nullable=False), + sa.Column( + "transaction_id", sa.BigInteger(), autoincrement=False, nullable=False + ), + sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), + sa.Column("operation_type", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint( + "ansible_group_id", + "host_id", + "transaction_id", + name=op.f("pk_ansible_groups_hosts_version"), + ), + ) + op.create_index( + op.f("ix_ansible_groups_hosts_version_end_transaction_id"), + "ansible_groups_hosts_version", + ["end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_ansible_groups_hosts_version_operation_type"), + "ansible_groups_hosts_version", + ["operation_type"], + unique=False, + ) + op.create_index( + op.f("ix_ansible_groups_hosts_version_transaction_id"), + "ansible_groups_hosts_version", + ["transaction_id"], + unique=False, + ) + ansible_groups_parent_child_version = op.create_table( + "ansible_groups_parent_child_version", + sa.Column("parent_group_id", sa.Integer(), autoincrement=False, nullable=False), + sa.Column("child_group_id", sa.Integer(), autoincrement=False, nullable=False), + sa.Column( + "transaction_id", sa.BigInteger(), autoincrement=False, nullable=False + ), + sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), + sa.Column("operation_type", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint( + "parent_group_id", + "child_group_id", + "transaction_id", + name=op.f("pk_ansible_groups_parent_child_version"), + ), + ) + op.create_index( + op.f("ix_ansible_groups_parent_child_version_end_transaction_id"), + "ansible_groups_parent_child_version", + ["end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_ansible_groups_parent_child_version_operation_type"), + "ansible_groups_parent_child_version", + ["operation_type"], + unique=False, + ) + op.create_index( + op.f("ix_ansible_groups_parent_child_version_transaction_id"), + "ansible_groups_parent_child_version", + ["transaction_id"], + unique=False, + ) + host_version = op.create_table( + "host_version", + sa.Column("created_at", sa.DateTime(), autoincrement=False, nullable=True), + sa.Column("updated_at", sa.DateTime(), autoincrement=False, nullable=True), + sa.Column("id", sa.Integer(), autoincrement=False, nullable=False), + sa.Column("name", sa.Text(), autoincrement=False, nullable=True), + sa.Column("description", sa.Text(), autoincrement=False, nullable=True), + sa.Column("device_type_id", sa.Integer(), autoincrement=False, nullable=True), + sa.Column( + "ansible_vars", + postgresql.JSONB(astext_type=sa.Text()), + autoincrement=False, + nullable=True, + ), + sa.Column("user_id", sa.Integer(), autoincrement=False, nullable=True), + sa.Column( + "transaction_id", sa.BigInteger(), autoincrement=False, nullable=False + ), + sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), + sa.Column("operation_type", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint("id", "transaction_id", name=op.f("pk_host_version")), + ) + op.create_index( + op.f("ix_host_version_end_transaction_id"), + "host_version", + ["end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_host_version_operation_type"), + "host_version", + ["operation_type"], + unique=False, + ) + op.create_index( + op.f("ix_host_version_transaction_id"), + "host_version", + ["transaction_id"], + unique=False, + ) + transaction_id = create_transaction() + create_initial_version(ansible_group_version, transaction_id) + create_initial_version(ansible_groups_hosts_version, transaction_id) + create_initial_version(ansible_groups_parent_child_version, transaction_id) + create_initial_version(host_version, transaction_id) + + +def downgrade(): + op.drop_index(op.f("ix_host_version_transaction_id"), table_name="host_version") + op.drop_index(op.f("ix_host_version_operation_type"), table_name="host_version") + op.drop_index(op.f("ix_host_version_end_transaction_id"), table_name="host_version") + op.drop_table("host_version") + op.drop_index( + op.f("ix_ansible_groups_parent_child_version_transaction_id"), + table_name="ansible_groups_parent_child_version", + ) + op.drop_index( + op.f("ix_ansible_groups_parent_child_version_operation_type"), + table_name="ansible_groups_parent_child_version", + ) + op.drop_index( + op.f("ix_ansible_groups_parent_child_version_end_transaction_id"), + table_name="ansible_groups_parent_child_version", + ) + op.drop_table("ansible_groups_parent_child_version") + op.drop_index( + op.f("ix_ansible_groups_hosts_version_transaction_id"), + table_name="ansible_groups_hosts_version", + ) + op.drop_index( + op.f("ix_ansible_groups_hosts_version_operation_type"), + table_name="ansible_groups_hosts_version", + ) + op.drop_index( + op.f("ix_ansible_groups_hosts_version_end_transaction_id"), + table_name="ansible_groups_hosts_version", + ) + op.drop_table("ansible_groups_hosts_version") + op.drop_index( + op.f("ix_ansible_group_version_transaction_id"), + table_name="ansible_group_version", + ) + op.drop_index( + op.f("ix_ansible_group_version_operation_type"), + table_name="ansible_group_version", + ) + op.drop_index( + op.f("ix_ansible_group_version_end_transaction_id"), + table_name="ansible_group_version", + ) + op.drop_table("ansible_group_version")