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

Add ansible_vars column on host table

Allow to enter Ansible variables on host in YAML format.
Use codemirror (http://codemirror.net) to make editing YAML easier.

JIRA INFRA-412
parent fb7f7f05
No related branches found
No related tags found
No related merge requests found
......@@ -57,6 +57,7 @@ def create_app(config=None):
app.config.update(config or {})
app.jinja_env.filters["datetimeformat"] = utils.format_datetime
app.jinja_env.filters["toyaml"] = utils.pretty_yaml
if not app.debug:
import logging
......
# -*- coding: utf-8 -*-
"""
app.fields
~~~~~~~~~~
This module defines extra WTForms fields
:copyright: (c) 2018 European Spallation Source ERIC
:license: BSD 2-Clause, see LICENSE for more details.
"""
import yaml
from wtforms import TextAreaField
class YAMLField(TextAreaField):
"""This field represents an HTML ``<textarea>`` used to input YAML"""
def _value(self):
return yaml.safe_dump(self.data, default_flow_style=False) if self.data else ""
def process_formdata(self, valuelist):
if valuelist:
try:
self.data = yaml.safe_load(valuelist[0])
except yaml.YAMLError:
raise ValueError("This field contains invalid YAML")
else:
self.data = None
def pre_validate(self, form):
super().pre_validate(form)
if self.data:
try:
yaml.safe_dump(self.data)
except yaml.YAMLError:
raise ValueError("This field contains invalid YAML")
......@@ -10,6 +10,7 @@ This module implements the models used in the app.
"""
import ipaddress
import json
import string
import qrcode
import itertools
......@@ -750,6 +751,7 @@ class Host(CreatedMixin, db.Model):
device_type_id = db.Column(
db.Integer, db.ForeignKey("device_type.id"), nullable=False
)
ansible_vars = db.Column(postgresql.JSONB)
interfaces = db.relationship("Interface", backref="host")
items = db.relationship("Item", backref="host")
......@@ -811,6 +813,7 @@ class Host(CreatedMixin, db.Model):
"description": self.description,
"items": [str(item) for item in self.items],
"interfaces": [str(interface) for interface in self.interfaces],
"ansible_vars": json.dumps(self.ansible_vars),
}
)
return d
......
......@@ -31,6 +31,7 @@ from ..validators import (
MAC_ADDRESS_RE,
NoValidateSelectField,
)
from ..fields import YAMLField
from .. import utils, models
......@@ -139,6 +140,10 @@ class HostForm(CSEntryForm):
)
description = TextAreaField("Description")
device_type_id = SelectField("Device Type")
ansible_vars = YAMLField(
"Ansible vars",
description="Enter variables in YAML format. See https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html",
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
......
......@@ -67,6 +67,7 @@ def create_host():
name=form.name.data,
device_type_id=form.device_type_id.data,
description=form.description.data or None,
ansible_vars=form.ansible_vars.data or None,
)
# The total number of tags will always be quite small
# It's more efficient to retrieve all of them in one query
......@@ -138,6 +139,7 @@ def edit_host(name):
host.name = form.name.data
host.device_type_id = form.device_type_id.data
host.description = form.description.data or None
host.ansible_vars = form.ansible_vars.data or None
current_app.logger.debug(f"Trying to update: {host!r}")
try:
db.session.commit()
......
$(document).ready(function() {
if( $("#hostForm").length || $("#editHostForm").length ) {
var hostVarsEditor = CodeMirror.fromTextArea(ansible_vars, {
lineNumbers: true,
mode: "yaml"
});
hostVarsEditor.setSize(null, 120);
}
function set_default_ip() {
// Retrieve the first available IP for the selected network
// and update the IP field
......
......@@ -16,6 +16,7 @@
{{ render_field(form.mac_address) }}
{{ render_field(form.cnames_string) }}
{{ render_field(form.tags, class_="selectpicker") }}
{{ render_field(form.ansible_vars) }}
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-primary">Submit</button>
......
......@@ -21,6 +21,7 @@
{{ render_field(form.name, class_="text-lowercase") }}
{{ render_field(form.device_type_id) }}
{{ render_field(form.description) }}
{{ render_field(form.ansible_vars) }}
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-primary">Submit</button>
......
......@@ -38,6 +38,10 @@
<dd class="col-sm-9">{{ host.user }}</dd>
<dt class="col-sm-3">Created at</dt>
<dd class="col-sm-9">{{ host.created_at | datetimeformat }}</dd>
{% if host.ansible_vars %}
<dt class="col-sm-3">Ansible vars</dt>
<dd class="col-sm-9"><pre>{{ host.ansible_vars | toyaml }}</pre></dd>
{% endif %}
</dl>
</div>
{% if host.device_type.name.startswith('Virtual') and current_user.is_admin %}
......
......@@ -15,6 +15,7 @@ import io
import random
import sqlalchemy as sa
import dateutil.parser
import yaml
from flask import current_app
from flask.globals import _app_ctx_stack, _request_ctx_stack
from flask_login import current_user
......@@ -225,6 +226,14 @@ def format_datetime(value, format="%Y-%m-%d %H:%M"):
return value.strftime(format)
def pretty_yaml(value):
"""Pretty print yaml
Function used as a jinja2 filter
"""
return yaml.safe_dump(value, default_flow_style=False)
def trigger_core_services_update():
"""Trigger a job to update the core services on the TN (DNS and DHCP)
......
"""Add ansible_vars column
Revision ID: d67f43bbd675
Revises: a9442567c6dc
Create Date: 2018-07-10 11:39:17.955468
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "d67f43bbd675"
down_revision = "a9442567c6dc"
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
"host",
sa.Column(
"ansible_vars", postgresql.JSONB(astext_type=sa.Text()), nullable=True
),
)
def downgrade():
op.drop_column("host", "ansible_vars")
......@@ -1098,6 +1098,13 @@ def test_get_hosts(client, host_factory, readonly_token):
check_input_is_subset_of_response(response, (host1.to_dict(), host2.to_dict()))
def test_get_hosts_with_ansible_vars(client, host_factory, readonly_token):
host_factory(ansible_vars={"foo": "hello", "mylist": [1, 2, 3]})
response = get(client, f"{API_URL}/network/hosts", token=readonly_token)
assert response.status_code == 200
assert response.json[0]["ansible_vars"] == '{"foo": "hello", "mylist": [1, 2, 3]}'
def test_create_host(client, device_type_factory, user_token):
device_type = device_type_factory(name="Virtual")
# check that name and device_type are mandatory
......@@ -1125,6 +1132,7 @@ def test_create_host(client, device_type_factory, user_token):
"description",
"items",
"interfaces",
"ansible_vars",
"created_at",
"updated_at",
"user",
......@@ -1160,6 +1168,20 @@ def test_create_host_with_items(client, item_factory, device_type_factory, user_
assert models.Item.query.get(item2.id).host_id == host.id
def test_create_host_with_ansible_vars(client, device_type_factory, user_token):
device_type = device_type_factory(name="VirtualMachine")
data = {
"name": "my-host",
"device_type": device_type.name,
"ansible_vars": {"foo": "hello", "mylist": [1, 2, 3]},
}
response = post(client, f"{API_URL}/network/hosts", data=data, token=user_token)
assert response.status_code == 201
assert response.json["ansible_vars"] == '{"foo": "hello", "mylist": [1, 2, 3]}'
host = models.Host.query.filter_by(name="my-host").first()
assert host.ansible_vars == {"foo": "hello", "mylist": [1, 2, 3]}
def test_create_host_as_consultant(
client, item_factory, device_type_factory, consultant_token
):
......
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