Compare commits

..

9 Commits

Author SHA1 Message Date
c496d10c1a bump: version 1.0.0-b4 → 1.0.0-b5 2024-07-31 17:02:31 +00:00
Jon
3993cc96a5 Merge branch '160-api-config' into 'development'
feat: add Config groups to API

Closes #161 and #160

See merge request nofusscomputing/projects/centurion_erp!45
2024-07-31 16:44:55 +00:00
Jon
a4b37b34a9 docs: Add dets of collection
!45 closes #161
2024-08-01 02:12:42 +09:30
Jon
2f55024f0b fix(api): Ensure device groups is read only
checks for required fields

!45 #160 #162
2024-08-01 01:54:24 +09:30
Jon
213644a51a test(api): Field existence and type checks for device
checks for required fields

!45 #160 #162
2024-08-01 01:12:33 +09:30
Jon
281d839801 feat(api): Add device config groups to devices
!45 #160 nofusscomputing/projects/ansible/collections/centurion_erp_collection!7 nofusscomputing/projects/ansible/collections/centurion_erp_collection#4
2024-08-01 00:32:16 +09:30
Jon
4fd157a785 test(api): test configgroups API fields
tests for type and existence

!45 closes #160
2024-07-31 22:27:45 +09:30
Jon
968b3a0f92 feat(api): Ability to fetch configgroups from api along with config
!45 #160 nofusscomputing/projects/ansible/collections/centurion_erp_collection#4
2024-07-31 22:27:32 +09:30
Jon
f5ba608ed1 ci: var to export for use in script
!45
2024-07-31 21:19:55 +09:30
13 changed files with 967 additions and 10 deletions

View File

@ -4,5 +4,5 @@ commitizen:
prerelease_offset: 1
tag_format: $version
update_changelog_on_bump: false
version: 1.0.0-b4
version: 1.0.0-b5
version_scheme: semver

View File

@ -151,7 +151,7 @@ Docker Container:
script:
- if [ "$CI_COMMIT_BRANCH" == "development" ] ; then RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout --prerelease beta); else RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout); fi
- RELEASE_VERSION_NEW=$(cz version --project)
- RELEASE_TAG=$RELEASE_VERSION_NEW
- export RELEASE_TAG=$RELEASE_VERSION_NEW
- echo "[DEBUG] RELEASE_VERSION_CURRENT[$RELEASE_VERSION_CURRENT]"
- echo "[DEBUG] RELEASE_CHANGELOG[$RELEASE_CHANGELOG]"
- echo "[DEBUG] RELEASE_VERSION_NEW[$RELEASE_VERSION_NEW]"

View File

@ -1,3 +1,14 @@
## 1.0.0-b5 (2024-07-31)
### Feat
- **api**: Add device config groups to devices
- **api**: Ability to fetch configgroups from api along with config
### Fix
- **api**: Ensure device groups is read only
## 1.0.0-b4 (2024-07-29)
### Feat

View File

@ -0,0 +1,86 @@
from rest_framework import serializers
from rest_framework.reverse import reverse
from config_management.models.groups import ConfigGroups
class ParentGroupSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField('get_url')
class Meta:
model = ConfigGroups
fields = [
'id',
'name',
'url',
]
read_only_fields = [
'id',
'name',
'url',
]
def get_url(self, obj):
request = self.context.get('request')
return request.build_absolute_uri(reverse("API:_api_config_group", args=[obj.pk]))
class ConfigGroupsSerializerBase(serializers.ModelSerializer):
parent = ParentGroupSerializer(read_only=True)
url = serializers.SerializerMethodField('get_url')
class Meta:
model = ConfigGroups
fields = [
'id',
'parent',
'name',
'config',
'url',
]
read_only_fields = [
'id',
'name',
'config',
'url',
]
def get_url(self, obj):
request = self.context.get('request')
return request.build_absolute_uri(reverse("API:_api_config_group", args=[obj.pk]))
class ConfigGroupsSerializer(ConfigGroupsSerializerBase):
class Meta:
model = ConfigGroups
depth = 1
fields = [
'id',
'parent',
'name',
'config',
'url',
]
read_only_fields = [
'id',
'parent',
'name',
'config',
'url',
]

View File

@ -1,9 +1,38 @@
from django.urls import reverse
from itam.models.device import Device
from rest_framework import serializers
from api.serializers.config import ParentGroupSerializer
from config_management.models.groups import ConfigGroupHosts
from itam.models.device import Device
class DeviceConfigGroupsSerializer(serializers.ModelSerializer):
name = serializers.CharField(source='group.name', read_only=True)
url = serializers.HyperlinkedIdentityField(
view_name="API:_api_config_group", format="html"
)
class Meta:
model = ConfigGroupHosts
fields = [
'id',
'name',
'url',
]
read_only_fields = [
'id',
'name',
'url',
]
class DeviceSerializer(serializers.ModelSerializer):
@ -13,7 +42,9 @@ class DeviceSerializer(serializers.ModelSerializer):
)
config = serializers.SerializerMethodField('get_device_config')
groups = DeviceConfigGroupsSerializer(source='configgrouphosts_set', many=True, read_only=True)
def get_device_config(self, device):
request = self.context.get('request')
@ -22,11 +53,29 @@ class DeviceSerializer(serializers.ModelSerializer):
class Meta:
model = Device
fields = '__all__'
read_only_fields = [
'inventorydate',
depth = 1
fields = [
'id',
'is_global',
'slug',
'name',
'config',
'serial_number',
'uuid',
'inventorydate',
'created',
'modified',
'groups',
'organization',
'url',
]
read_only_fields = [
'id',
'config',
'inventorydate',
'created',
'modified',
'groups',
'url',
]

View File

@ -3,7 +3,7 @@ from django.urls import path
from rest_framework.routers import DefaultRouter
from rest_framework.urlpatterns import format_suffix_patterns
from .views import access, index
from .views import access, config, index
from .views.itam import software, config as itam_config
from .views.itam.device import DeviceViewSet
@ -24,6 +24,9 @@ router.register('software', software.SoftwareViewSet, basename='software')
urlpatterns = [
path("config/<slug:slug>/", itam_config.View.as_view(), name="_api_device_config"),
path("configuration/", config.ConfigGroupsList.as_view(), name='_api_config_groups'),
path("configuration/<int:pk>", config.ConfigGroupsDetail.as_view(), name='_api_config_group'),
path("device/inventory", inventory.Collect.as_view(), name="_api_device_inventory"),
path("organization/", access.OrganizationList.as_view(), name='_api_orgs'),

54
app/api/views/config.py Normal file
View File

@ -0,0 +1,54 @@
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import generics
from api.serializers.config import ConfigGroupsSerializer
from api.views.mixin import OrganizationPermissionAPI
from config_management.models.groups import ConfigGroups
@extend_schema_view(
get=extend_schema(
summary = "Fetch Config groups",
description="Returns a list of Config Groups."
),
)
class ConfigGroupsList(generics.ListAPIView):
permission_classes = [
OrganizationPermissionAPI
]
queryset = ConfigGroups.objects.all()
lookup_field = 'pk'
serializer_class = ConfigGroupsSerializer
def get_view_name(self):
return "Config Groups"
@extend_schema_view(
get=extend_schema(
summary = "Get A Config Group",
# responses = {}
),
)
class ConfigGroupsDetail(generics.RetrieveAPIView):
permission_classes = [
OrganizationPermissionAPI
]
queryset = ConfigGroups.objects.all()
lookup_field = 'pk'
serializer_class = ConfigGroupsSerializer
def get_view_name(self):
return "Config Group"

View File

@ -27,6 +27,7 @@ class Index(viewsets.ViewSet):
{
# "teams": reverse("_api_teams", request=request),
"devices": reverse("API:device-list", request=request),
"config_groups": reverse("API:_api_config_groups", request=request),
"organizations": reverse("API:_api_orgs", request=request),
"software": reverse("API:software-list", request=request),
}

View File

@ -0,0 +1,224 @@
import pytest
import unittest
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import Client, TestCase
from access.models import Organization, Team, TeamUsers, Permission
from app.tests.abstract.models import TenancyModel
from config_management.models.groups import ConfigGroups
@pytest.mark.django_db
class ConfigGroupsAPI(
TestCase,
):
model = ConfigGroups
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
2. Create an item
"""
self.organization = Organization.objects.create(name='test_org')
self.item = self.model.objects.create(
organization = self.organization,
name = 'one',
config = dict({"key": "one", "existing": "dont_over_write"})
)
self.second_item = self.model.objects.create(
organization = self.organization,
name = 'one_two',
config = dict({"key": "two"}),
parent = self.item
)
self.url_view_kwargs = {'pk': self.second_item.id}
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = self.organization,
)
view_team.permissions.set([view_permissions])
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
client = Client()
url = reverse('API:_api_config_group', kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.get(url)
self.api_data = response.data
def test_api_field_exists_id(self):
""" Test for existance of API Field
id field must exist
"""
assert 'id' in self.api_data
def test_api_field_type_id(self):
""" Test for type for API Field
id field must be int
"""
assert type(self.api_data['id']) is int
def test_api_field_exists_parent(self):
""" Test for existance of API Field
parent field must exist
"""
assert 'parent' in self.api_data
def test_api_field_type_parent(self):
""" Test for type for API Field
parent field must be dict
"""
assert type(self.api_data['parent']) is dict
def test_api_field_exists_name(self):
""" Test for existance of API Field
name field must exist
"""
assert 'name' in self.api_data
def test_api_field_type_name(self):
""" Test for type for API Field
name field must be str
"""
assert type(self.api_data['name']) is str
def test_api_field_exists_config(self):
""" Test for existance of API Field
config field must exist
"""
assert 'config' in self.api_data
def test_api_field_type_config(self):
""" Test for type for API Field
config field must be dict
"""
assert type(self.api_data['config']) is dict
def test_api_field_exists_url(self):
""" Test for existance of API Field
url field must exist
"""
assert 'url' in self.api_data
def test_api_field_type_url(self):
""" Test for type for API Field
url field must be str
"""
assert type(self.api_data['url']) is str
def test_api_field_exists_parent_id(self):
""" Test for existance of API Field
parent.id field must exist
"""
assert 'id' in self.api_data['parent']
def test_api_field_type_parent_id(self):
""" Test for type for API Field
parent.id field must be int
"""
assert type(self.api_data['parent']['id']) is int
def test_api_field_exists_parent_name(self):
""" Test for existance of API Field
parent.name field must exist
"""
assert 'name' in self.api_data['parent']
def test_api_field_type_parent_name(self):
""" Test for type for API Field
parent.name field must be str
"""
assert type(self.api_data['parent']['name']) is str
def test_api_field_exists_parent_url(self):
""" Test for existance of API Field
parent.url field must exist
"""
assert 'url' in self.api_data['parent']
def test_api_field_type_parent_url(self):
""" Test for type for API Field
parent.url field must be str
"""
assert type(self.api_data['parent']['url']) is str

View File

@ -0,0 +1,522 @@
import pytest
import unittest
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import Client, TestCase
from rest_framework.relations import Hyperlink
from access.models import Organization, Team, TeamUsers, Permission
from api.tests.abstract.api_permissions import APIPermissions
from config_management.models.groups import ConfigGroups, ConfigGroupHosts
from itam.models.device import Device
class DeviceAPI(TestCase):
model = Device
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
. create an organization that is different to item
2. Create a device
3. create teams with each permission: view, add, change, delete
4. create a user per team
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.item = self.model.objects.create(
organization=organization,
name = 'deviceone',
uuid = 'val',
serial_number = 'another val'
)
config_group = ConfigGroups.objects.create(
organization = self.organization,
name = 'one',
config = dict({"key": "one", "existing": "dont_over_write"})
)
config_group_second_item = ConfigGroups.objects.create(
organization = self.organization,
name = 'one_two',
config = dict({"key": "two"}),
parent = config_group
)
config_group_hosts = ConfigGroupHosts.objects.create(
organization = organization,
host = self.item,
group = config_group,
)
config_group_hosts_two = ConfigGroupHosts.objects.create(
organization = organization,
host = self.item,
group = config_group_second_item,
)
# self.url_kwargs = {'pk': self.item.id}
self.url_view_kwargs = {'pk': self.item.id}
# self.add_data = {'name': 'device', 'organization': self.organization.id}
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_permissions])
# add_permissions = Permission.objects.get(
# codename = 'add_' + self.model._meta.model_name,
# content_type = ContentType.objects.get(
# app_label = self.model._meta.app_label,
# model = self.model._meta.model_name,
# )
# )
# add_team = Team.objects.create(
# team_name = 'add_team',
# organization = organization,
# )
# add_team.permissions.set([add_permissions])
# change_permissions = Permission.objects.get(
# codename = 'change_' + self.model._meta.model_name,
# content_type = ContentType.objects.get(
# app_label = self.model._meta.app_label,
# model = self.model._meta.model_name,
# )
# )
# change_team = Team.objects.create(
# team_name = 'change_team',
# organization = organization,
# )
# change_team.permissions.set([change_permissions])
# delete_permissions = Permission.objects.get(
# codename = 'delete_' + self.model._meta.model_name,
# content_type = ContentType.objects.get(
# app_label = self.model._meta.app_label,
# model = self.model._meta.model_name,
# )
# )
# delete_team = Team.objects.create(
# team_name = 'delete_team',
# organization = organization,
# )
# delete_team.permissions.set([delete_permissions])
# self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
# self.add_user = User.objects.create_user(username="test_user_add", password="password")
# teamuser = TeamUsers.objects.create(
# team = add_team,
# user = self.add_user
# )
# self.change_user = User.objects.create_user(username="test_user_change", password="password")
# teamuser = TeamUsers.objects.create(
# team = change_team,
# user = self.change_user
# )
# self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
# teamuser = TeamUsers.objects.create(
# team = delete_team,
# user = self.delete_user
# )
# self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
# different_organization_team = Team.objects.create(
# team_name = 'different_organization_team',
# organization = different_organization,
# )
# different_organization_team.permissions.set([
# view_permissions,
# add_permissions,
# change_permissions,
# delete_permissions,
# ])
# TeamUsers.objects.create(
# team = different_organization_team,
# user = self.different_organization_user
# )
client = Client()
url = reverse('API:device-detail', kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.get(url)
self.api_data = response.data
def test_api_field_exists_id(self):
""" Test for existance of API Field
id field must exist
"""
assert 'id' in self.api_data
def test_api_field_type_id(self):
""" Test for type for API Field
id field must be int
"""
assert type(self.api_data['id']) is int
def test_api_field_exists_is_global(self):
""" Test for existance of API Field
is_global field must exist
"""
assert 'is_global' in self.api_data
def test_api_field_type_is_global(self):
""" Test for type for API Field
is_global field must be boolean
"""
assert type(self.api_data['is_global']) is bool
def test_api_field_exists_name(self):
""" Test for existance of API Field
name field must exist
"""
assert 'name' in self.api_data
def test_api_field_type_name(self):
""" Test for type for API Field
name field must be str
"""
assert type(self.api_data['name']) is str
def test_api_field_exists_config(self):
""" Test for existance of API Field
config field must exist
"""
assert 'config' in self.api_data
def test_api_field_type_config(self):
""" Test for type for API Field
config field must be dict
"""
assert type(self.api_data['config']) is str
def test_api_field_exists_serial_number(self):
""" Test for existance of API Field
serial_number field must exist
"""
assert 'serial_number' in self.api_data
def test_api_field_type_serial_number(self):
""" Test for type for API Field
serial_number field must be str
"""
assert type(self.api_data['serial_number']) is str
def test_api_field_exists_uuid(self):
""" Test for existance of API Field
uuid field must exist
"""
assert 'uuid' in self.api_data
def test_api_field_type_uuid(self):
""" Test for type for API Field
uuid field must be str
"""
assert type(self.api_data['uuid']) is str
def test_api_field_exists_inventorydate(self):
""" Test for existance of API Field
inventorydate field must exist
"""
assert 'inventorydate' in self.api_data
def test_api_field_type_inventorydate(self):
""" Test for type for API Field
inventorydate field must be str
"""
assert (
type(self.api_data['inventorydate']) is str
or
self.api_data['inventorydate'] is None
)
def test_api_field_exists_created(self):
""" Test for existance of API Field
created field must exist
"""
assert 'created' in self.api_data
def test_api_field_type_created(self):
""" Test for type for API Field
created field must be str
"""
assert type(self.api_data['created']) is str
def test_api_field_exists_modified(self):
""" Test for existance of API Field
modified field must exist
"""
assert 'modified' in self.api_data
def test_api_field_type_modified(self):
""" Test for type for API Field
modified field must be str
"""
assert type(self.api_data['modified']) is str
def test_api_field_exists_groups(self):
""" Test for existance of API Field
groups field must exist
"""
assert 'groups' in self.api_data
def test_api_field_type_groups(self):
""" Test for type for API Field
groups field must be list
"""
assert type(self.api_data['groups']) is list
def test_api_field_exists_organization(self):
""" Test for existance of API Field
organization field must exist
"""
assert 'organization' in self.api_data
def test_api_field_type_organization(self):
""" Test for type for API Field
organization field must be dict
"""
assert type(self.api_data['organization']) is dict
def test_api_field_exists_url(self):
""" Test for existance of API Field
url field must exist
"""
assert 'url' in self.api_data
def test_api_field_type_url(self):
""" Test for type for API Field
url field must be str
"""
assert type(self.api_data['url']) is Hyperlink
def test_api_field_exists_organization_id(self):
""" Test for existance of API Field
organization.id field must exist
"""
assert 'id' in self.api_data['organization']
def test_api_field_type_organization_id(self):
""" Test for type for API Field
organization.id field must be int
"""
assert type(self.api_data['organization']['id']) is int
def test_api_field_exists_organization_name(self):
""" Test for existance of API Field
organization.name field must exist
"""
assert 'name' in self.api_data['organization']
def test_api_field_type_organization_name(self):
""" Test for type for API Field
organization.name field must be str
"""
assert type(self.api_data['organization']['name']) is str
def test_api_field_exists_groups_id(self):
""" Test for existance of API Field
groups.id field must exist
"""
assert 'id' in self.api_data['groups'][0]
def test_api_field_type_groups_id(self):
""" Test for type for API Field
groups.id field must be int
"""
assert type(self.api_data['groups'][0]['id']) is int
def test_api_field_exists_groups_name(self):
""" Test for existance of API Field
groups.name field must exist
"""
assert 'name' in self.api_data['groups'][0]
def test_api_field_type_groups_name(self):
""" Test for type for API Field
groups.name field must be str
"""
assert type(self.api_data['groups'][0]['name']) is str
def test_api_field_exists_groups_url(self):
""" Test for existance of API Field
groups.url field must exist
"""
assert 'url' in self.api_data['groups'][0]
def test_api_field_type_groups_url(self):
""" Test for type for API Field
groups.url field must be str
"""
assert type(self.api_data['groups'][0]['url']) is Hyperlink

View File

@ -16,3 +16,8 @@ This documentation is targeted towards those whom administer the applications de
- [Backup](./backup.md)
- [Installation](./installation.md)
## Ansible Automation Platform / AWX
We have built an [Ansible Collection](../../ansible/collections/centurion/index.md) for Centurion ERP that you could consider the bridge between the config within Centurion and the end device. This collection can be directly added to AAP / AWX as a project which enables accessing the features the collection has to offer. Please refer to the [collections documentation](../../ansible/collections/centurion/index.md) for further information.

View File

@ -27,6 +27,8 @@ Whilst there are many Enterprise Rescource Planning (ERP) applications, Centurio
Centurion ERP contains the following modules:
- [Companion Ansible Collection](../ansible/collections/centurion/index.md)
- [Configuration Management](./user/config_management/index.md)
- [IT Asset Management (ITAM)](./user/itam/index.md)