Commit c7ef78f0 by Szeberényi Imre

Merge branch 'webhook-iac' into 'master'

Webhook iac

See merge request !19
parents 2ca1821d cc827f1f
...@@ -60,7 +60,7 @@ from vm.models import ( ...@@ -60,7 +60,7 @@ from vm.models import (
InstanceTemplate, Lease, InterfaceTemplate, Node, Trait, Instance InstanceTemplate, Lease, InterfaceTemplate, Node, Trait, Instance
) )
from .models import Profile, GroupProfile, Message from .models import Profile, GroupProfile, Message
from .validators import domain_validator, meta_data_validator, user_data_validator from .validators import domain_validator, meta_data_validator, user_data_validator, network_data_validator
LANGUAGES_WITH_CODE = ((l[0], format_lazy("{} ({})",l[1], l[0])) LANGUAGES_WITH_CODE = ((l[0], format_lazy("{} ({})",l[1], l[0]))
for l in LANGUAGES) for l in LANGUAGES)
...@@ -1540,16 +1540,20 @@ class CIDataForm(forms.ModelForm): ...@@ -1540,16 +1540,20 @@ class CIDataForm(forms.ModelForm):
ci_user_data = forms.CharField( ci_user_data = forms.CharField(
widget=forms.Textarea(attrs={'rows': 12, 'style' : 'display:none;'}), widget=forms.Textarea(attrs={'rows': 12, 'style' : 'display:none;'}),
required=False) required=False)
ci_network_config = forms.CharField(
widget=forms.Textarea(attrs={'rows': 5, 'style' : 'display:none;'}),
required=False)
def clean(self): def clean(self):
data = self.cleaned_data data = self.cleaned_data
meta_data_validator(self.instance, data['ci_meta_data']) meta_data_validator(self.instance, data['ci_meta_data'])
user_data_validator(self.instance, data['ci_user_data']) user_data_validator(self.instance, data['ci_user_data'])
network_data_validator(self.instance, data['ci_network_config'])
return super().clean() return super().clean()
class Meta: class Meta:
model = Instance model = Instance
fields = ('ci_meta_data', 'ci_user_data',) fields = ('ci_meta_data', 'ci_user_data', 'ci_network_config',)
class GroupPermissionForm(forms.ModelForm): class GroupPermissionForm(forms.ModelForm):
......
...@@ -5,6 +5,7 @@ from django.contrib.auth.models import Group, User ...@@ -5,6 +5,7 @@ from django.contrib.auth.models import Group, User
from vm.models import Instance, InstanceTemplate, Lease, Interface, Node, InstanceActivity from vm.models import Instance, InstanceTemplate, Lease, Interface, Node, InstanceActivity
from firewall.models import Vlan, Rule from firewall.models import Vlan, Rule
from storage.models import Disk, StorageActivity from storage.models import Disk, StorageActivity
from vm.models.common import Variable
class RuleSerializer(serializers.ModelSerializer): class RuleSerializer(serializers.ModelSerializer):
class Meta: class Meta:
...@@ -50,7 +51,7 @@ class InstanceTemplateSerializer(serializers.ModelSerializer): ...@@ -50,7 +51,7 @@ class InstanceTemplateSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = InstanceTemplate model = InstanceTemplate
fields = [ 'id', 'name', 'description', 'parent', 'owner', 'access_method', 'boot_menu', fields = [ 'id', 'name', 'description', 'parent', 'owner', 'access_method', 'boot_menu',
'lease', 'raw_data', 'cloud_init', 'ci_meta_data', 'ci_user_data', 'system', 'lease', 'raw_data', 'cloud_init', 'ci_network_config', 'ci_meta_data', 'ci_user_data', 'system',
'has_agent', 'num_cores', 'ram_size', 'max_ram_size', 'arch', 'priority', 'disks'] 'has_agent', 'num_cores', 'ram_size', 'max_ram_size', 'arch', 'priority', 'disks']
...@@ -60,6 +61,12 @@ class LeaseSerializer(serializers.ModelSerializer): ...@@ -60,6 +61,12 @@ class LeaseSerializer(serializers.ModelSerializer):
fields = [ 'id', 'name', 'suspend_interval_seconds', 'delete_interval_seconds'] fields = [ 'id', 'name', 'suspend_interval_seconds', 'delete_interval_seconds']
class VariableSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Variable
fields = [ 'id', 'key', 'value', 'url']
class DiskSerializer(serializers.ModelSerializer): class DiskSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Disk model = Disk
...@@ -71,6 +78,7 @@ class InstanceSerializer(serializers.ModelSerializer): ...@@ -71,6 +78,7 @@ class InstanceSerializer(serializers.ModelSerializer):
ipv4addr = serializers.SerializerMethodField('get_ipv4') ipv4addr = serializers.SerializerMethodField('get_ipv4')
ipv6addr = serializers.SerializerMethodField('get_ipv6') ipv6addr = serializers.SerializerMethodField('get_ipv6')
vlans = serializers.SerializerMethodField('get_vlans') vlans = serializers.SerializerMethodField('get_vlans')
macaddr = serializers.SerializerMethodField('get_mac')
#interfaces = serializers.SerializerMethodField('get_interfaces') #interfaces = serializers.SerializerMethodField('get_interfaces')
def get_ipv4(self, i): def get_ipv4(self, i):
...@@ -79,6 +87,9 @@ class InstanceSerializer(serializers.ModelSerializer): ...@@ -79,6 +87,9 @@ class InstanceSerializer(serializers.ModelSerializer):
def get_ipv6(self, i): def get_ipv6(self, i):
return str(i.ipv6) return str(i.ipv6)
def get_mac(self, i):
return str(i.mac).lower()
def get_vlans(self, i): def get_vlans(self, i):
return list(net.vlan.id for net in i.interface_set.all() if net.host) return list(net.vlan.id for net in i.interface_set.all() if net.host)
...@@ -87,13 +98,19 @@ class InstanceSerializer(serializers.ModelSerializer): ...@@ -87,13 +98,19 @@ class InstanceSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Instance model = Instance
fields = ['id', 'name', 'description', 'status', 'owner', 'access_method', 'boot_menu', 'pw', 'is_base', fields = ['id', 'name', 'description', 'status', 'owner', 'access_method', 'boot_menu', 'pw', 'is_base', 'macaddr',
'lease', 'raw_data', 'cloud_init', 'ci_meta_data', 'ci_user_data', 'system', 'req_traits', 'interface_set', 'lease', 'raw_data', 'cloud_init', 'ci_meta_data', 'ci_user_data', 'ci_network_config', 'system', 'req_traits', 'interface_set',
'has_agent', 'num_cores', 'ram_size', 'max_ram_size', 'arch', 'priority', 'disks', 'node', 'ipv4addr', 'ipv6addr', 'vlans'] 'has_agent', 'num_cores', 'ram_size', 'max_ram_size', 'arch', 'priority', 'disks', 'node', 'ipv4addr', 'ipv6addr', 'vlans',
'hookurl']
extra_kwargs = { extra_kwargs = {
'disks': {'required': False, 'allow_empty': True,}, 'disks': {'required': False, 'allow_empty': True,},
'req_traits': {'required': False, 'allow_empty': True,}, 'req_traits': {'required': False, 'allow_empty': True,},
'interface_set': {'required': False, 'allow_empty': True,} 'interface_set': {'required': False, 'allow_empty': True,},
'hookurl': {'required': False },
'ci_network_config': {'required': False, },
'ci_user_data': {'required': False, },
'ci_meta_data': {'required': False, },
'raw_data': {'required': False, },
} }
......
...@@ -1374,3 +1374,8 @@ textarea[name="new_members"] { ...@@ -1374,3 +1374,8 @@ textarea[name="new_members"] {
width: 95%; width: 95%;
height: 350px; height: 350px;
} }
#ace-network-data {
position: absolute;
width: 95%;
height: 150px;
}
\ No newline at end of file
...@@ -1608,3 +1608,9 @@ textarea[name="new_members"] { ...@@ -1608,3 +1608,9 @@ textarea[name="new_members"] {
width: 95%; width: 95%;
height: 350px; height: 350px;
} }
#ace-network-data {
position: absolute;
width: 95%;
height: 150px;
}
\ No newline at end of file
var Websock_native; // not sure var Websock_native; // not sure
$(function() { $(function() {
if ($("#ace-meta-data").length && $("#ace-user-data").length) { if ($("#ace-meta-data").length && $("#ace-user-data").length && $("#ace-network-data").length) {
var meta_data = ace.edit('ace-meta-data', { var meta_data = ace.edit('ace-meta-data', {
mode: "ace/mode/yaml", useWorker: false, mode: "ace/mode/yaml", useWorker: false,
selectionStyle: "text" }); selectionStyle: "text" });
...@@ -18,12 +18,23 @@ $(function() { ...@@ -18,12 +18,23 @@ $(function() {
user_data.getSession().on("change", function () { user_data.getSession().on("change", function () {
textarea_user.val(user_data.getSession().getValue()); textarea_user.val(user_data.getSession().getValue());
}); });
var network_data = ace.edit('ace-network-data', {
mode: "ace/mode/yaml", useWorker: false,
selectionStyle: "text" });
var textarea_network = $('textarea[name="ci_network_config"]');
network_data.getSession().setValue(textarea_network.val())
network_data.getSession().on("change", function () {
textarea_network.val(network_data.getSession().getValue());
});
meta_data.session.setTabSize(4); meta_data.session.setTabSize(4);
meta_data.session.setUseSoftTabs(true); meta_data.session.setUseSoftTabs(true);
user_data.session.setTabSize(4); user_data.session.setTabSize(4);
user_data.session.setUseSoftTabs(true); user_data.session.setUseSoftTabs(true);
network_data.session.setTabSize(4);
network_data.session.setUseSoftTabs(true);
document.getElementById('ace-meta-data').style.fontSize='14px'; document.getElementById('ace-meta-data').style.fontSize='14px';
document.getElementById('ace-user-data').style.fontSize='14px'; document.getElementById('ace-user-data').style.fontSize='14px';
document.getElementById('ace-network-data').style.fontSize='14px';
/* */ /* */
$('#vm-details-cidata-save').click(function(e) { $('#vm-details-cidata-save').click(function(e) {
$.ajax({ $.ajax({
......
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
</div> </div>
{{ form.cloud_init|as_crispy_field }} {{ form.cloud_init|as_crispy_field }}
{{ form.ci_meta_data|as_crispy_field }} {{ form.ci_meta_data|as_crispy_field }}
{{ form.ci_network_config|as_crispy_field }}
{{ form.ci_user_data|as_crispy_field }} {{ form.ci_user_data|as_crispy_field }}
</fieldset> </fieldset>
......
...@@ -59,6 +59,7 @@ ...@@ -59,6 +59,7 @@
</div> </div>
{{ form.cloud_init|as_crispy_field }} {{ form.cloud_init|as_crispy_field }}
{{ form.ci_meta_data|as_crispy_field }} {{ form.ci_meta_data|as_crispy_field }}
{{ form.ci_network_config|as_crispy_field }}
{{ form.ci_user_data|as_crispy_field }} {{ form.ci_user_data|as_crispy_field }}
</fieldset> </fieldset>
<fieldset> <fieldset>
......
...@@ -16,6 +16,9 @@ ...@@ -16,6 +16,9 @@
{{ ci_data.ci_meta_data|as_crispy_field }} {{ ci_data.ci_meta_data|as_crispy_field }}
<div id="ace-meta-data" style="border: 1px solid #ccc;"></div> <div id="ace-meta-data" style="border: 1px solid #ccc;"></div>
<div style="height: 160px;"></div> <div style="height: 160px;"></div>
{{ ci_data.ci_network_config|as_crispy_field }}
<div id="ace-network-data" style="border: 1px solid #ccc;"></div>
<div style="height: 160px;"></div>
{{ ci_data.ci_user_data|as_crispy_field }} {{ ci_data.ci_user_data|as_crispy_field }}
<div id="ace-user-data" style="border: 1px solid #ccc;"></div> <div id="ace-user-data" style="border: 1px solid #ccc;"></div>
<div style="height: 360px;"></div> <div style="height: 360px;"></div>
......
...@@ -68,6 +68,7 @@ from .views import ( ...@@ -68,6 +68,7 @@ from .views import (
EnableTwoFactorView, DisableTwoFactorView, EnableTwoFactorView, DisableTwoFactorView,
AclUserGroupAutocomplete, AclUserAutocomplete, AclUserGroupAutocomplete, AclUserAutocomplete,
RescheduleView, GroupImportView, GroupExportView, RescheduleView, GroupImportView, GroupExportView,
VariableREST, GetVariableREST,
) )
from .views.node import node_ops, NodeREST, GetNodeREST from .views.node import node_ops, NodeREST, GetNodeREST
from .views.vm import vm_ops, vm_mass_ops from .views.vm import vm_ops, vm_mass_ops
...@@ -83,6 +84,8 @@ urlpatterns = [ ...@@ -83,6 +84,8 @@ urlpatterns = [
path('acpi/group/', GroupREST.as_view()), path('acpi/group/', GroupREST.as_view()),
path('acpi/group/<int:pk>/', GetGroupREST.as_view()), path('acpi/group/<int:pk>/', GetGroupREST.as_view()),
path('acpi/vm/', InstanceREST.as_view()), path('acpi/vm/', InstanceREST.as_view()),
path('acpi/var/', VariableREST.as_view()),
path('acpi/var/<int:pk>/', GetVariableREST.as_view(), name='variable-detail'),
path('acpi/node/', NodeREST.as_view()), path('acpi/node/', NodeREST.as_view()),
path('acpi/node/<int:pk>/', GetNodeREST.as_view()), path('acpi/node/<int:pk>/', GetNodeREST.as_view()),
path('acpi/vm/<int:pk>/', GetInstanceREST.as_view()), path('acpi/vm/<int:pk>/', GetInstanceREST.as_view()),
......
...@@ -90,6 +90,23 @@ def user_data_validator(instance, value): ...@@ -90,6 +90,23 @@ def user_data_validator(instance, value):
except jinja2.exceptions.TemplateError as exc: except jinja2.exceptions.TemplateError as exc:
raise ValidationError(exc.message) raise ValidationError(exc.message)
def network_data_validator(instance, value):
try:
instance.validate_ci_data(value)
except yaml.YAMLError as exc:
if hasattr(exc, 'problem_mark'):
if exc.context != None:
raise ValidationError(' parser says\n' + str(exc.problem_mark) + '\n ' +
str(exc.problem) + ' ' + str(exc.context) +
'\nPlease correct data and retry.')
else:
raise ValidationError(' parser says\n' + str(exc.problem_mark) + '\n ' +
str(exc.problem) + '\nPlease correct data and retry.')
else:
raise ValidationError("Something went wrong while parsing yaml file")
except jinja2.exceptions.TemplateError as exc:
raise ValidationError(exc.message)
def connect_command_template_validator(value): def connect_command_template_validator(value):
"""Validate value as a connect command template. """Validate value as a connect command template.
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
import json import json
import logging import logging
from multiprocessing import context
import re import re
from collections import OrderedDict from collections import OrderedDict
from urllib.parse import urljoin from urllib.parse import urljoin
...@@ -56,9 +57,10 @@ from rest_framework.views import APIView ...@@ -56,9 +57,10 @@ from rest_framework.views import APIView
from rest_framework.parsers import JSONParser from rest_framework.parsers import JSONParser
from rest_framework.authentication import TokenAuthentication, BasicAuthentication from rest_framework.authentication import TokenAuthentication, BasicAuthentication
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
from vm.models.common import Variable
from storage.models import StorageActivity from storage.models import StorageActivity
from dashboard.serializers import InstanceActivitySerializer, StorageActivitySerializer from dashboard.serializers import InstanceActivitySerializer, StorageActivitySerializer, VariableSerializer
from common.models import HumanReadableException, HumanReadableObject from common.models import HumanReadableException, HumanReadableObject
from ..models import GroupProfile, Profile from ..models import GroupProfile, Profile
...@@ -107,6 +109,40 @@ class GetStorageActivityREST(APIView): ...@@ -107,6 +109,40 @@ class GetStorageActivityREST(APIView):
return JsonResponse(serializer.data, safe=False) return JsonResponse(serializer.data, safe=False)
class VariableREST(APIView):
authentication_classes = [TokenAuthentication,BasicAuthentication]
permission_classes = [IsAdminUser]
def get(self, request, format=None):
if request.query_params.get('key'):
try:
template = Variable.objects.filter(key__istartswith=request.query_params.get('key')).get()
serializer = VariableSerializer(template, many=False, context={'request': request})
return JsonResponse(serializer.data, safe=False)
except:
return JsonResponse({}, status=404)
templates = Variable.objects.all()
serializer = VariableSerializer(templates, many=True, context={'request': request})
return JsonResponse(serializer.data, safe=False)
def post(self, request, format=None):
data = JSONParser().parse(request)
serializer = VariableSerializer(data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, status=201)
return JsonResponse(serializer.errors, status=400)
class GetVariableREST(APIView):
authentication_classes = [TokenAuthentication,BasicAuthentication]
permission_classes = [IsAdminUser]
def get(self, request, pk, format=None):
act = Variable.objects.get(pk=pk)
serializer = VariableSerializer(act, many=False, context={'request': request})
return JsonResponse(serializer.data, safe=False)
class RedirectToLoginMixin(AccessMixin): class RedirectToLoginMixin(AccessMixin):
redirect_exception_classes = (PermissionDenied, ) redirect_exception_classes = (PermissionDenied, )
......
...@@ -448,14 +448,14 @@ class Disk(TimeStampedModel): ...@@ -448,14 +448,14 @@ class Disk(TimeStampedModel):
return result return result
@classmethod @classmethod
def create_ci_disk(cls, meta_data, user_data, user = None, **params): def create_ci_disk(cls, meta_data, user_data, network_data, user = None, **params):
params.setdefault('name', 'ci-disk') params.setdefault('name', 'ci-disk')
params.setdefault('type', 'raw-ro') params.setdefault('type', 'raw-ro')
params.setdefault('size', None) params.setdefault('size', None)
disk = cls.__create(params=params, user=user) disk = cls.__create(params=params, user=user)
queue_name = disk.get_remote_queue_name('storage', priority="fast") queue_name = disk.get_remote_queue_name('storage', priority="fast")
disk_desc = disk.get_disk_desc() disk_desc = disk.get_disk_desc()
result = storage_tasks.create_ci_disk.apply_async(args=[disk_desc, meta_data, user_data], result = storage_tasks.create_ci_disk.apply_async(args=[disk_desc, meta_data, user_data, network_data],
queue=queue_name queue=queue_name
).get(timeout=15) ).get(timeout=15)
disk.size = result['size'] disk.size = result['size']
......
...@@ -34,7 +34,7 @@ def create(disk_desc): ...@@ -34,7 +34,7 @@ def create(disk_desc):
@celery.task(name='storagedriver.create_ci_disk') @celery.task(name='storagedriver.create_ci_disk')
def create_ci_disk(disk_desc, meta_data, user_data): def create_ci_disk(disk_desc, meta_data, user_data, network_data):
pass pass
......
...@@ -17,6 +17,8 @@ ...@@ -17,6 +17,8 @@
from django.contrib import admin from django.contrib import admin
from .models.common import Variable
from .models import (Instance, InstanceActivity, InstanceTemplate, Interface, from .models import (Instance, InstanceActivity, InstanceTemplate, Interface,
InterfaceTemplate, Lease, NamedBaseResourceConfig, Node, InterfaceTemplate, Lease, NamedBaseResourceConfig, Node,
NodeActivity, Trait) NodeActivity, Trait)
...@@ -36,3 +38,4 @@ admin.site.register(NamedBaseResourceConfig) ...@@ -36,3 +38,4 @@ admin.site.register(NamedBaseResourceConfig)
admin.site.register(Node) admin.site.register(Node)
admin.site.register(NodeActivity) admin.site.register(NodeActivity)
admin.site.register(Trait) admin.site.register(Trait)
admin.site.register(Variable)
# Generated by Django 3.2.3 on 2022-10-04 09:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('vm', '0010_auto_20220914_1532'),
]
operations = [
migrations.AddField(
model_name='instance',
name='hookurl',
field=models.CharField(blank=True, help_text='If expiration due, call remote url.', max_length=250, verbose_name='hookurl'),
),
]
# Generated by Django 3.2.3 on 2022-10-06 14:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('vm', '0011_instance_hookurl'),
]
operations = [
migrations.AddField(
model_name='instance',
name='ci_network_config',
field=models.TextField(blank=True, default='#cloud-config\n\nusers:\n - name: {{ sysuser }} \n sudo: [\'ALL=(ALL) NOPASSWD:ALL\']\n groups: sudo\n shell: /bin/bash\n ssh_pwauth: True\n chpasswd: { expire: False }\n lock-passwd: false\n passwd: "{{ password | hash }}"', help_text='When cloud-init is active, set network-config(YAML format)', verbose_name='CI Network Data'),
),
migrations.AddField(
model_name='instancetemplate',
name='ci_network_config',
field=models.TextField(blank=True, default='#cloud-config\n\nusers:\n - name: {{ sysuser }} \n sudo: [\'ALL=(ALL) NOPASSWD:ALL\']\n groups: sudo\n shell: /bin/bash\n ssh_pwauth: True\n chpasswd: { expire: False }\n lock-passwd: false\n passwd: "{{ password | hash }}"', help_text='When cloud-init is active, set network-config(YAML format)', verbose_name='CI Network Data'),
),
]
# Generated by Django 3.2.3 on 2022-10-06 15:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('vm', '0012_auto_20221006_1433'),
]
operations = [
migrations.AlterField(
model_name='instance',
name='ci_network_config',
field=models.TextField(blank=True, default="network:\n version: 2\n ethernets:\n ens3:\n match:\n macaddress: 'mac'\n addresses: \n - ipv4/24\n gateway4: ipv4\n nameservers:\n addresses:\n - 8.8.8.8", help_text='When cloud-init is active, set network-config(YAML format)', verbose_name='CI Network Data'),
),
migrations.AlterField(
model_name='instancetemplate',
name='ci_network_config',
field=models.TextField(blank=True, default="network:\n version: 2\n ethernets:\n ens3:\n match:\n macaddress: 'mac'\n addresses: \n - ipv4/24\n gateway4: ipv4\n nameservers:\n addresses:\n - 8.8.8.8", help_text='When cloud-init is active, set network-config(YAML format)', verbose_name='CI Network Data'),
),
]
# Generated by Django 3.2.3 on 2022-10-10 14:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('vm', '0013_auto_20221006_1509'),
]
operations = [
migrations.CreateModel(
name='Variable',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(max_length=100, unique=True, verbose_name='variable name')),
('value', models.CharField(max_length=150, verbose_name='variable value')),
],
),
migrations.AlterField(
model_name='instance',
name='ci_network_config',
field=models.TextField(blank=True, default='network:\n version: 2\n ethernets:\n ens3:\n match:\n macaddress: {{ net.mac }}\n addresses: \n - {{ net.ipv4 }}/24\n gateway4: ipv4\n nameservers:\n addresses:\n - 8.8.8.8', help_text='When cloud-init is active, set network-config(YAML format)', verbose_name='CI Network Data'),
),
migrations.AlterField(
model_name='instance',
name='ci_user_data',
field=models.TextField(blank=True, default="#cloud-config\n\nssh_pwauth: 1\n\nusers:\n - name: {{ sysuser }} \n sudo: ['ALL=(ALL) NOPASSWD:ALL']\n groups: sudo\n shell: /bin/bash\n chpasswd: { expire: False }\n lock-passwd: false\nchpasswd:\n list: |\n {{ sysuser }}:{{ password }}\n expire: False", help_text='When cloud-init is active, set user-data (YAML format)', verbose_name='CI User Data'),
),
migrations.AlterField(
model_name='instancetemplate',
name='ci_network_config',
field=models.TextField(blank=True, default='network:\n version: 2\n ethernets:\n ens3:\n match:\n macaddress: {{ net.mac }}\n addresses: \n - {{ net.ipv4 }}/24\n gateway4: ipv4\n nameservers:\n addresses:\n - 8.8.8.8', help_text='When cloud-init is active, set network-config(YAML format)', verbose_name='CI Network Data'),
),
migrations.AlterField(
model_name='instancetemplate',
name='ci_user_data',
field=models.TextField(blank=True, default="#cloud-config\n\nssh_pwauth: 1\n\nusers:\n - name: {{ sysuser }} \n sudo: ['ALL=(ALL) NOPASSWD:ALL']\n groups: sudo\n shell: /bin/bash\n chpasswd: { expire: False }\n lock-passwd: false\nchpasswd:\n list: |\n {{ sysuser }}:{{ password }}\n expire: False", help_text='When cloud-init is active, set user-data (YAML format)', verbose_name='CI User Data'),
),
]
...@@ -33,6 +33,14 @@ ARCHITECTURES = (('x86_64', 'x86-64 (64 bit)'), ...@@ -33,6 +33,14 @@ ARCHITECTURES = (('x86_64', 'x86-64 (64 bit)'),
('i686', 'x86 (32 bit)')) ('i686', 'x86 (32 bit)'))
class Variable(Model):
key = CharField(verbose_name='variable name', unique=True, max_length=100)
value = CharField(verbose_name='variable value', max_length=150)
def __str__(self):
return self.key
class BaseResourceConfigModel(Model): class BaseResourceConfigModel(Model):
"""Abstract base for models with base resource configuration parameters. """Abstract base for models with base resource configuration parameters.
......
...@@ -23,6 +23,7 @@ from functools import partial ...@@ -23,6 +23,7 @@ from functools import partial
from importlib import import_module from importlib import import_module
from ipaddress import ip_interface from ipaddress import ip_interface
from logging import getLogger from logging import getLogger
from urllib import request
from warnings import warn from warnings import warn
from xml.dom.minidom import Text from xml.dom.minidom import Text
...@@ -60,7 +61,7 @@ from common.models import ( ...@@ -60,7 +61,7 @@ from common.models import (
from common.operations import OperatedMixin from common.operations import OperatedMixin
from ..tasks import agent_tasks from ..tasks import agent_tasks
from .activity import (ActivityInProgressError, InstanceActivity) from .activity import (ActivityInProgressError, InstanceActivity)
from .common import BaseResourceConfigModel, Lease from .common import BaseResourceConfigModel, Lease, Variable
from .network import Interface from .network import Interface
from .node import Node, Trait from .node import Node, Trait
...@@ -87,15 +88,34 @@ platform: circle3 ...@@ -87,15 +88,34 @@ platform: circle3
CI_USER_DATA_DEF = """ CI_USER_DATA_DEF = """
#cloud-config #cloud-config
ssh_pwauth: 1
users: users:
- name: {{ sysuser }} - name: {{ sysuser }}
sudo: ['ALL=(ALL) NOPASSWD:ALL'] sudo: ['ALL=(ALL) NOPASSWD:ALL']
groups: sudo groups: sudo
shell: /bin/bash shell: /bin/bash
ssh_pwauth: True
chpasswd: { expire: False } chpasswd: { expire: False }
lock-passwd: false lock-passwd: false
passwd: "{{ password | hash }}" chpasswd:
list: |
{{ sysuser }}:{{ password }}
expire: False
""".strip()
CI_NETWORK_CONFIG_DEF = """
network:
version: 2
ethernets:
ens3:
match:
macaddress: {{ net.mac }}
addresses:
- {{ net.ipv4 }}/24
gateway4: ipv4
nameservers:
addresses:
- 8.8.8.8
""".strip() """.strip()
try: try:
...@@ -150,6 +170,8 @@ class VirtualMachineDescModel(BaseResourceConfigModel): ...@@ -150,6 +170,8 @@ class VirtualMachineDescModel(BaseResourceConfigModel):
'When cloud-init is active, set meta-data (YAML format)'), default=CI_META_DATA_DEF) 'When cloud-init is active, set meta-data (YAML format)'), default=CI_META_DATA_DEF)
ci_user_data = TextField(verbose_name=_('CI User Data'), blank=True, help_text=_( ci_user_data = TextField(verbose_name=_('CI User Data'), blank=True, help_text=_(
'When cloud-init is active, set user-data (YAML format)'), default=CI_USER_DATA_DEF) 'When cloud-init is active, set user-data (YAML format)'), default=CI_USER_DATA_DEF)
ci_network_config = TextField(verbose_name=_('CI Network Data'), blank=True, help_text=_(
'When cloud-init is active, set network-config(YAML format)'), default=CI_NETWORK_CONFIG_DEF)
req_traits = ManyToManyField(Trait, blank=True, req_traits = ManyToManyField(Trait, blank=True,
help_text=_("A set of traits required for a " help_text=_("A set of traits required for a "
"node to declare to be suitable " "node to declare to be suitable "
...@@ -295,13 +317,13 @@ class NetTemplate: ...@@ -295,13 +317,13 @@ class NetTemplate:
self.ipv4 = str(net.host.ipv4) self.ipv4 = str(net.host.ipv4)
self.ipv6 = str(net.host.ipv6) self.ipv6 = str(net.host.ipv6)
self.name = str(net.vlan.name) self.name = str(net.vlan.name)
self.mac = str(net.host.mac)
def __init__(self, instance): def __init__(self, instance):
self.vlans = list(NetTemplate.Host(net) for net in instance.interface_set.all() if net.host) self.vlans = list(NetTemplate.Host(net) for net in instance.interface_set.all() if net.host)
self.ipv4 = str(instance.ipv4) self.ipv4 = str(instance.ipv4)
self.ipv6 = str(instance.ipv6) self.ipv6 = str(instance.ipv6)
self.mac = str(instance.mac)
class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
...@@ -325,6 +347,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -325,6 +347,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
) )
name = CharField(blank=True, max_length=100, verbose_name=_('name'), name = CharField(blank=True, max_length=100, verbose_name=_('name'),
help_text=_("Human readable name of instance.")) help_text=_("Human readable name of instance."))
hookurl = CharField(blank=True, max_length=250, verbose_name=_('hookurl'),
help_text=_("If expiration due, call remote url."))
description = TextField(blank=True, verbose_name=_('description')) description = TextField(blank=True, verbose_name=_('description'))
template = ForeignKey(InstanceTemplate, blank=True, null=True, template = ForeignKey(InstanceTemplate, blank=True, null=True,
related_name='instance_set', on_delete=SET_NULL, related_name='instance_set', on_delete=SET_NULL,
...@@ -421,6 +445,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -421,6 +445,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
return self.status return self.status
def get_ci_data_dict(self): def get_ci_data_dict(self):
list = Variable.objects.all()
vars = { }
for var in list:
vars[var.key] = var.value
datas = { datas = {
"sysuser": "cloud", "sysuser": "cloud",
"hostname": self.short_hostname, "hostname": self.short_hostname,
...@@ -430,6 +458,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -430,6 +458,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
"acl": AclTemplate(self), "acl": AclTemplate(self),
"ssh": SSHKeyTemplate(self), "ssh": SSHKeyTemplate(self),
"ci": CITemplate(), "ci": CITemplate(),
"variables": vars,
} }
return datas return datas
...@@ -447,6 +476,13 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -447,6 +476,13 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
template = env.from_string(data) template = env.from_string(data)
return template.render(ci_datas) return template.render(ci_datas)
@property
def get_network_config(self):
data = str(self.ci_network_config)
ci_datas = self.get_ci_data_dict()
template = env.from_string(data)
return template.render(ci_datas)
def validate_ci_data(self, data): def validate_ci_data(self, data):
ci_datas = self.get_ci_data_dict() ci_datas = self.get_ci_data_dict()
template = env.from_string(data) template = env.from_string(data)
...@@ -454,6 +490,16 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -454,6 +490,16 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
yaml.dump(yaml.load(data, Loader=yaml.Loader)) yaml.dump(yaml.load(data, Loader=yaml.Loader))
return True return True
def callhookurl(self):
import requests as req
if self.hookurl:
logger.info("notify remote host (callhookurl): " + str(self.hookurl))
try:
req.post(url=str(self.hookurl))
except:
logger.info("Error when call hookurl: " + str(self.hookurl))
def _update_status(self): def _update_status(self):
"""Set the proper status of the instance to Instance.status. """Set the proper status of the instance to Instance.status.
""" """
......
...@@ -539,14 +539,16 @@ class DeployOperation(InstanceOperation): ...@@ -539,14 +539,16 @@ class DeployOperation(InstanceOperation):
import json import json
logger.info('create ci image') logger.info('create ci image')
disk = Disk.create_ci_disk(meta_data=self.instance.get_meta_data, disk = Disk.create_ci_disk(meta_data=self.instance.get_meta_data,
user_data=self.instance.get_user_data) user_data=self.instance.get_user_data,
network_data=self.instance.get_network_config)
disk.full_clean() disk.full_clean()
disk.save() disk.save()
self.instance.disks.add(disk) self.instance.disks.add(disk)
return create_readable(ugettext_noop( return create_readable(ugettext_noop(
" ---- META-DATA ----\n%(meta_data)s\n\n ---- USER-DATA ----\n%(user_data)s"), " ---- META-DATA ----\n%(meta_data)s\n\n ---- NETWORK-CONFIG ----\n%(network_data)s\n\n ---- USER-DATA ----\n%(user_data)s"),
meta_data=self.instance.get_meta_data, meta_data=self.instance.get_meta_data,
user_data=self.instance.get_user_data) user_data=self.instance.get_user_data,
network_data=self.instance.get_network_config)
@register_operation @register_operation
class DeployDisksOperation(SubOperationMixin, InstanceOperation): class DeployDisksOperation(SubOperationMixin, InstanceOperation):
...@@ -616,6 +618,9 @@ class DestroyOperation(InstanceOperation): ...@@ -616,6 +618,9 @@ class DestroyOperation(InstanceOperation):
self.instance.destroyed_at = timezone.now() self.instance.destroyed_at = timezone.now()
self.instance.save() self.instance.save()
logger.info('destroy vm operation ' + str(system))
if system:
self.instance.callhookurl()
@register_operation @register_operation
class DeleteVmOperation(SubOperationMixin, RemoteInstanceOperation): class DeleteVmOperation(SubOperationMixin, RemoteInstanceOperation):
...@@ -1027,6 +1032,9 @@ class SleepOperation(InstanceOperation): ...@@ -1027,6 +1032,9 @@ class SleepOperation(InstanceOperation):
self.instance.shutdown_net() self.instance.shutdown_net()
self.instance._suspend_vm(parent_activity=activity) self.instance._suspend_vm(parent_activity=activity)
self.instance.yield_node() self.instance.yield_node()
logger.info('sleep vm operation ' + str(system))
if system:
self.instance.callhookurl()
@register_operation @register_operation
class SuspendVmOperation(SubOperationMixin, RemoteInstanceOperation): class SuspendVmOperation(SubOperationMixin, RemoteInstanceOperation):
...@@ -1498,7 +1506,7 @@ class CIDataChangeOperation(InstanceOperation): ...@@ -1498,7 +1506,7 @@ class CIDataChangeOperation(InstanceOperation):
accept_states = ('STOPPED', 'PENDING') accept_states = ('STOPPED', 'PENDING')
def _operation(self, user, activity, def _operation(self, user, activity,
ci_meta_data, ci_user_data, ci_meta_data, ci_user_data, ci_network_config,
with_shutdown=False, task=None): with_shutdown=False, task=None):
logger.debug('save cloud-init: %s \n %s', ci_meta_data, ci_user_data) logger.debug('save cloud-init: %s \n %s', ci_meta_data, ci_user_data)
if self.instance.status == 'RUNNING' and not with_shutdown: if self.instance.status == 'RUNNING' and not with_shutdown:
...@@ -1513,6 +1521,7 @@ class CIDataChangeOperation(InstanceOperation): ...@@ -1513,6 +1521,7 @@ class CIDataChangeOperation(InstanceOperation):
self.instance.ci_meta_data = ci_meta_data self.instance.ci_meta_data = ci_meta_data
self.instance.ci_user_data = ci_user_data self.instance.ci_user_data = ci_user_data
self.instance.ci_network_config = ci_network_config
self.instance.full_clean() self.instance.full_clean()
self.instance.save() self.instance.save()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment