Compare commits

...

11 Commits

Author SHA1 Message Date
Marliana Lara
2a1dffd363 Fix edit constructed inventory hanging loading state (#14343) 2023-08-21 12:36:36 -04:00
digitalbadger-uk
8c7ab8fcf2 Added required epoc time field for Splunk HEC Event Receiver (#14246)
Signed-off-by: Iain <iain@digitalbadger.com>
2023-08-21 09:44:52 -03:00
Hao Liu
3de8455960 Fix missing trailing / in PUBLIC_PATH for UI
Missing trialing `/` cause UI to load file from incorrect static dir location.
2023-08-17 15:16:59 -04:00
Hao Liu
d832e75e99 Fix ui-next build step file path issue
Add full path for the mv command so that the command can be run from ui_next and from project root.

Additionally move the rename of file to src build step.
2023-08-17 15:16:59 -04:00
abwalczyk
a89e266feb Fixed task and web docs (#14350) 2023-08-17 12:22:51 -04:00
Hao Liu
8e1516eeb7 Update UI_NEXT build to set PRODUCT and PUBLIC_PATH
https://github.com/ansible/ansible-ui/pull/792 added configurable public path (which was change to '/' in https://github.com/ansible/ansible-ui/pull/766/files#diff-2606df06d89b38ff979770f810c3c269083e7c0fbafb27aba7f9ea0297179828L128-R157)

This PR added the variable when building ui-next
2023-08-16 18:35:12 -04:00
Hao Liu
c7f2fdbe57 Rename ui_next index.html to index_awx.html during build process
Due to change made in https://github.com/ansible/ansible-ui/pull/766/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519R18 awx/ui_next/build/awx/index_awx.html was renamed to awx/ui_next/build/awx/index.html

This PR fixes the problem by renaming the file back
2023-08-16 18:35:12 -04:00
delinea-sagar
c75757bf22 Update python-tss-sdk dependency (#14207)
Signed-off-by: delinea-sagar <sagar.wani@c.delinea.com>
2023-08-16 20:07:35 +00:00
Kevin Pavon
b8ec7c4072 Schedule rruleset fix related #13446 (#13611)
Signed-off-by: Kevin Pavon <7450065+KaraokeKev@users.noreply.github.com>
Co-authored-by: Jessica Steurer <70719005+jay-steurer@users.noreply.github.com>
2023-08-16 16:10:31 -03:00
jbreitwe-rh
bb1c155bc9 Fixed typos (#14347) 2023-08-16 15:05:23 -04:00
Jeff Bradberry
4822dd79fc Revert "Improve performance for awx cli export (#13182)" 2023-08-15 15:55:10 -04:00
23 changed files with 233 additions and 137 deletions

View File

@@ -1,7 +1,10 @@
from .plugin import CredentialPlugin
from django.utils.translation import gettext_lazy as _
from thycotic.secrets.server import DomainPasswordGrantAuthorizer, PasswordGrantAuthorizer, SecretServer, ServerSecret
try:
from delinea.secrets.server import DomainPasswordGrantAuthorizer, PasswordGrantAuthorizer, SecretServer, ServerSecret
except ImportError:
from thycotic.secrets.server import DomainPasswordGrantAuthorizer, PasswordGrantAuthorizer, SecretServer, ServerSecret
tss_inputs = {
'fields': [

View File

@@ -161,7 +161,7 @@ class AWXConsumerRedis(AWXConsumerBase):
class AWXConsumerPG(AWXConsumerBase):
def __init__(self, *args, schedule=None, **kwargs):
super().__init__(*args, **kwargs)
self.pg_max_wait = settings.DISPATCHER_DB_DOWNTOWN_TOLLERANCE
self.pg_max_wait = settings.DISPATCHER_DB_DOWNTIME_TOLERANCE
# if no successful loops have ran since startup, then we should fail right away
self.pg_is_down = True # set so that we fail if we get database errors on startup
init_time = time.time()

View File

@@ -29,7 +29,7 @@ class RunnerCallback:
self.safe_env = {}
self.event_ct = 0
self.model = model
self.update_attempts = int(settings.DISPATCHER_DB_DOWNTOWN_TOLLERANCE / 5)
self.update_attempts = int(settings.DISPATCHER_DB_DOWNTIME_TOLERANCE / 5)
self.wrapup_event_dispatched = False
self.artifacts_processed = False
self.extra_update_fields = {}

View File

@@ -112,7 +112,7 @@ class BaseTask(object):
def __init__(self):
self.cleanup_paths = []
self.update_attempts = int(settings.DISPATCHER_DB_DOWNTOWN_TOLLERANCE / 5)
self.update_attempts = int(settings.DISPATCHER_DB_DOWNTIME_TOLERANCE / 5)
self.runner_callback = self.callback_class(model=self.model)
def update_model(self, pk, _attempt=0, **updates):

View File

@@ -283,6 +283,7 @@ class LogstashFormatter(LogstashFormatterBase):
message.update(self.get_debug_fields(record))
if settings.LOG_AGGREGATOR_TYPE == 'splunk':
# splunk messages must have a top level "event" key
message = {'event': message}
# splunk messages must have a top level "event" key when using the /services/collector/event receiver.
# The event receiver wont scan an event for a timestamp field therefore a time field must also be supplied containing epoch timestamp
message = {'time': record.created, 'event': message}
return self.serialize(message)

View File

@@ -453,7 +453,7 @@ RECEPTOR_SERVICE_ADVERTISEMENT_PERIOD = 60 # https://github.com/ansible/recepto
EXECUTION_NODE_REMEDIATION_CHECKS = 60 * 30 # once every 30 minutes check if an execution node errors have been resolved
# Amount of time dispatcher will try to reconnect to database for jobs and consuming new work
DISPATCHER_DB_DOWNTOWN_TOLLERANCE = 40
DISPATCHER_DB_DOWNTIME_TOLERANCE = 40
BROKER_URL = 'unix:///var/run/redis/redis.sock'
CELERYBEAT_SCHEDULE = {

View File

@@ -6,5 +6,20 @@ class ConstructedInventories extends InstanceGroupsMixin(Base) {
super(http);
this.baseUrl = 'api/v2/constructed_inventories/';
}
async readConstructedInventoryOptions(id, method) {
const {
data: { actions },
} = await this.http.options(`${this.baseUrl}${id}/`);
if (actions[method]) {
return actions[method];
}
throw new Error(
`You have insufficient access to this Constructed Inventory.
Please contact your system administrator if there is an issue with your access.`
);
}
}
export default ConstructedInventories;

View File

@@ -0,0 +1,51 @@
import ConstructedInventories from './ConstructedInventories';
describe('ConstructedInventoriesAPI', () => {
const constructedInventoryId = 1;
const constructedInventoryMethod = 'PUT';
let ConstructedInventoriesAPI;
let mockHttp;
beforeEach(() => {
const optionsPromise = () =>
Promise.resolve({
data: {
actions: {
PUT: {},
},
},
});
mockHttp = {
options: jest.fn(optionsPromise),
};
ConstructedInventoriesAPI = new ConstructedInventories(mockHttp);
});
afterEach(() => {
jest.resetAllMocks();
});
test('readConstructedInventoryOptions calls options with the expected params', async () => {
await ConstructedInventoriesAPI.readConstructedInventoryOptions(
constructedInventoryId,
constructedInventoryMethod
);
expect(mockHttp.options).toHaveBeenCalledTimes(1);
expect(mockHttp.options).toHaveBeenCalledWith(
`api/v2/constructed_inventories/${constructedInventoryId}/`
);
});
test('readConstructedInventory should throw an error if action method is missing', async () => {
try {
await ConstructedInventoriesAPI.readConstructedInventoryOptions(
constructedInventoryId,
'POST'
);
} catch (error) {
expect(error.message).toContain(
'You have insufficient access to this Constructed Inventory.'
);
}
});
});

View File

@@ -1,14 +1,43 @@
import React, { useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Card, PageSection } from '@patternfly/react-core';
import { ConstructedInventoriesAPI, InventoriesAPI } from 'api';
import useRequest from 'hooks/useRequest';
import { CardBody } from 'components/Card';
import ContentError from 'components/ContentError';
import ContentLoading from 'components/ContentLoading';
import ConstructedInventoryForm from '../shared/ConstructedInventoryForm';
function ConstructedInventoryAdd() {
const history = useHistory();
const [submitError, setSubmitError] = useState(null);
const {
isLoading: isLoadingOptions,
error: optionsError,
request: fetchOptions,
result: options,
} = useRequest(
useCallback(async () => {
const res = await ConstructedInventoriesAPI.readOptions();
const { data } = res;
return data.actions.POST;
}, []),
null
);
useEffect(() => {
fetchOptions();
}, [fetchOptions]);
if (isLoadingOptions || (!options && !optionsError)) {
return <ContentLoading />;
}
if (optionsError) {
return <ContentError error={optionsError} />;
}
const handleCancel = () => {
history.push('/inventories');
};
@@ -48,6 +77,7 @@ function ConstructedInventoryAdd() {
onCancel={handleCancel}
onSubmit={handleSubmit}
submitError={submitError}
options={options}
/>
</CardBody>
</Card>

View File

@@ -55,6 +55,7 @@ describe('<ConstructedInventoryAdd />', () => {
context: { router: { history } },
});
});
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
});
afterEach(() => {

View File

@@ -20,6 +20,27 @@ function ConstructedInventoryEdit({ inventory }) {
const detailsUrl = `/inventories/constructed_inventory/${inventory.id}/details`;
const constructedInventoryId = inventory.id;
const {
isLoading: isLoadingOptions,
error: optionsError,
request: fetchOptions,
result: options,
} = useRequest(
useCallback(
() =>
ConstructedInventoriesAPI.readConstructedInventoryOptions(
constructedInventoryId,
'PUT'
),
[constructedInventoryId]
),
null
);
useEffect(() => {
fetchOptions();
}, [fetchOptions]);
const {
result: { initialInstanceGroups, initialInputInventories },
request: fetchedRelatedData,
@@ -44,6 +65,7 @@ function ConstructedInventoryEdit({ inventory }) {
isLoading: true,
}
);
useEffect(() => {
fetchedRelatedData();
}, [fetchedRelatedData]);
@@ -99,12 +121,12 @@ function ConstructedInventoryEdit({ inventory }) {
const handleCancel = () => history.push(detailsUrl);
if (isLoading) {
return <ContentLoading />;
if (contentError || optionsError) {
return <ContentError error={contentError || optionsError} />;
}
if (contentError) {
return <ContentError error={contentError} />;
if (isLoading || isLoadingOptions || (!options && !optionsError)) {
return <ContentLoading />;
}
return (
@@ -116,6 +138,7 @@ function ConstructedInventoryEdit({ inventory }) {
constructedInventory={inventory}
instanceGroups={initialInstanceGroups}
inputInventories={initialInputInventories}
options={options}
/>
</CardBody>
);

View File

@@ -51,27 +51,22 @@ describe('<ConstructedInventoryEdit />', () => {
};
beforeEach(async () => {
ConstructedInventoriesAPI.readOptions.mockResolvedValue({
data: {
related: {},
actions: {
POST: {
limit: {
label: 'Limit',
help_text: '',
},
update_cache_timeout: {
label: 'Update cache timeout',
help_text: 'help',
},
verbosity: {
label: 'Verbosity',
help_text: '',
},
},
ConstructedInventoriesAPI.readConstructedInventoryOptions.mockResolvedValue(
{
limit: {
label: 'Limit',
help_text: '',
},
},
});
update_cache_timeout: {
label: 'Update cache timeout',
help_text: 'help',
},
verbosity: {
label: 'Verbosity',
help_text: '',
},
}
);
InventoriesAPI.readInstanceGroups.mockResolvedValue({
data: {
results: associatedInstanceGroups,
@@ -169,6 +164,21 @@ describe('<ConstructedInventoryEdit />', () => {
expect(wrapper.find('ContentError').length).toBe(1);
});
test('should throw content error if user has insufficient options permissions', async () => {
expect(wrapper.find('ContentError').length).toBe(0);
ConstructedInventoriesAPI.readConstructedInventoryOptions.mockImplementationOnce(
() => Promise.reject(new Error())
);
await act(async () => {
wrapper = mountWithContexts(
<ConstructedInventoryEdit inventory={mockInv} />
);
});
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
expect(wrapper.find('ContentError').length).toBe(1);
});
test('unsuccessful form submission should show an error message', async () => {
const error = {
response: {

View File

@@ -1,14 +1,10 @@
import React, { useCallback, useEffect } from 'react';
import React, { useCallback } from 'react';
import { Formik, useField, useFormikContext } from 'formik';
import { func, shape } from 'prop-types';
import { t } from '@lingui/macro';
import { ConstructedInventoriesAPI } from 'api';
import { minMaxValue, required } from 'util/validators';
import useRequest from 'hooks/useRequest';
import { Form, FormGroup } from '@patternfly/react-core';
import { VariablesField } from 'components/CodeEditor';
import ContentError from 'components/ContentError';
import ContentLoading from 'components/ContentLoading';
import FormActionGroup from 'components/FormActionGroup/FormActionGroup';
import FormField, { FormSubmitError } from 'components/FormField';
import { FormFullWidthLayout, FormColumnLayout } from 'components/FormLayout';
@@ -165,6 +161,7 @@ function ConstructedInventoryForm({
onCancel,
onSubmit,
submitError,
options,
}) {
const initialValues = {
kind: 'constructed',
@@ -179,32 +176,6 @@ function ConstructedInventoryForm({
source_vars: constructedInventory?.source_vars || '---',
};
const {
isLoading,
error,
request: fetchOptions,
result: options,
} = useRequest(
useCallback(async () => {
const res = await ConstructedInventoriesAPI.readOptions();
const { data } = res;
return data.actions.POST;
}, []),
null
);
useEffect(() => {
fetchOptions();
}, [fetchOptions]);
if (isLoading || (!options && !error)) {
return <ContentLoading />;
}
if (error) {
return <ContentError error={error} />;
}
return (
<Formik initialValues={initialValues} onSubmit={onSubmit}>
{(formik) => (

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { ConstructedInventoriesAPI } from 'api';
import {
mountWithContexts,
waitForElement,
@@ -19,38 +18,35 @@ const mockFormValues = {
inputInventories: [{ id: 100, name: 'East' }],
};
const options = {
limit: {
label: 'Limit',
help_text: '',
},
update_cache_timeout: {
label: 'Update cache timeout',
help_text: 'help',
},
verbosity: {
label: 'Verbosity',
help_text: '',
},
};
describe('<ConstructedInventoryForm />', () => {
let wrapper;
const onSubmit = jest.fn();
beforeEach(async () => {
ConstructedInventoriesAPI.readOptions.mockResolvedValue({
data: {
related: {},
actions: {
POST: {
limit: {
label: 'Limit',
help_text: '',
},
update_cache_timeout: {
label: 'Update cache timeout',
help_text: 'help',
},
verbosity: {
label: 'Verbosity',
help_text: '',
},
},
},
},
});
await act(async () => {
wrapper = mountWithContexts(
<ConstructedInventoryForm onCancel={() => {}} onSubmit={onSubmit} />
<ConstructedInventoryForm
onCancel={() => {}}
onSubmit={onSubmit}
options={options}
/>
);
});
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
});
afterEach(() => {
@@ -104,20 +100,4 @@ describe('<ConstructedInventoryForm />', () => {
'The plugin parameter is required.'
);
});
test('should throw content error when option request fails', async () => {
let newWrapper;
ConstructedInventoriesAPI.readOptions.mockImplementationOnce(() =>
Promise.reject(new Error())
);
await act(async () => {
newWrapper = mountWithContexts(
<ConstructedInventoryForm onCancel={() => {}} onSubmit={() => {}} />
);
});
expect(newWrapper.find('ContentError').length).toBe(0);
newWrapper.update();
expect(newWrapper.find('ContentError').length).toBe(1);
jest.clearAllMocks();
});
});

View File

@@ -5,10 +5,13 @@ UI_NEXT_DIR := $(patsubst %/,%,$(dir $(lastword $(MAKEFILE_LIST))))
# NOTE: you will not be able to build within the docker-compose development environment if you use this option
UI_NEXT_LOCAL ?=
# Git repo and branch to the UI_NEXT repo
## Git repo and branch to the UI_NEXT repo
UI_NEXT_GIT_REPO ?= https://github.com/ansible/ansible-ui.git
UI_NEXT_GIT_BRANCH ?= main
## Product name to display on the UI used in UI_NEXT build process
PRODUCT ?= AWX
.PHONY: ui-next
## Default build target of ui-next Makefile, builds ui-next/build
ui-next: ui-next/build
@@ -32,7 +35,8 @@ ui-next/src/build: $(UI_NEXT_DIR)/src/build/awx
## True target for ui-next/src/build. Build ui_next from source.
$(UI_NEXT_DIR)/src/build/awx: $(UI_NEXT_DIR)/src $(UI_NEXT_DIR)/src/node_modules/webpack
@echo "=== Building ui_next ==="
@cd $(UI_NEXT_DIR)/src && npm run build:awx
@cd $(UI_NEXT_DIR)/src && PRODUCT="$(PRODUCT)" PUBLIC_PATH=/static/awx/ npm run build:awx
@mv $(UI_NEXT_DIR)/src/build/awx/index.html $(UI_NEXT_DIR)/src/build/awx/index_awx.html
.PHONY: ui-next/src
## Clone or link src of UI_NEXT to ui-next/src, will re-clone/link/update if necessary.

View File

@@ -214,7 +214,7 @@ class LookupModule(LookupBase):
if not isinstance(rule[field_name], list):
rule[field_name] = rule[field_name].split(',')
for value in rule[field_name]:
value = value.strip()
value = value.strip().lower()
if value not in valid_list:
raise AnsibleError('In rule {0} {1} must only contain values in {2}'.format(rule_number, field_name, ', '.join(valid_list.keys())))
return_values.append(valid_list[value])

View File

@@ -356,3 +356,19 @@
that:
- results is success
- "'DTSTART;TZID=UTC:20220430T103045 RRULE:FREQ=MONTHLY;BYMONTHDAY=12,13,14,15,16,17,18;BYDAY=SA;INTERVAL=1' == complex_rule"
- name: mondays, Tuesdays, and WEDNESDAY with case-insensitivity
set_fact:
complex_rule: "{{ query(ruleset_plugin_name, '2022-04-30 10:30:45', rules=rrules, timezone='UTC' ) }}"
ignore_errors: True
register: results
vars:
rrules:
- frequency: 'day'
interval: 1
byweekday: monday, Tuesday, WEDNESDAY
- assert:
that:
- results is success
- "'DTSTART;TZID=UTC:20220430T103045 RRULE:FREQ=DAILY;BYDAY=MO,TU,WE;INTERVAL=1' == complex_rule"

View File

@@ -162,7 +162,7 @@ class ApiV2(base.Base):
export_key = 'create_approval_template'
rel_option_endpoint = _page.related.get('create_approval_template')
rel_post_fields = self._cache.get_post_fields(rel_option_endpoint)
rel_post_fields = utils.get_post_fields(rel_option_endpoint, self._cache)
if rel_post_fields is None:
log.debug("%s is a read-only endpoint.", rel_endpoint)
continue
@@ -202,7 +202,7 @@ class ApiV2(base.Base):
return utils.remove_encrypted(fields)
def _export_list(self, endpoint):
post_fields = self._cache.get_post_fields(endpoint)
post_fields = utils.get_post_fields(endpoint, self._cache)
if post_fields is None:
return None
@@ -267,7 +267,7 @@ class ApiV2(base.Base):
def _import_list(self, endpoint, assets):
log.debug("_import_list -- endpoint: %s, assets: %s", endpoint.endpoint, repr(assets))
post_fields = self._cache.get_post_fields(endpoint)
post_fields = utils.get_post_fields(endpoint, self._cache)
changed = False

View File

@@ -495,7 +495,6 @@ class TentativePage(str):
class PageCache(object):
def __init__(self):
self.options = {}
self.post_fields = {}
self.pages_by_url = {}
self.pages_by_natural_key = {}
@@ -517,29 +516,6 @@ class PageCache(object):
return self.options.setdefault(url, options)
def get_post_fields(self, page):
url = page.endpoint if isinstance(page, Page) else str(page)
key = get_registered_page(url)
if key in self.post_fields:
return self.post_fields[key]
options_page = self.get_options(page)
if options_page is None:
return None
if 'POST' not in options_page.r.headers.get('Allow', ''):
return None
if 'POST' in options_page.json['actions']:
post_fields = options_page.json['actions']['POST']
else:
log.warning("Insufficient privileges on %s, inferring POST fields from description.", options_page.endpoint)
post_fields = utils.parse_description(options_page.json['description'])
self.post_fields[key] = post_fields
return post_fields
def set_page(self, page):
log.debug("set_page: %s %s", type(page), page.endpoint)
self.pages_by_url[page.endpoint] = page

View File

@@ -31,3 +31,18 @@ def remove_encrypted(value):
if isinstance(value, dict):
return {k: remove_encrypted(v) for k, v in value.items()}
return value
def get_post_fields(page, cache):
options_page = cache.get_options(page)
if options_page is None:
return None
if 'POST' not in options_page.r.headers.get('Allow', ''):
return None
if 'POST' in options_page.json['actions']:
return options_page.json['actions']['POST']
else:
log.warning("Insufficient privileges on %s, inferring POST fields from description.", options_page.endpoint)
return parse_description(options_page.json['description'])

View File

@@ -137,12 +137,12 @@ To retrieve your admin password
To tail logs from the task containers
```bash
kubectl logs -f deployment/awx -n awx -c awx-web
kubectl logs -f deployment/awx-task -n awx -c awx-task
```
To tail logs from the web containers
```bash
kubectl logs -f deployment/awx -n awx -c awx-web
kubectl logs -f deployment/awx-web -n awx -c awx-web
```
NOTE: If there's multiple replica of the awx deployment you can use `stern` to tail logs from all replicas. For more information about `stern` check out https://github.com/wercker/stern.

View File

@@ -42,7 +42,7 @@ pygerduty
pyparsing==2.4.6 # Upgrading to v3 of pyparsing introduce errors on smart host filtering: Expected 'or' term, found 'or' (at char 15), (line:1, col:16)
python-daemon>3.0.0
python-dsv-sdk
python-tss-sdk==1.0.0
python-tss-sdk==1.2.1
python-ldap
pyyaml>=6.0.1
receptorctl==1.3.0

View File

@@ -333,7 +333,7 @@ python-ldap==3.4.3
# django-auth-ldap
python-string-utils==1.0.0
# via openshift
python-tss-sdk==1.0.0
python-tss-sdk==1.2.1
# via -r /awx_devel/requirements/requirements.in
python3-openid==3.2.0
# via social-auth-core