Skip to content
Snippets Groups Projects
Commit 5e76c0b1 authored by Benjamin Bertrand's avatar Benjamin Bertrand
Browse files

Add ansible_group table

- add ansible_groups_hosts_table for many-to-many relationship between
groups and hosts
- add new api endpoint /network/groups

JIRA INFRA-412
parent 4c597037
No related branches found
No related tags found
No related merge requests found
......@@ -133,6 +133,30 @@ def create_interface():
)
@bp.route("/groups")
@jwt_required
def get_ansible_groups():
"""Return ansible groups
.. :quickref: Network; Get Ansible groups
"""
return get_generic_model(models.AnsibleGroup, order_by=models.AnsibleGroup.name)
@bp.route("/groups", methods=["POST"])
@jwt_required
@jwt_groups_accepted("admin")
def create_ansible_groups():
"""Create a new Ansible group
.. :quickref: Network; Create new Ansible group
:jsonparam name: group name
:jsonparam vars: (optional) Ansible variables
"""
return create_generic_model(models.AnsibleGroup, mandatory_fields=("name",))
@bp.route("/hosts")
@jwt_required
def get_hosts():
......@@ -155,6 +179,8 @@ def create_host():
:jsonparam device_type: Physical|Virtual|...
:jsonparam description: (optional) description
:jsonparam items: (optional) list of items ICS id linked to the host
:jsonparam ansible_vars: (optional) Ansible variables
:jsonparam ansible_groups: (optional) list of Ansible groups names
"""
return create_generic_model(models.Host, mandatory_fields=("name", "device_type"))
......
......@@ -149,6 +149,7 @@ def create_app(config=None):
admin.add_view(AdminModelView(models.NetworkScope, db.session))
admin.add_view(NetworkAdmin(models.Network, db.session, endpoint="networks"))
admin.add_view(AdminModelView(models.DeviceType, db.session))
admin.add_view(AdminModelView(models.AnsibleGroup, db.session))
admin.add_view(AdminModelView(models.Host, db.session))
admin.add_view(AdminModelView(models.Interface, db.session))
admin.add_view(AdminModelView(models.Mac, db.session))
......
......@@ -745,6 +745,39 @@ class DeviceType(db.Model):
}
# Table required for Many-to-Many relationships between Ansible groups and hosts
ansible_groups_hosts_table = db.Table(
"ansible_groups_hosts",
db.Column(
"ansible_group_id",
db.Integer,
db.ForeignKey("ansible_group.id"),
primary_key=True,
),
db.Column("host_id", db.Integer, db.ForeignKey("host.id"), primary_key=True),
)
class AnsibleGroup(CreatedMixin, db.Model):
__tablename__ = "ansible_group"
name = db.Column(CIText, nullable=False, unique=True)
vars = db.Column(postgresql.JSONB)
def __str__(self):
return str(self.name)
def to_dict(self):
d = super().to_dict()
d.update(
{
"name": self.name,
"vars": json.dumps(self.vars),
"hosts": [str(host) for host in self.hosts],
}
)
return d
class Host(CreatedMixin, db.Model):
name = db.Column(db.Text, nullable=False, unique=True)
description = db.Column(db.Text)
......@@ -755,6 +788,12 @@ class Host(CreatedMixin, db.Model):
interfaces = db.relationship("Interface", backref="host")
items = db.relationship("Item", backref="host")
ansible_groups = db.relationship(
"AnsibleGroup",
secondary=ansible_groups_hosts_table,
lazy=True,
backref=db.backref("hosts"),
)
def __init__(self, **kwargs):
# Automatically convert device_type as an instance of its class if passed as a string
......@@ -768,6 +807,12 @@ class Host(CreatedMixin, db.Model):
utils.convert_to_model(item, Item, filter="ics_id")
for item in kwargs["items"]
]
# Automatically convert ansible groups to a list of instances if passed as a list of strings
if "ansible_groups" in kwargs:
kwargs["ansible_groups"] = [
utils.convert_to_model(group, AnsibleGroup)
for group in kwargs["ansible_groups"]
]
super().__init__(**kwargs)
@property
......@@ -814,6 +859,7 @@ class Host(CreatedMixin, db.Model):
"items": [str(item) for item in self.items],
"interfaces": [str(interface) for interface in self.interfaces],
"ansible_vars": json.dumps(self.ansible_vars),
"ansible_groups": [str(group) for group in self.ansible_groups],
}
)
return d
......
"""Add ansible_group table
Revision ID: 924d15deb321
Revises: d67f43bbd675
Create Date: 2018-07-11 11:38:38.262279
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
import citext
# revision identifiers, used by Alembic.
revision = "924d15deb321"
down_revision = "d67f43bbd675"
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"ansible_group",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("updated_at", sa.DateTime(), nullable=True),
sa.Column("name", citext.CIText(), nullable=False),
sa.Column("vars", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["user_account.id"],
name=op.f("fk_ansible_group_user_id_user_account"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_ansible_group")),
sa.UniqueConstraint("name", name=op.f("uq_ansible_group_name")),
)
op.create_table(
"ansible_groups_hosts",
sa.Column("ansible_group_id", sa.Integer(), nullable=False),
sa.Column("host_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["ansible_group_id"],
["ansible_group.id"],
name=op.f("fk_ansible_groups_hosts_ansible_group_id_ansible_group"),
),
sa.ForeignKeyConstraint(
["host_id"], ["host.id"], name=op.f("fk_ansible_groups_hosts_host_id_host")
),
sa.PrimaryKeyConstraint(
"ansible_group_id", "host_id", name=op.f("pk_ansible_groups_hosts")
),
)
def downgrade():
op.drop_table("ansible_groups_hosts")
op.drop_table("ansible_group")
......@@ -28,6 +28,7 @@ register(factories.NetworkScopeFactory)
register(factories.NetworkFactory)
register(factories.InterfaceFactory)
register(factories.DeviceTypeFactory)
register(factories.AnsibleGroupFactory)
register(factories.HostFactory)
register(factories.MacFactory)
register(factories.DomainFactory)
......
......@@ -162,6 +162,16 @@ class DeviceTypeFactory(factory.alchemy.SQLAlchemyModelFactory):
name = factory.Sequence(lambda n: f"Type{n}")
class AnsibleGroupFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = models.AnsibleGroup
sqlalchemy_session = common.Session
sqlalchemy_session_persistence = "commit"
name = factory.Sequence(lambda n: f"group{n}")
user = factory.SubFactory(UserFactory)
class HostFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = models.Host
......
......@@ -26,6 +26,7 @@ ENDPOINT_MODEL = {
"network/networks": models.Network,
"network/interfaces": models.Interface,
"network/hosts": models.Host,
"network/groups": models.AnsibleGroup,
"network/macs": models.Mac,
"network/domains": models.Domain,
"network/cnames": models.Cname,
......@@ -1088,6 +1089,47 @@ def test_create_mac_invalid_address(address, client, user_token):
)
def test_get_ansible_groups(client, ansible_group_factory, readonly_token):
# Create some Ansible groups
group1 = ansible_group_factory(vars={"foo": "hello"})
group2 = ansible_group_factory()
response = get(client, f"{API_URL}/network/groups", token=readonly_token)
assert response.status_code == 200
assert len(response.json) == 2
check_input_is_subset_of_response(response, (group1.to_dict(), group2.to_dict()))
def test_create_ansible_group(client, admin_token):
# check that name is mandatory
response = post(client, f"{API_URL}/network/groups", data={}, token=admin_token)
check_response_message(response, "Missing mandatory field 'name'", 422)
data = {"name": "mygroup"}
response = post(client, f"{API_URL}/network/groups", data=data, token=admin_token)
assert response.status_code == 201
assert {"id", "name", "vars", "hosts", "created_at", "updated_at", "user"} == set(
response.json.keys()
)
assert response.json["name"] == data["name"]
# Check that name shall be unique
response = post(client, f"{API_URL}/network/groups", data=data, token=admin_token)
check_response_message(
response,
"(psycopg2.IntegrityError) duplicate key value violates unique constraint",
422,
)
def test_create_ansible_group_with_vars(client, admin_token):
data = {"name": "mygroup", "vars": {"foo": "hello", "mylist": [1, 2, 3]}}
response = post(client, f"{API_URL}/network/groups", data=data, token=admin_token)
assert response.status_code == 201
assert response.json["vars"] == '{"foo": "hello", "mylist": [1, 2, 3]}'
group = models.AnsibleGroup.query.filter_by(name="mygroup").first()
assert group.vars == data["vars"]
def test_get_hosts(client, host_factory, readonly_token):
# Create some hosts
host1 = host_factory()
......@@ -1133,6 +1175,7 @@ def test_create_host(client, device_type_factory, user_token):
"items",
"interfaces",
"ansible_vars",
"ansible_groups",
"created_at",
"updated_at",
"user",
......@@ -1182,6 +1225,23 @@ def test_create_host_with_ansible_vars(client, device_type_factory, user_token):
assert host.ansible_vars == {"foo": "hello", "mylist": [1, 2, 3]}
def test_create_host_with_ansible_groups(
client, device_type_factory, ansible_group_factory, user_token
):
device_type = device_type_factory(name="VirtualMachine")
group1 = ansible_group_factory(name="mygroup")
group2 = ansible_group_factory(name="another")
data = {
"name": "my-host",
"device_type": device_type.name,
"ansible_groups": [group1.name, group2.name],
}
response = post(client, f"{API_URL}/network/hosts", data=data, token=user_token)
assert response.status_code == 201
host = models.Host.query.filter_by(name="my-host").first()
assert host.ansible_groups == [group1, group2]
def test_create_host_as_consultant(
client, item_factory, device_type_factory, consultant_token
):
......
......@@ -166,3 +166,14 @@ def test_tag_validation(tag_factory):
with pytest.raises(ValidationError) as excinfo:
tag = tag_factory(name="My tag")
assert "'My tag' is an invalid tag name" in str(excinfo.value)
def test_ansible_groups(ansible_group_factory, host_factory):
group1 = ansible_group_factory()
group2 = ansible_group_factory()
host1 = host_factory(ansible_groups=[group1])
assert host1.ansible_groups == [group1]
assert group1.hosts == [host1]
host1.ansible_groups.append(group2)
assert host1.ansible_groups == [group1, group2]
assert group2.hosts == [host1]
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment