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.6.1 #798

Merged
merged 11 commits into from
Nov 15, 2024
2 changes: 1 addition & 1 deletion .github/workflows/on-pull-request-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
poetry run coverage lcov

- name: 'Upload Artifact'
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: htmlcov
path: htmlcov
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
# 2.6.1 2024-11-15

## Fix

- Fix file image upload in documentation
- Standard users no longer able to list admin CREATE operation


## Enhancement

- UI: accept button show when auto process will be executed after approval
- UI: add auto accept and auto process status icon for all user
- add when condition parameter on day 2 operations
- Add parameter to define list of administrators to which to send 500 errors via SMTP
- Add parameters to login into SMTP server

# 2.6.0 2024-05-22

## Fix
Expand Down
13 changes: 13 additions & 0 deletions Squest/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,11 @@
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
"mail_admins": {
"level": "ERROR",
"class": "django.utils.log.AdminEmailHandler",
"include_html": True,
},
},
'loggers': {
'': {
Expand Down Expand Up @@ -311,17 +316,25 @@
# -----------------------------------------
# Squest email config
# -----------------------------------------
ADMINS = [("", element) for element in os.environ.get('SQUEST_ADMINS', '').split(',') if element]
SQUEST_HOST = os.environ.get('SQUEST_HOST', "http://squest.domain.local")
SQUEST_EMAIL_HOST = os.environ.get('SQUEST_EMAIL_HOST', "squest@squest.domain.local")
SERVER_EMAIL = SQUEST_EMAIL_HOST
SQUEST_EMAIL_NOTIFICATION_ENABLED = str_to_bool(os.environ.get('SQUEST_EMAIL_NOTIFICATION_ENABLED', False))
EMAIL_HOST = os.environ.get('EMAIL_HOST', 'localhost')
EMAIL_PORT = os.environ.get('EMAIL_PORT', 25)
EMAIL_HOST_USER= os.environ.get('EMAIL_HOST_USER', None)
EMAIL_HOST_PASSWORD= os.environ.get('EMAIL_HOST_PASSWORD', None)
EMAIL_USE_SSL= os.environ.get('EMAIL_USE_SSL', False)

print(f"ADMINS: {ADMINS}")
print(f"SQUEST_HOST: {SQUEST_HOST}")
print(f"SQUEST_EMAIL_HOST: {SQUEST_EMAIL_HOST}")
print(f"SQUEST_EMAIL_NOTIFICATION_ENABLED: {SQUEST_EMAIL_NOTIFICATION_ENABLED}")
print(f"EMAIL_HOST: {EMAIL_HOST}")
print(f"EMAIL_PORT: {EMAIL_PORT}")
print(f"EMAIL_HOST_USER: {EMAIL_HOST_USER}")
print(f"EMAIL_USE_SSL: {EMAIL_USE_SSL}")

# -----------------------------------------
# Martor CONFIG https://github.com/agusmakmun/django-markdown-editor
Expand Down
5 changes: 5 additions & 0 deletions Squest/utils/ansible_when.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import logging

from jinja2 import Template, UndefinedError, TemplateSyntaxError

logger = logging.getLogger(__name__)

class AnsibleWhen(object):

Expand All @@ -11,9 +14,11 @@ def when_render(cls, context, when_string):
try:
template = Template(template_string)
except TemplateSyntaxError:
logger.warning(f"when_render error when templating: {context} with string '{when_string}'")
return False
try:
template_rendered = template.render(context)
return bool(template_rendered)
except UndefinedError:
logger.warning(f"when_render error when templating: {context} with string '{when_string}'")
return False
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.0"
__version__ = "2.6.1"
VERSION = __version__
37 changes: 33 additions & 4 deletions docs/configuration/squest_settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ E.G: "Use your corporate email and password".

### MAINTENANCE_MODE_ENABLED

**Default:** False
**Default:** `False`

When enabled, only administrators can access squest UI and API.
This can be used for example to block new requests by end users from the service catalog. So an administrator can perform operations against the API like migrating instance specs.
Expand All @@ -185,6 +185,17 @@ Address of the Squest portal instance. Used in email templates and in metadata s

Domain name used as email sender. E.g: "squest@squest.domain.local".

### SQUEST_ADMINS

**Default:** `''`

A list of all the email who get code error notifications. When DEBUG=False.
Example:

```text
elias.boulharts@mail.com,nicolas.marcq@mail.com
```

### SQUEST_EMAIL_NOTIFICATION_ENABLED

**Default:** Based on `DEBUG` value by default
Expand All @@ -193,7 +204,7 @@ Set to `True` to enable email notifications.

### IS_DEV_SERVER

**Default:** False
**Default:** `False`

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

Expand All @@ -209,7 +220,25 @@ The SMTP host to use for sending email.

**Default:** `25`

Port to use for the SMTP server defined in `EMAIL_HOST`.
Port to use for the SMTP server defined in `EMAIL_HOST`.

### EMAIL_HOST_USER

**Default:** `None`

User to use to authenticate with the SMTP server defined in `EMAIL_HOST` in combination with `EMAIL_HOST_PASSWORD`. Leave empty/unconfigured to send emails unauthenticated.

### EMAIL_HOST_PASSWORD

**Default:** `None`

Password to use to authenticate with the SMTP server defined in `EMAIL_HOST` in combination with `EMAIL_HOST_USER`. Leave empty/unconfigured to send emails unauthenticated.

### EMAIL_USE_SSL

**Default:** `False`

Whether to use an implicit TLS (secure) connection when talking to the SMTP server defined in `EMAIL_HOST`.

## Backup

Expand Down Expand Up @@ -287,7 +316,7 @@ Django secret key used for cryptographic signing. [Doc](https://docs.djangoproje

### DEBUG

**Default:** True
**Default:** `True`

Django DEBUG mode. Switch to `False` for production.

Expand Down
23 changes: 22 additions & 1 deletion docs/manual/service_catalog/operation.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ Operations of type "update" or "delete" can be then added to manage the lifecycl
| Default job type | Job type (Run or Check) |
| 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 |

## Job template config

By default, Squest will execute the selected Job Template with the config as set in RHAAP/AWX.
By default, Squest will execute the selected Job Template with the config as set in RHAAP/AWX.

If a field is configured to "Prompt on launch" in RHAAP/AWX, the administrator can override it from the "Process" page of an accepted request:

Expand Down Expand Up @@ -62,3 +63,23 @@ flowchart LR
**Default inventory ID** field is expecting an integer that correspond the the inventory ID in RHAAP/AWX.

**Default credential IDs** field is expecting a comma separated list of integer that correspond existings credentials ID in RHAAP/AWX.

## When condition

The when configuration allows to filter day 2 operations following conditions based on the `instance` object state.

For example, to expose an operation based on a user spec of an instance. The `when` condition can be setup like the following:
```
instance.user_spec.location==grenoble
```

With this configuration, only instances with the following user_spec will see the operation from the instance details view:
```json
{
"location": "grenoble"
}
```

!!! note

Like for Ansible, double curly braces are not used in 'when' declaration.
7 changes: 6 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "squest"
version = "2.6.0"
version = "2.6.1"
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 Expand Up @@ -30,7 +30,7 @@ drf-yasg = "1.21.7"
graphviz = "0.20.3"
django-taggit = "5.0.1"
mike = "2.1.1"
martor = "1.6.44"
martor = "^1.6.44"
django-tempus-dominus = "5.1.2.17"
gunicorn = "21.2.0"
django-tables2 = "2.7.0"
Expand Down
2 changes: 2 additions & 0 deletions service_catalog/api/serializers/request_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ def validate(self, data):
raise PermissionDenied("Operation service and instance service doesn't match")
if self.operation.type not in [OperationType.UPDATE, OperationType.DELETE]:
raise PermissionDenied("Operation type UPDATE and DELETE only")
if not self.operation.when_instance_authorized(self.squest_instance):
raise PermissionDenied("Operation not allowed")
fill_in_survey = data.get("fill_in_survey")
request_comment = data.get("request_comment")
fill_in_survey.update({"request_comment": request_comment})
Expand Down
8 changes: 7 additions & 1 deletion service_catalog/api/views/operation_api_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,10 @@ def get_object(self):
def get_queryset(self):
if getattr(self, "swagger_fake_view", False):
return Operation.objects.none()
return self.get_object().service.operations.exclude(type=OperationType.CREATE)

operations = self.get_object().service.operations.exclude(type=OperationType.CREATE)
# filter operation with when condition
for operation in operations.all():
if not operation.when_instance_authorized(self.get_object()):
operations = operations.exclude(id=operation.id)
return operations
2 changes: 1 addition & 1 deletion service_catalog/forms/operation_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Meta:
fields = ["service", "name", "description", "job_template", "type", "process_timeout_second",
"auto_accept", "auto_process", "enabled", "is_admin_operation", "extra_vars", "default_inventory_id",
"default_limits", "default_tags", "default_skip_tags", "default_credentials_ids", "default_verbosity",
"default_diff_mode", "default_job_type", "validators"]
"default_diff_mode", "default_job_type", "validators", "when"]

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down
18 changes: 18 additions & 0 deletions service_catalog/migrations/0043_operation_when.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.13 on 2024-11-15 08:27

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('service_catalog', '0042_alter_instance_options'),
]

operations = [
migrations.AddField(
model_name='operation',
name='when',
field=models.CharField(blank=True, help_text="Ansible like 'when' with `instance` as context. No Jinja brackets needed. Cannot be set on 'create' type of operation as the instance does not exist yet", max_length=2000, null=True),
),
]
12 changes: 12 additions & 0 deletions service_catalog/models/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.urls import reverse
from django.utils.translation import gettext_lazy as _

from Squest.utils.ansible_when import AnsibleWhen
from Squest.utils.plugin_controller import PluginController
from Squest.utils.squest_model import SquestModel
from service_catalog.models.job_templates import JobTemplate
Expand Down Expand Up @@ -51,6 +52,8 @@ class Operation(SquestModel):
default_job_type = CharField(max_length=500, blank=True, null=True,
help_text="Jinja supported. Job template type")
validators = CharField(null=True, blank=True, max_length=200, verbose_name="Survey validators")
when = CharField(max_length=2000, blank=True, null=True,
help_text="Ansible like 'when' with `instance` as context. No Jinja brackets needed. Cannot be set on 'create' type of operation as the instance does not exist yet")

@property
def validators_name(self):
Expand Down Expand Up @@ -155,6 +158,15 @@ def add_job_template_survey_as_default_survey(cls, sender, instance, created, *a
position += 1


def when_instance_authorized(self, instance):
from service_catalog.api.serializers import InstanceSerializer
if not self.when:
return True
when_context = {
"instance": InstanceSerializer(instance).data
}
return AnsibleWhen.when_render(context=when_context, when_string=self.when)

post_save.connect(Operation.add_job_template_survey_as_default_survey, sender=Operation)


Expand Down
12 changes: 8 additions & 4 deletions service_catalog/tables/operation_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,24 @@ class OperationTableFromInstanceDetails(SquestTable):
class Meta:
model = Operation
attrs = {"id": "operation_table", "class": "table squest-pagination-tables"}
fields = ("name", "description", "type", "is_admin_operation", "actions")
fields = ("name", "description", "type", "is_admin_operation","auto_accept", "auto_process", "actions")

type = TemplateColumn(template_name='service_catalog/custom_columns/operation_type.html')
auto_accept = TemplateColumn(template_name='generics/custom_columns/generic_boolean.html')
auto_process = TemplateColumn(template_name='generics/custom_columns/generic_boolean.html')
is_admin_operation = TemplateColumn(template_name='generics/custom_columns/generic_boolean.html')
actions = TemplateColumn(template_name='service_catalog/custom_columns/operation_request.html', orderable=False,
verbose_name="")
is_admin_operation = TemplateColumn(template_name='generics/custom_columns/generic_boolean.html')


class CreateOperationTable(SquestTable):
class Meta:
model = Operation
attrs = {"id": "operation_table", "class": "table squest-pagination-tables"}
fields = ("name", "description", "is_admin_operation", "actions")
fields = ("name", "description", "is_admin_operation", "auto_accept", "auto_process", "actions")

auto_accept = TemplateColumn(template_name='generics/custom_columns/generic_boolean.html')
auto_process = TemplateColumn(template_name='generics/custom_columns/generic_boolean.html')
is_admin_operation = TemplateColumn(template_name='generics/custom_columns/generic_boolean.html')
actions = TemplateColumn(template_name='service_catalog/custom_columns/create_operation_request.html',
orderable=False)
is_admin_operation = TemplateColumn(template_name='generics/custom_columns/generic_boolean.html')
2 changes: 1 addition & 1 deletion service_catalog/views/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def markdown_uploader(request):
Makdown image upload for locale storage
and represent as json to markdown editor.
"""
if request.method == 'POST' and request.is_ajax():
if request.method == 'POST' and request.headers.get('x-requested-with') == 'XMLHttpRequest':
if 'markdown-image-upload' in request.FILES:
image = request.FILES['markdown-image-upload']
image_types = [
Expand Down
8 changes: 8 additions & 0 deletions service_catalog/views/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ def get_context_data(self, **kwargs):
enabled=True,
type__in=[OperationType.UPDATE,
OperationType.DELETE])

# filter operation with when condition
for operation in operations.all():
if not operation.when_instance_authorized(self.object):
operations = operations.exclude(id=operation.id)

if operations.exists():
context['operations_table'] = OperationTableFromInstanceDetails(operations, prefix="operation-")
if not self.request.user.has_perm("service_catalog.admin_request_on_instance", self.object):
Expand Down Expand Up @@ -200,6 +206,8 @@ def instance_request_new_operation(request, instance_id, operation_id):
# do not allow to ask for a deletion if delete request already there
if instance.has_pending_delete_request:
raise PermissionDenied("A deletion request has already been submitted for this instance")
if not operation.when_instance_authorized(instance):
raise PermissionDenied("Operation not available for this instance")

parameters = {
'operation': operation,
Expand Down
12 changes: 8 additions & 4 deletions service_catalog/views/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,14 @@ def get_generic_url(self, action):
return ""

def get_queryset(self):
return Operation.get_queryset_for_user(self.request.user, "service_catalog.view_operation").filter(
service__id=self.kwargs.get('service_id'),
enabled=True, type=OperationType.CREATE,
)
operation_qs = Operation.get_queryset_for_user(self.request.user, "service_catalog.view_operation").filter(
service__id=self.kwargs.get('service_id'),
enabled=True, type=OperationType.CREATE,
)
if Service.get_queryset_for_user(self.request.user, perm="service_catalog.admin_request_on_service").filter(id=self.kwargs.get('service_id')).exists():
return operation_qs
else:
return operation_qs.exclude(is_admin_operation=True)

def dispatch(self, request, *args, **kwargs):
if self.get_queryset().count() == 1:
Expand Down
Loading
Loading