Merge branch 'devel' into wsrelay

This commit is contained in:
Rick Elrod
2023-01-07 15:31:51 -06:00
11 changed files with 77 additions and 22 deletions

View File

@@ -411,9 +411,11 @@ class AWXReceptorJob:
unit_status = receptor_ctl.simple_command(f'work status {self.unit_id}') unit_status = receptor_ctl.simple_command(f'work status {self.unit_id}')
detail = unit_status.get('Detail', None) detail = unit_status.get('Detail', None)
state_name = unit_status.get('StateName', None) state_name = unit_status.get('StateName', None)
stdout_size = unit_status.get('StdoutSize', 0)
except Exception: except Exception:
detail = '' detail = ''
state_name = '' state_name = ''
stdout_size = 0
logger.exception(f'An error was encountered while getting status for work unit {self.unit_id}') logger.exception(f'An error was encountered while getting status for work unit {self.unit_id}')
if 'exceeded quota' in detail: if 'exceeded quota' in detail:
@@ -424,9 +426,16 @@ class AWXReceptorJob:
return return
try: try:
resultsock = receptor_ctl.get_work_results(self.unit_id, return_sockfile=True) receptor_output = ''
lines = resultsock.readlines() if state_name == 'Failed' and self.task.runner_callback.event_ct == 0:
receptor_output = b"".join(lines).decode() # if receptor work unit failed and no events were emitted, work results may
# contain useful information about why the job failed. In case stdout is
# massive, only ask for last 1000 bytes
startpos = max(stdout_size - 1000, 0)
resultsock, resultfile = receptor_ctl.get_work_results(self.unit_id, startpos=startpos, return_socket=True, return_sockfile=True)
resultsock.setblocking(False) # this makes resultfile reads non blocking
lines = resultfile.readlines()
receptor_output = b"".join(lines).decode()
if receptor_output: if receptor_output:
self.task.runner_callback.delay_update(result_traceback=receptor_output) self.task.runner_callback.delay_update(result_traceback=receptor_output)
elif detail: elif detail:

View File

@@ -103,6 +103,10 @@ ColorHandler = logging.StreamHandler
if settings.COLOR_LOGS is True: if settings.COLOR_LOGS is True:
try: try:
from logutils.colorize import ColorizingStreamHandler from logutils.colorize import ColorizingStreamHandler
import colorama
colorama.deinit()
colorama.init(wrap=False, convert=False, strip=False)
class ColorHandler(ColorizingStreamHandler): class ColorHandler(ColorizingStreamHandler):
def colorize(self, line, record): def colorize(self, line, record):

View File

@@ -420,7 +420,7 @@ describe('<AdvancedSearch />', () => {
const selectOptions = wrapper.find( const selectOptions = wrapper.find(
'Select[aria-label="Related search type"] SelectOption' 'Select[aria-label="Related search type"] SelectOption'
); );
expect(selectOptions).toHaveLength(2); expect(selectOptions).toHaveLength(3);
expect( expect(
selectOptions.find('SelectOption[id="name-option-select"]').prop('value') selectOptions.find('SelectOption[id="name-option-select"]').prop('value')
).toBe('name__icontains'); ).toBe('name__icontains');

View File

@@ -31,6 +31,12 @@ function RelatedLookupTypeInput({
value="name__icontains" value="name__icontains"
description={t`Fuzzy search on name field.`} description={t`Fuzzy search on name field.`}
/> />
<SelectOption
id="name-exact-option-select"
key="name"
value="name"
description={t`Exact search on name field.`}
/>
<SelectOption <SelectOption
id="id-option-select" id="id-option-select"
key="id" key="id"

View File

@@ -41,7 +41,7 @@ function JobEvent({
if (lineNumber < 0) { if (lineNumber < 0) {
return null; return null;
} }
const canToggle = index === toggleLineIndex; const canToggle = index === toggleLineIndex && !event.isTracebackOnly;
return ( return (
<JobEventLine <JobEventLine
onClick={isClickable ? onJobEventClick : undefined} onClick={isClickable ? onJobEventClick : undefined}
@@ -55,7 +55,7 @@ function JobEvent({
onToggle={onToggleCollapsed} onToggle={onToggleCollapsed}
/> />
<JobEventLineNumber> <JobEventLineNumber>
{lineNumber} {!event.isTracebackOnly ? lineNumber : ''}
<JobEventEllipsis isCollapsed={isCollapsed && canToggle} /> <JobEventEllipsis isCollapsed={isCollapsed && canToggle} />
</JobEventLineNumber> </JobEventLineNumber>
<JobEventLineText <JobEventLineText

View File

@@ -187,7 +187,9 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
useEffect(() => { useEffect(() => {
const pendingRequests = Object.values(eventByUuidRequests.current || {}); const pendingRequests = Object.values(eventByUuidRequests.current || {});
setHasContentLoading(true); // prevents "no content found" screen from flashing setHasContentLoading(true); // prevents "no content found" screen from flashing
setIsFollowModeEnabled(false); if (location.search) {
setIsFollowModeEnabled(false);
}
Promise.allSettled(pendingRequests).then(() => { Promise.allSettled(pendingRequests).then(() => {
setRemoteRowCount(0); setRemoteRowCount(0);
clearLoadedEvents(); clearLoadedEvents();
@@ -251,6 +253,9 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
}); });
const updated = oldWsEvents.concat(newEvents); const updated = oldWsEvents.concat(newEvents);
jobSocketCounter.current = updated.length; jobSocketCounter.current = updated.length;
if (!oldWsEvents.length && min > remoteRowCount + 1) {
loadJobEvents(min);
}
return updated.sort((a, b) => a.counter - b.counter); return updated.sort((a, b) => a.counter - b.counter);
}); });
setCssMap((prevCssMap) => ({ setCssMap((prevCssMap) => ({
@@ -358,7 +363,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
} }
}; };
const loadJobEvents = async () => { const loadJobEvents = async (firstWsCounter = null) => {
const [params, loadRange] = getEventRequestParams(job, 50, [1, 50]); const [params, loadRange] = getEventRequestParams(job, 50, [1, 50]);
if (isMounted.current) { if (isMounted.current) {
@@ -371,6 +376,9 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
if (isFlatMode) { if (isFlatMode) {
params.not__stdout = ''; params.not__stdout = '';
} }
if (firstWsCounter) {
params.counter__lt = firstWsCounter;
}
const qsParams = parseQueryString(QS_CONFIG, location.search); const qsParams = parseQueryString(QS_CONFIG, location.search);
const eventPromise = getJobModel(job.type).readEvents(job.id, { const eventPromise = getJobModel(job.type).readEvents(job.id, {
...params, ...params,
@@ -435,7 +443,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
if (getEvent(counter)) { if (getEvent(counter)) {
return true; return true;
} }
if (index > remoteRowCount && index < remoteRowCount + wsEvents.length) { if (index >= remoteRowCount && index < remoteRowCount + wsEvents.length) {
return true; return true;
} }
return currentlyLoading.includes(counter); return currentlyLoading.includes(counter);
@@ -462,7 +470,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
} }
if ( if (
!event && !event &&
index > remoteRowCount && index >= remoteRowCount &&
index < remoteRowCount + wsEvents.length index < remoteRowCount + wsEvents.length
) { ) {
event = wsEvents[index - remoteRowCount]; event = wsEvents[index - remoteRowCount];
@@ -629,10 +637,14 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
setIsFollowModeEnabled(false); setIsFollowModeEnabled(false);
}; };
const scrollToEnd = () => { const scrollToEnd = useCallback(() => {
scrollToRow(-1); scrollToRow(-1);
setTimeout(() => scrollToRow(-1), 100); let timeout;
}; if (isFollowModeEnabled) {
setTimeout(() => scrollToRow(-1), 100);
}
return () => clearTimeout(timeout);
}, [isFollowModeEnabled]);
const handleScrollLast = () => { const handleScrollLast = () => {
scrollToEnd(); scrollToEnd();

View File

@@ -29,8 +29,11 @@ export function prependTraceback(job, events) {
start_line: 0, start_line: 0,
}; };
const firstIndex = events.findIndex((jobEvent) => jobEvent.counter === 1); const firstIndex = events.findIndex((jobEvent) => jobEvent.counter === 1);
if (firstIndex && events[firstIndex]?.stdout) { if (firstIndex > -1) {
const stdoutLines = events[firstIndex].stdout.split('\r\n'); if (!events[firstIndex].stdout) {
events[firstIndex].isTracebackOnly = true;
}
const stdoutLines = events[firstIndex].stdout?.split('\r\n') || [];
stdoutLines[0] = tracebackEvent.stdout; stdoutLines[0] = tracebackEvent.stdout;
events[firstIndex].stdout = stdoutLines.join('\r\n'); events[firstIndex].stdout = stdoutLines.join('\r\n');
} else { } else {

View File

@@ -141,14 +141,14 @@ function JobsEdit() {
<FormColumnLayout> <FormColumnLayout>
<InputField <InputField
name="AWX_ISOLATION_BASE_PATH" name="AWX_ISOLATION_BASE_PATH"
config={jobs.AWX_ISOLATION_BASE_PATH} config={jobs.AWX_ISOLATION_BASE_PATH ?? null}
isRequired isRequired={Boolean(options?.AWX_ISOLATION_BASE_PATH)}
/> />
<InputField <InputField
name="SCHEDULE_MAX_JOBS" name="SCHEDULE_MAX_JOBS"
config={jobs.SCHEDULE_MAX_JOBS} config={jobs.SCHEDULE_MAX_JOBS ?? null}
type="number" type={options?.SCHEDULE_MAX_JOBS ? 'number' : undefined}
isRequired isRequired={Boolean(options?.SCHEDULE_MAX_JOBS)}
/> />
<InputField <InputField
name="DEFAULT_JOB_TIMEOUT" name="DEFAULT_JOB_TIMEOUT"

View File

@@ -122,4 +122,22 @@ describe('<JobsEdit />', () => {
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
expect(wrapper.find('ContentError').length).toBe(1); expect(wrapper.find('ContentError').length).toBe(1);
}); });
test('Form input fields that are invisible (due to being set manually via a settings file) should not prevent submitting the form', async () => {
const mockOptions = Object.assign({}, mockAllOptions);
// If AWX_ISOLATION_BASE_PATH has been set in a settings file it will be absent in the PUT options
delete mockOptions['actions']['PUT']['AWX_ISOLATION_BASE_PATH'];
await act(async () => {
wrapper = mountWithContexts(
<SettingsProvider value={mockOptions.actions}>
<JobsEdit />
</SettingsProvider>
);
});
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
await act(async () => {
wrapper.find('Form').invoke('onSubmit')();
});
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
});
}); });

View File

@@ -397,7 +397,10 @@ const InputField = ({ name, config, type = 'text', isRequired = false }) => {
}; };
InputField.propTypes = { InputField.propTypes = {
name: string.isRequired, name: string.isRequired,
config: shape({}).isRequired, config: shape({}),
};
InputField.defaultProps = {
config: null,
}; };
const TextAreaField = ({ name, config, isRequired = false }) => { const TextAreaField = ({ name, config, isRequired = false }) => {

View File

@@ -116,7 +116,7 @@ RUN dnf -y update && dnf install -y 'dnf-command(config-manager)' && \
python3-psycopg2 \ python3-psycopg2 \
python3-setuptools \ python3-setuptools \
rsync \ rsync \
"rsyslog >= 8.1911.0" \ rsyslog-8.2102.0-106.el9 \
subversion \ subversion \
sudo \ sudo \
vim-minimal \ vim-minimal \