Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 2.7 #804

Merged
merged 11 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
# 2.7.0 2024-12-16

## Breaking changes ⚠

- drop support for docker compose v1
- SurveyValidator form utils import switched from `from service_catalog.forms import SurveyValidator` to `from service_catalog.forms.form_utils import SurveyValidator`

## Fix

- Credential id set in operation was not sent to AWX

## Enhancement

- Add support of multiple workers to gunicorn to speedup Squest

## Feature

- Add permission on operation, service and portfolio

# 2.6.1 2024-11-15

## Fix
Expand Down
4 changes: 2 additions & 2 deletions Squest/utils/plugin_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def is_validator(obj):
"""
Returns True if the object is a Script.
"""
from service_catalog.forms import SurveyValidator
from service_catalog.forms.form_utils import SurveyValidator
try:
return issubclass(obj, SurveyValidator) and obj != SurveyValidator
except TypeError:
Expand Down Expand Up @@ -103,4 +103,4 @@ def _load_validator_module(module_name, definition_kind, filepath):
except ModuleNotFoundError:
logger.warning(f"[PluginController] Validator file not loaded: {module_name}."
f" Check that the file exists in the plugin directory.")
return None
return None
6 changes: 6 additions & 0 deletions Squest/utils/squest_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ class Meta:
def get_q_filter(cls, user, perm):
return Q(pk=None)

@classmethod
def get_queryset_for_user_filtered(cls, user, perm, unique=True):
app_label, codename = perm.split(".")
return cls.get_queryset_for_user(user, perm).filter(permission__content_type__app_label=app_label,
permission__codename=codename)

@classmethod
def get_queryset_for_user(cls, user, perm, unique=True):
from profiles.models.squest_permission import Permission
Expand Down
2 changes: 1 addition & 1 deletion Squest/utils/squest_rbac.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,4 @@ def has_perm(self, user_obj, perm, obj=None):
except AttributeError:
logger.debug("is_owner method not found")
cache.set(key, permission_granted, 60)
return permission_granted
return permission_granted
2 changes: 1 addition & 1 deletion Squest/version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__version__ = "2.6.1"
__version__ = "2.7.0"
VERSION = __version__
2 changes: 0 additions & 2 deletions dev.docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# add this file to the docker compose execution when developing Squest
version: '3.7'

services:

db:
Expand Down
2 changes: 0 additions & 2 deletions docker-compose.override.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# this file is loaded automatically when running "docker-compose up"
version: '3.7'

services:
nginx:
ports:
Expand Down
2 changes: 0 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# docker-compose -f docker-compose.yml up
version: '3.7'

services:

db:
Expand Down
2 changes: 1 addition & 1 deletion docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ echo "Inserting default data"
python manage.py insert_default_data

echo "Starting web server"
gunicorn --bind 0.0.0.0:8000 --pythonpath /app/squest Squest.wsgi
gunicorn --bind 0.0.0.0:8000 --workers ${GUNICORN_WORKERS:-4} --pythonpath /app/squest Squest.wsgi
2 changes: 2 additions & 0 deletions docker/environment_variables/squest.env
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ REDIS_CACHE_HOST=redis-cache

WAIT_HOSTS=db:3306,rabbitmq:5672
WAIT_TIMEOUT=60

GUNICORN_WORKERS=4
6 changes: 6 additions & 0 deletions docs/configuration/squest_settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,12 @@ Set to `True` to enable email notifications.

Set to `True` to change the navbar and footer color to visually identify a testing instance of Squest.

### GUNICORN_WORKERS

**Default:** `4`

Number of workers used by Gunicorn process in charge of serving client connection. Increase the number of worker threads to serve more clients concurrently

## SMTP

### EMAIL_HOST
Expand Down
6 changes: 3 additions & 3 deletions docs/manual/advanced/survey_validators.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Create Python class that inherit from SurveyValidator with a method `validate_su

```python
# plugins/survey_validators/MySurveyValidator.py
from service_catalog.forms import SurveyValidator
from service_catalog.forms.form_utils import SurveyValidator

class MyCustomValidatorFoo(SurveyValidator):
def validate_survey(self):
Expand Down Expand Up @@ -75,7 +75,7 @@ This validator will always fail if:
- It's not the weekend yet

```python
from service_catalog.forms import SurveyValidator
from service_catalog.forms.form_utils import SurveyValidator
import datetime

class ValidatorForVM(SurveyValidator):
Expand All @@ -86,4 +86,4 @@ class ValidatorForVM(SurveyValidator):
weekday = datetime.datetime.today().weekday()
if weekday < 5:
self.fail("Sorry it's not the weekend yet")
```
```
1 change: 1 addition & 0 deletions docs/manual/service_catalog/operation.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Operations of type "update" or "delete" can be then added to manage the lifecycl
| Default diff mode | Default `False`. This is equivalent to Ansible's --diff mode in the CLI |
| Default credential IDs | Comma separated list of credentials ID |
| When | Ansible 'when' condition to make operation available to some instance spec condition |
| Permission | Set a permission required to view the operation. By default set to "view_operation" |

## Job template config

Expand Down
2 changes: 0 additions & 2 deletions ldap.docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# docker-compose -f docker-compose.yml -f docker-compose.override.yml -f tls.docker-compose.yml -f ldap.docker-compose.yml up
version: '3.7'

services:

django:
Expand Down
2 changes: 1 addition & 1 deletion profiles/migrations/0021_create_default_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,4 @@ class Migration(migrations.Migration):
migrations.RunPython(force_create_permissions),
migrations.RunPython(add_perm_owner),
migrations.RunPython(add_perm_global),
]
]
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "squest"
version = "2.6.1"
version = "2.7.0"
description = "Service catalog on top of Red Hat Ansible Automation Platform(RHAAP)/AWX (formerly known as Ansible Tower)"
authors = ["Nicolas Marcq <nicolas.marcq@hpe.com>", "Elias Boulharts <elias.boulharts@hpe.com", "Anthony Belhadj <abelhadj@hpe.com>"]
license = "MIT"
Expand Down
7 changes: 3 additions & 4 deletions service_catalog/api/serializers/request_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,9 @@ def save(self, **kwargs):

def validate(self, data):
super(OperationRequestSerializer, self).validate(data)

if self.operation.is_admin_operation and not self.user.has_perm("service_catalog.admin_request_on_instance"):
if not self.user.has_perm("service_catalog.request_on_instance"):
raise PermissionDenied
if not self.operation.is_admin_operation and not self.user.has_perm("service_catalog.request_on_instance"):
if not self.user.has_perm(self.operation.permission.permission_str, self.squest_instance):
raise PermissionDenied
if self.squest_instance.state not in [InstanceState.AVAILABLE]:
raise PermissionDenied("Instance not available")
Expand Down Expand Up @@ -180,4 +179,4 @@ class Meta:
exclude = ['periodic_task', 'periodic_task_date_expire', 'failure_message']

instance = InstanceReadSerializer(read_only=True)
user = UserSerializerNested(read_only=True)
user = UserSerializerNested(read_only=True)
3 changes: 1 addition & 2 deletions service_catalog/forms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,4 @@
from .service_request_forms import *
from .support_message_forms import *
from .support_request_forms import *
from .tower_server_forms import *
from .form_utils import *
from .tower_server_forms import *
12 changes: 8 additions & 4 deletions service_catalog/forms/form_utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import inspect
import logging

from jinja2 import Template
from jinja2.exceptions import UndefinedError
import inspect

from rest_framework.exceptions import ValidationError

from service_catalog.models import Instance

logger = logging.getLogger(__name__)


Expand All @@ -30,6 +28,11 @@ def template_field(cls, jinja_template_string, template_data_dict):
pass
return templated_string

@classmethod
def get_default_permission_for_operation(cls):
from django.contrib.auth.models import Permission
return Permission.objects.get(codename="view_operation").id




Expand Down Expand Up @@ -62,3 +65,4 @@ def fail(self, message, field="__all__"):
self._form.add_error(field, message)
else:
raise ValidationError({field: message})

15 changes: 11 additions & 4 deletions service_catalog/forms/operation_forms.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
from django.forms import MultipleChoiceField, SelectMultiple
from django.forms import MultipleChoiceField, SelectMultiple, CharField, ModelChoiceField, forms

from Squest.utils.plugin_controller import PluginController
from Squest.utils.squest_model_form import SquestModelForm
from service_catalog.models import Operation
from profiles.models import Permission
from service_catalog.forms.form_utils import FormUtils

from service_catalog.models import Operation

class OperationForm(SquestModelForm):
validators = MultipleChoiceField(label="Validators",
required=False,
choices=[],
widget=SelectMultiple(attrs={'data-live-search': "true"}))

permission = ModelChoiceField(queryset=Permission.objects.filter(content_type__model="operation",
content_type__app_label="service_catalog"),
initial=FormUtils.get_default_permission_for_operation,
help_text=Operation.permission.field.help_text)

class Meta:
model = Operation
fields = ["service", "name", "description", "job_template", "type", "process_timeout_second",
"auto_accept", "auto_process", "enabled", "is_admin_operation", "extra_vars", "default_inventory_id",
"auto_accept", "auto_process", "enabled", "extra_vars", "default_inventory_id",
"default_limits", "default_tags", "default_skip_tags", "default_credentials_ids", "default_verbosity",
"default_diff_mode", "default_job_type", "validators", "when"]
"default_diff_mode", "default_job_type", "validators", "when", "permission"]

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down
33 changes: 31 additions & 2 deletions service_catalog/forms/portfolio_form.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,45 @@
from django.forms import ImageField, FileInput
from django.forms import ImageField, FileInput, CharField, ModelChoiceField

from Squest.utils.squest_model_form import SquestModelForm
from profiles.models import Permission
from service_catalog.forms.form_utils import FormUtils
from service_catalog.models.portfolio import Portfolio


class PortfolioForm(SquestModelForm):
class Meta:
model = Portfolio
fields = ["name", "description", "image", "description_doc", "parent_portfolio"]
fields = ["name", "description", "image", "description_doc", "parent_portfolio", "permission"]

image = ImageField(
label="Choose a file",
required=False,
widget=FileInput()
)

permission = ModelChoiceField(
queryset=Permission.objects.filter(content_type__model="operation", content_type__app_label="service_catalog"),
initial=FormUtils.get_default_permission_for_operation,
help_text="Applying a new permission here will apply it on all operations in all sub services")


def __init__(self, *args, **kwargs):
super(PortfolioForm, self).__init__(*args, **kwargs)
if self.instance.id: # Edit object
# set permission field. If one operation in the service is not using the default
all_permission_current_service = Permission.objects.filter(operation__service__in=self.instance.service_list.all()).distinct()
if all_permission_current_service.count() > 1:
set_at_operation_level = ('set_at_operation_level','OVERWRITTEN BY OPERATION')
self.fields["permission"].choices = list(self.fields['permission'].choices) + [set_at_operation_level]
self.fields["permission"].initial = set_at_operation_level
else:
self.fields["permission"].initial = all_permission_current_service.first()


def save(self, commit=True):
# save as usual
obj = super().save(commit)
# bulk edit on permission
new_perm = self.cleaned_data.get('permission')
obj.bulk_set_permission_on_operation(new_perm)
return obj
26 changes: 25 additions & 1 deletion service_catalog/forms/service_forms.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from django import forms
from django.forms import CharField, ModelChoiceField

from Squest.utils.squest_model_form import SquestModelForm
from profiles.models import Permission
from service_catalog.forms.form_utils import FormUtils
from service_catalog.models.services import Service


Expand All @@ -16,6 +19,14 @@ def __init__(self, *args, **kwargs):
self.fields['enabled'].disabled = True
self.fields['enabled'].help_text = \
"'CREATE' operation with a job template is required to enable this service."
# set permission field. If one operation in the service is not using the default
all_permission_current_service = Permission.objects.filter(operation__service=self.instance).distinct()
if all_permission_current_service.count() > 1:
set_at_operation_level = ('set_at_operation_level','OVERWRITTEN BY OPERATION')
self.fields["permission"].choices = list(self.fields['permission'].choices) + [set_at_operation_level]
self.fields["permission"].initial = set_at_operation_level
else:
self.fields["permission"].initial = all_permission_current_service.first()

image = forms.ImageField(label="Choose a file",
required=False,
Expand All @@ -25,8 +36,21 @@ def __init__(self, *args, **kwargs):
help_text="Redirect support button to the given URL",
widget=forms.Textarea(attrs={'rows': 5, 'class': 'form-control'}),
required=False)
permission = ModelChoiceField(
queryset=Permission.objects.filter(content_type__model="operation", content_type__app_label="service_catalog"),
initial=FormUtils.get_default_permission_for_operation,
help_text="Applying a new permission here will apply it on all operations")

def save(self, commit=True):
# save as usual
obj = super().save(commit)
# bulk edit on permission
new_perm = self.cleaned_data.get('permission')
obj.bulk_set_permission_on_operation(new_perm)
return obj

class Meta:
model = Service
fields = ["name", "description", "image", "enabled",
"parent_portfolio", "external_support_url", "extra_vars", "description_doc", "attribute_definitions"]
"parent_portfolio", "external_support_url", "extra_vars", "description_doc", "attribute_definitions",
"permission"]
4 changes: 1 addition & 3 deletions service_catalog/management/commands/insert_testing_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,8 @@ def handle(self, *args, **options):
job_template=job_templates.get(name="Demo Job Template"))
states = [RequestState.SUBMITTED, RequestState.FAILED, RequestState.ACCEPTED, RequestState.ON_HOLD,
RequestState.REJECTED, RequestState.CANCELED, RequestState.PROCESSING, RequestState.COMPLETE]
for i in range(random.randint(1, 3)):
for i in range(3):
for username in users:
if random.randint(0, 2) == 1:
continue
user = users[username]
new_instance = Instance.objects.create(service=service, name=f"Instance - {username} - {i}",
requester=user, quota_scope=random.choice(organization))
Expand Down
Loading
Loading