From 0c382a73e5e0462b4bd598734159626f14f3a96e Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 8 Jun 2024 07:17:20 +0930 Subject: [PATCH] feat(config_management): assign software action to config group !22 #43 --- app/app/helpers/merge_software.py | 33 +++++ .../forms/group/add_software.py | 21 +++ .../forms/group/change_software.py | 22 +++ .../migrations/0004_configgroupsoftware.py | 36 +++++ app/config_management/models/groups.py | 90 +++++++++++- .../templates/config_management/group.html.j2 | 39 +++++ app/config_management/urls.py | 8 +- app/config_management/views/groups/groups.py | 5 +- .../views/groups/software.py | 137 ++++++++++++++++++ 9 files changed, 388 insertions(+), 3 deletions(-) create mode 100644 app/app/helpers/merge_software.py create mode 100644 app/config_management/forms/group/add_software.py create mode 100644 app/config_management/forms/group/change_software.py create mode 100644 app/config_management/migrations/0004_configgroupsoftware.py create mode 100644 app/config_management/views/groups/software.py diff --git a/app/app/helpers/merge_software.py b/app/app/helpers/merge_software.py new file mode 100644 index 00000000..7c02959b --- /dev/null +++ b/app/app/helpers/merge_software.py @@ -0,0 +1,33 @@ + +def merge_software(software: list, new_software: list) -> list: + """ Merge two lists of software actions + + Args: + software (list(dict)): Original list to merge over + new_software (list(dict)): new list to use to merge over + + Returns: + list(dict): merged list of software actions + """ + + merge_software = [] + + merge: dict = {} + + for original in software: + + merge.update({ + original['name']: original + }) + + for new in new_software: + + merge.update({ + new['name']: new + }) + + for key, value in merge.items(): + + merge_software = merge_software + [ value ] + + return merge_software \ No newline at end of file diff --git a/app/config_management/forms/group/add_software.py b/app/config_management/forms/group/add_software.py new file mode 100644 index 00000000..9829ca50 --- /dev/null +++ b/app/config_management/forms/group/add_software.py @@ -0,0 +1,21 @@ +from django import forms +from django.db.models import Q + +from config_management.models.groups import ConfigGroupSoftware +from itam.models.software import Software + + +class SoftwareAdd(forms.ModelForm): + + class Meta: + model = ConfigGroupSoftware + fields = [ + 'software', + 'action' + ] + + def __init__(self, *args, **kwargs): + organizations = kwargs.pop('organizations') + super().__init__(*args, **kwargs) + + self.fields['software'].queryset = Software.objects.filter(Q(organization_id__in=organizations) | Q(is_global = True)) diff --git a/app/config_management/forms/group/change_software.py b/app/config_management/forms/group/change_software.py new file mode 100644 index 00000000..6bca5c33 --- /dev/null +++ b/app/config_management/forms/group/change_software.py @@ -0,0 +1,22 @@ +from django import forms +from django.db.models import Q + +from config_management.models.groups import ConfigGroupSoftware +from itam.models.software import Software, SoftwareVersion + + +class SoftwareUpdate(forms.ModelForm): + + class Meta: + model = ConfigGroupSoftware + fields = [ + 'action', + 'version', + ] + + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + self.fields['version'].queryset = SoftwareVersion.objects.filter(software_id=self.instance.software.id) + diff --git a/app/config_management/migrations/0004_configgroupsoftware.py b/app/config_management/migrations/0004_configgroupsoftware.py new file mode 100644 index 00000000..611b6be2 --- /dev/null +++ b/app/config_management/migrations/0004_configgroupsoftware.py @@ -0,0 +1,36 @@ +# Generated by Django 5.0.6 on 2024-06-07 21:43 + +import access.fields +import access.models +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('access', '0003_alter_team_organization'), + ('config_management', '0003_alter_configgrouphosts_organization_and_more'), + ('itam', '0013_alter_device_organization_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ConfigGroupSoftware', + fields=[ + ('is_global', models.BooleanField(default=False)), + ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), + ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), + ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), + ('action', models.CharField(blank=True, choices=[('1', 'Install'), ('0', 'Remove')], default=None, max_length=1, null=True)), + ('config_group', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='config_management.configgroups')), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])), + ('software', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='itam.software')), + ('version', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.softwareversion')), + ], + options={ + 'ordering': ['-action', 'software'], + }, + ), + ] diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index 38090dbc..7c37f00d 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -7,9 +7,12 @@ from django.forms import ValidationError from access.fields import * from access.models import TenancyObject +from app.helpers.merge_software import merge_software + from core.mixin.history_save import SaveHistory -from itam.models.device import Device +from itam.models.device import Device, DeviceSoftware +from itam.models.software import Software, SoftwareVersion @@ -125,6 +128,43 @@ class ConfigGroups(GroupsCommonFields, SaveHistory): config.update(self.config) + softwares = ConfigGroupSoftware.objects.filter(config_group=self.id) + + software_actions = { + "software": [] + } + + for software in softwares: + + if software.action: + + if int(software.action) == 1: + + state = 'present' + + elif int(software.action) == 0: + + state = 'absent' + + software_action = { + "name": software.software.slug, + "state": state + } + + + if software.version: + software_action['version'] = software.version.name + + software_actions['software'] = software_actions['software'] + [ software_action ] + + if len(software_actions['software']) > 0: # don't add empty software as it prevents parent software from being added + + if 'software' not in config.keys(): + + config['software'] = [] + + config['software'] = merge_software(config['software'], software_actions['software']) + return json.dumps(config) @@ -177,3 +217,51 @@ class ConfigGroupHosts(GroupsCommonFields, SaveHistory): null = False, blank= False ) + + + + +class ConfigGroupSoftware(GroupsCommonFields, SaveHistory): + """ A way to configure software to install/remove per config group """ + + class Meta: + ordering = [ + '-action', + 'software' + ] + + + config_group = models.ForeignKey( + ConfigGroups, + on_delete=models.CASCADE, + default = None, + null = False, + blank= False + ) + + + software = models.ForeignKey( + Software, + on_delete=models.CASCADE, + default = None, + null = False, + blank= False + ) + + action = models.CharField( + max_length=1, + choices=DeviceSoftware.Actions, + default=None, + null=True, + blank = True, + ) + + version = models.ForeignKey( + SoftwareVersion, + on_delete=models.CASCADE, + default = None, + null = True, + blank= True + ) + + diff --git a/app/config_management/templates/config_management/group.html.j2 b/app/config_management/templates/config_management/group.html.j2 index 0c47956d..4c76c426 100644 --- a/app/config_management/templates/config_management/group.html.j2 +++ b/app/config_management/templates/config_management/group.html.j2 @@ -120,6 +120,45 @@ Software + + + + + + + + + + {% if softwares %} + {% for software in softwares %} + + + + + + + + {% endfor %} + {% else %} + + {% endif %} +
NameCategoryActionDesired Version 
{{ software.software }}{{ software.software.category }} + {% url 'Config Management:_group_software_change' group_id=group.id pk=software.id as icon_link %} + {% if software.get_action_display == 'Install' %} + {% include 'icons/success_text.html.j2' with icon_text=software.get_action_display icon_link=icon_link %} + {% elif software.get_action_display == 'Remove'%} + {% include 'icons/cross_text.html.j2' with icon_text=software.get_action_display %} + {% else %} + {% include 'icons/add_link.html.j2' with icon_text='Add' %} + {% endif %} + + {% if software.version %} + {{ software.version }} + {% else %} + - + {% endif %} +  
Nothing Found
+
diff --git a/app/config_management/urls.py b/app/config_management/urls.py index 314eae4d..e332d3c4 100644 --- a/app/config_management/urls.py +++ b/app/config_management/urls.py @@ -1,6 +1,7 @@ from django.urls import path -from config_management.views.groups import GroupIndexView, GroupAdd, GroupDelete, GroupView, GroupHostAdd, GroupHostDelete +from config_management.views.groups.groups import GroupIndexView, GroupAdd, GroupDelete, GroupView, GroupHostAdd, GroupHostDelete +from config_management.views.groups.software import GroupSoftwareAdd, GroupSoftwareChange, GroupSoftwareDelete app_name = "Config Management" @@ -8,9 +9,14 @@ urlpatterns = [ path('group', GroupIndexView.as_view(), name='Groups'), path('group/add', GroupAdd.as_view(), name='_group_add'), path('group/', GroupView.as_view(), name='_group_view'), + path('group//child', GroupAdd.as_view(), name='_group_add_child'), path('group//delete', GroupDelete.as_view(), name='_group_delete'), + path("group//software/add", GroupSoftwareAdd.as_view(), name="_group_software_add"), + path("group//software/", GroupSoftwareChange.as_view(), name="_group_software_change"), + path("group//software//delete", GroupSoftwareDelete.as_view(), name="_group_software_delete"), + path('group//host', GroupHostAdd.as_view(), name='_group_add_host'), path('group//host//delete', GroupHostDelete.as_view(), name='_group_delete_host'), diff --git a/app/config_management/views/groups/groups.py b/app/config_management/views/groups/groups.py index a28338bf..8f32d500 100644 --- a/app/config_management/views/groups/groups.py +++ b/app/config_management/views/groups/groups.py @@ -16,7 +16,7 @@ from itam.models.device import Device from settings.models.user_settings import UserSettings from config_management.forms.group_hosts import ConfigGroupHostsForm -from config_management.models.groups import ConfigGroups, ConfigGroupHosts +from config_management.models.groups import ConfigGroups, ConfigGroupHosts, ConfigGroupSoftware @@ -146,6 +146,9 @@ class GroupView(OrganizationPermission, generic.UpdateView): context['model_delete_url'] = reverse('Config Management:_group_delete', args=(self.kwargs['pk'],)) + softwares = ConfigGroupSoftware.objects.filter(config_group=self.kwargs['pk'])[:50] + context['softwares'] = softwares + context['content_title'] = self.object.name # if self.request.user.is_superuser: diff --git a/app/config_management/views/groups/software.py b/app/config_management/views/groups/software.py new file mode 100644 index 00000000..62eb7be9 --- /dev/null +++ b/app/config_management/views/groups/software.py @@ -0,0 +1,137 @@ +from django.urls import reverse +from django.views import generic + +from access.mixin import OrganizationPermission + +from itam.models.software import Software + +from config_management.forms.group.add_software import SoftwareAdd +from config_management.forms.group.change_software import SoftwareUpdate +from config_management.models.groups import ConfigGroups, ConfigGroupSoftware + + + +class GroupSoftwareAdd(OrganizationPermission, generic.CreateView): + + form_class = SoftwareAdd + + model = ConfigGroupSoftware + + parent_model = ConfigGroups + + permission_required = [ + 'config_management.add_configgroupsoftware', + ] + + template_name = 'form.html.j2' + + + def form_valid(self, form): + config_group = ConfigGroups.objects.get(pk=self.kwargs['pk']) + form.instance.organization_id = config_group.organization.id + form.instance.config_group = config_group + + software = Software.objects.get(pk=form.instance.software.id) + + if ConfigGroupSoftware.objects.filter( + config_group=config_group, + software=software + ).exists(): + + existing_object = ConfigGroupSoftware.objects.get( + device=device, + software=software + ) + + existing_object.action = form.instance.action + existing_object.save() + + return HttpResponseRedirect(self.get_success_url()) + + else: + + return super().form_valid(form) + + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + obj = ConfigGroups.objects.get(pk=self.kwargs['pk']) + kwargs['organizations'] = [ obj.organization.id ] + return kwargs + + + def get_success_url(self, **kwargs): + + return reverse('Config Management:_group_view', args=(self.kwargs['pk'],)) + + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + context['content_title'] = 'Add Software Action' + + return context + + + +class GroupSoftwareChange(OrganizationPermission, generic.UpdateView): + + form_class = SoftwareUpdate + + model = ConfigGroupSoftware + + permission_required = [ + 'config_management.change_configgroupsoftware' + + ] + + template_name = 'form.html.j2' + + + def form_valid(self, form): + config_group = ConfigGroups.objects.get(pk=self.kwargs['group_id']) + + form.instance.organization_id = config_group.organization.id + form.instance.config_group = config_group + + return super().form_valid(form) + + + def get_success_url(self, **kwargs): + + return reverse('Config Management:_group_view', args=(self.kwargs['group_id'],)) + + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + context['model_delete_url'] = reverse('Config Management:_group_software_delete', args=(self.kwargs['group_id'], self.kwargs['pk'],)) + + context['content_title'] = 'Edit Software Action' + + return context + + + +class GroupSoftwareDelete(OrganizationPermission, generic.DeleteView): + + model = ConfigGroupSoftware + + permission_required = [ + 'config_management.delete_configgroupsoftware', + ] + + template_name = 'form.html.j2' + + + def get_success_url(self, **kwargs): + + return reverse('Config Management:_group_view', args=(self.kwargs['pk'],)) + + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + context['content_title'] = 'Delete ' + + return context