mirror of
https://github.com/ansible/awx.git
synced 2026-02-12 07:04:45 -03:30
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a1dffd363 | ||
|
|
8c7ab8fcf2 | ||
|
|
3de8455960 | ||
|
|
d832e75e99 | ||
|
|
a89e266feb | ||
|
|
8e1516eeb7 | ||
|
|
c7f2fdbe57 | ||
|
|
c75757bf22 | ||
|
|
b8ec7c4072 | ||
|
|
bb1c155bc9 | ||
|
|
4822dd79fc |
@@ -1,7 +1,10 @@
|
|||||||
from .plugin import CredentialPlugin
|
from .plugin import CredentialPlugin
|
||||||
from django.utils.translation import gettext_lazy as _
|
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 = {
|
tss_inputs = {
|
||||||
'fields': [
|
'fields': [
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ class AWXConsumerRedis(AWXConsumerBase):
|
|||||||
class AWXConsumerPG(AWXConsumerBase):
|
class AWXConsumerPG(AWXConsumerBase):
|
||||||
def __init__(self, *args, schedule=None, **kwargs):
|
def __init__(self, *args, schedule=None, **kwargs):
|
||||||
super().__init__(*args, **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
|
# 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
|
self.pg_is_down = True # set so that we fail if we get database errors on startup
|
||||||
init_time = time.time()
|
init_time = time.time()
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class RunnerCallback:
|
|||||||
self.safe_env = {}
|
self.safe_env = {}
|
||||||
self.event_ct = 0
|
self.event_ct = 0
|
||||||
self.model = model
|
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.wrapup_event_dispatched = False
|
||||||
self.artifacts_processed = False
|
self.artifacts_processed = False
|
||||||
self.extra_update_fields = {}
|
self.extra_update_fields = {}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ class BaseTask(object):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.cleanup_paths = []
|
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)
|
self.runner_callback = self.callback_class(model=self.model)
|
||||||
|
|
||||||
def update_model(self, pk, _attempt=0, **updates):
|
def update_model(self, pk, _attempt=0, **updates):
|
||||||
|
|||||||
@@ -283,6 +283,7 @@ class LogstashFormatter(LogstashFormatterBase):
|
|||||||
message.update(self.get_debug_fields(record))
|
message.update(self.get_debug_fields(record))
|
||||||
|
|
||||||
if settings.LOG_AGGREGATOR_TYPE == 'splunk':
|
if settings.LOG_AGGREGATOR_TYPE == 'splunk':
|
||||||
# splunk messages must have a top level "event" key
|
# splunk messages must have a top level "event" key when using the /services/collector/event receiver.
|
||||||
message = {'event': message}
|
# 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)
|
return self.serialize(message)
|
||||||
|
|||||||
@@ -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
|
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
|
# 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'
|
BROKER_URL = 'unix:///var/run/redis/redis.sock'
|
||||||
CELERYBEAT_SCHEDULE = {
|
CELERYBEAT_SCHEDULE = {
|
||||||
|
|||||||
@@ -6,5 +6,20 @@ class ConstructedInventories extends InstanceGroupsMixin(Base) {
|
|||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = 'api/v2/constructed_inventories/';
|
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;
|
export default ConstructedInventories;
|
||||||
|
|||||||
51
awx/ui/src/api/models/ConstructedInventories.test.js
Normal file
51
awx/ui/src/api/models/ConstructedInventories.test.js
Normal 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.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,14 +1,43 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { Card, PageSection } from '@patternfly/react-core';
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
import { ConstructedInventoriesAPI, InventoriesAPI } from 'api';
|
import { ConstructedInventoriesAPI, InventoriesAPI } from 'api';
|
||||||
|
import useRequest from 'hooks/useRequest';
|
||||||
import { CardBody } from 'components/Card';
|
import { CardBody } from 'components/Card';
|
||||||
|
import ContentError from 'components/ContentError';
|
||||||
|
import ContentLoading from 'components/ContentLoading';
|
||||||
import ConstructedInventoryForm from '../shared/ConstructedInventoryForm';
|
import ConstructedInventoryForm from '../shared/ConstructedInventoryForm';
|
||||||
|
|
||||||
function ConstructedInventoryAdd() {
|
function ConstructedInventoryAdd() {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [submitError, setSubmitError] = useState(null);
|
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 = () => {
|
const handleCancel = () => {
|
||||||
history.push('/inventories');
|
history.push('/inventories');
|
||||||
};
|
};
|
||||||
@@ -48,6 +77,7 @@ function ConstructedInventoryAdd() {
|
|||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
submitError={submitError}
|
submitError={submitError}
|
||||||
|
options={options}
|
||||||
/>
|
/>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ describe('<ConstructedInventoryAdd />', () => {
|
|||||||
context: { router: { history } },
|
context: { router: { history } },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -20,6 +20,27 @@ function ConstructedInventoryEdit({ inventory }) {
|
|||||||
const detailsUrl = `/inventories/constructed_inventory/${inventory.id}/details`;
|
const detailsUrl = `/inventories/constructed_inventory/${inventory.id}/details`;
|
||||||
const constructedInventoryId = inventory.id;
|
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 {
|
const {
|
||||||
result: { initialInstanceGroups, initialInputInventories },
|
result: { initialInstanceGroups, initialInputInventories },
|
||||||
request: fetchedRelatedData,
|
request: fetchedRelatedData,
|
||||||
@@ -44,6 +65,7 @@ function ConstructedInventoryEdit({ inventory }) {
|
|||||||
isLoading: true,
|
isLoading: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchedRelatedData();
|
fetchedRelatedData();
|
||||||
}, [fetchedRelatedData]);
|
}, [fetchedRelatedData]);
|
||||||
@@ -99,12 +121,12 @@ function ConstructedInventoryEdit({ inventory }) {
|
|||||||
|
|
||||||
const handleCancel = () => history.push(detailsUrl);
|
const handleCancel = () => history.push(detailsUrl);
|
||||||
|
|
||||||
if (isLoading) {
|
if (contentError || optionsError) {
|
||||||
return <ContentLoading />;
|
return <ContentError error={contentError || optionsError} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentError) {
|
if (isLoading || isLoadingOptions || (!options && !optionsError)) {
|
||||||
return <ContentError error={contentError} />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -116,6 +138,7 @@ function ConstructedInventoryEdit({ inventory }) {
|
|||||||
constructedInventory={inventory}
|
constructedInventory={inventory}
|
||||||
instanceGroups={initialInstanceGroups}
|
instanceGroups={initialInstanceGroups}
|
||||||
inputInventories={initialInputInventories}
|
inputInventories={initialInputInventories}
|
||||||
|
options={options}
|
||||||
/>
|
/>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -51,27 +51,22 @@ describe('<ConstructedInventoryEdit />', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
ConstructedInventoriesAPI.readOptions.mockResolvedValue({
|
ConstructedInventoriesAPI.readConstructedInventoryOptions.mockResolvedValue(
|
||||||
data: {
|
{
|
||||||
related: {},
|
limit: {
|
||||||
actions: {
|
label: 'Limit',
|
||||||
POST: {
|
help_text: '',
|
||||||
limit: {
|
|
||||||
label: 'Limit',
|
|
||||||
help_text: '',
|
|
||||||
},
|
|
||||||
update_cache_timeout: {
|
|
||||||
label: 'Update cache timeout',
|
|
||||||
help_text: 'help',
|
|
||||||
},
|
|
||||||
verbosity: {
|
|
||||||
label: 'Verbosity',
|
|
||||||
help_text: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
update_cache_timeout: {
|
||||||
});
|
label: 'Update cache timeout',
|
||||||
|
help_text: 'help',
|
||||||
|
},
|
||||||
|
verbosity: {
|
||||||
|
label: 'Verbosity',
|
||||||
|
help_text: '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
InventoriesAPI.readInstanceGroups.mockResolvedValue({
|
InventoriesAPI.readInstanceGroups.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
results: associatedInstanceGroups,
|
results: associatedInstanceGroups,
|
||||||
@@ -169,6 +164,21 @@ describe('<ConstructedInventoryEdit />', () => {
|
|||||||
expect(wrapper.find('ContentError').length).toBe(1);
|
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 () => {
|
test('unsuccessful form submission should show an error message', async () => {
|
||||||
const error = {
|
const error = {
|
||||||
response: {
|
response: {
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Formik, useField, useFormikContext } from 'formik';
|
import { Formik, useField, useFormikContext } from 'formik';
|
||||||
import { func, shape } from 'prop-types';
|
import { func, shape } from 'prop-types';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { ConstructedInventoriesAPI } from 'api';
|
|
||||||
import { minMaxValue, required } from 'util/validators';
|
import { minMaxValue, required } from 'util/validators';
|
||||||
import useRequest from 'hooks/useRequest';
|
|
||||||
import { Form, FormGroup } from '@patternfly/react-core';
|
import { Form, FormGroup } from '@patternfly/react-core';
|
||||||
import { VariablesField } from 'components/CodeEditor';
|
import { VariablesField } from 'components/CodeEditor';
|
||||||
import ContentError from 'components/ContentError';
|
|
||||||
import ContentLoading from 'components/ContentLoading';
|
|
||||||
import FormActionGroup from 'components/FormActionGroup/FormActionGroup';
|
import FormActionGroup from 'components/FormActionGroup/FormActionGroup';
|
||||||
import FormField, { FormSubmitError } from 'components/FormField';
|
import FormField, { FormSubmitError } from 'components/FormField';
|
||||||
import { FormFullWidthLayout, FormColumnLayout } from 'components/FormLayout';
|
import { FormFullWidthLayout, FormColumnLayout } from 'components/FormLayout';
|
||||||
@@ -165,6 +161,7 @@ function ConstructedInventoryForm({
|
|||||||
onCancel,
|
onCancel,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
submitError,
|
submitError,
|
||||||
|
options,
|
||||||
}) {
|
}) {
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
kind: 'constructed',
|
kind: 'constructed',
|
||||||
@@ -179,32 +176,6 @@ function ConstructedInventoryForm({
|
|||||||
source_vars: constructedInventory?.source_vars || '---',
|
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 (
|
return (
|
||||||
<Formik initialValues={initialValues} onSubmit={onSubmit}>
|
<Formik initialValues={initialValues} onSubmit={onSubmit}>
|
||||||
{(formik) => (
|
{(formik) => (
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { ConstructedInventoriesAPI } from 'api';
|
|
||||||
import {
|
import {
|
||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
@@ -19,38 +18,35 @@ const mockFormValues = {
|
|||||||
inputInventories: [{ id: 100, name: 'East' }],
|
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 />', () => {
|
describe('<ConstructedInventoryForm />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
const onSubmit = jest.fn();
|
const onSubmit = jest.fn();
|
||||||
|
|
||||||
beforeEach(async () => {
|
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 () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<ConstructedInventoryForm onCancel={() => {}} onSubmit={onSubmit} />
|
<ConstructedInventoryForm
|
||||||
|
onCancel={() => {}}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -104,20 +100,4 @@ describe('<ConstructedInventoryForm />', () => {
|
|||||||
'The plugin parameter is required.'
|
'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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
# NOTE: you will not be able to build within the docker-compose development environment if you use this option
|
||||||
UI_NEXT_LOCAL ?=
|
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_REPO ?= https://github.com/ansible/ansible-ui.git
|
||||||
UI_NEXT_GIT_BRANCH ?= main
|
UI_NEXT_GIT_BRANCH ?= main
|
||||||
|
|
||||||
|
## Product name to display on the UI used in UI_NEXT build process
|
||||||
|
PRODUCT ?= AWX
|
||||||
|
|
||||||
.PHONY: ui-next
|
.PHONY: ui-next
|
||||||
## Default build target of ui-next Makefile, builds ui-next/build
|
## Default build target of ui-next Makefile, builds ui-next/build
|
||||||
ui-next: 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.
|
## 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
|
$(UI_NEXT_DIR)/src/build/awx: $(UI_NEXT_DIR)/src $(UI_NEXT_DIR)/src/node_modules/webpack
|
||||||
@echo "=== Building ui_next ==="
|
@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
|
.PHONY: ui-next/src
|
||||||
## Clone or link src of UI_NEXT to ui-next/src, will re-clone/link/update if necessary.
|
## Clone or link src of UI_NEXT to ui-next/src, will re-clone/link/update if necessary.
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ class LookupModule(LookupBase):
|
|||||||
if not isinstance(rule[field_name], list):
|
if not isinstance(rule[field_name], list):
|
||||||
rule[field_name] = rule[field_name].split(',')
|
rule[field_name] = rule[field_name].split(',')
|
||||||
for value in rule[field_name]:
|
for value in rule[field_name]:
|
||||||
value = value.strip()
|
value = value.strip().lower()
|
||||||
if value not in valid_list:
|
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())))
|
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])
|
return_values.append(valid_list[value])
|
||||||
|
|||||||
@@ -356,3 +356,19 @@
|
|||||||
that:
|
that:
|
||||||
- results is success
|
- results is success
|
||||||
- "'DTSTART;TZID=UTC:20220430T103045 RRULE:FREQ=MONTHLY;BYMONTHDAY=12,13,14,15,16,17,18;BYDAY=SA;INTERVAL=1' == complex_rule"
|
- "'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"
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ class ApiV2(base.Base):
|
|||||||
export_key = 'create_approval_template'
|
export_key = 'create_approval_template'
|
||||||
rel_option_endpoint = _page.related.get('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:
|
if rel_post_fields is None:
|
||||||
log.debug("%s is a read-only endpoint.", rel_endpoint)
|
log.debug("%s is a read-only endpoint.", rel_endpoint)
|
||||||
continue
|
continue
|
||||||
@@ -202,7 +202,7 @@ class ApiV2(base.Base):
|
|||||||
return utils.remove_encrypted(fields)
|
return utils.remove_encrypted(fields)
|
||||||
|
|
||||||
def _export_list(self, endpoint):
|
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:
|
if post_fields is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -267,7 +267,7 @@ class ApiV2(base.Base):
|
|||||||
|
|
||||||
def _import_list(self, endpoint, assets):
|
def _import_list(self, endpoint, assets):
|
||||||
log.debug("_import_list -- endpoint: %s, assets: %s", endpoint.endpoint, repr(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
|
changed = False
|
||||||
|
|
||||||
|
|||||||
@@ -495,7 +495,6 @@ class TentativePage(str):
|
|||||||
class PageCache(object):
|
class PageCache(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.options = {}
|
self.options = {}
|
||||||
self.post_fields = {}
|
|
||||||
self.pages_by_url = {}
|
self.pages_by_url = {}
|
||||||
self.pages_by_natural_key = {}
|
self.pages_by_natural_key = {}
|
||||||
|
|
||||||
@@ -517,29 +516,6 @@ class PageCache(object):
|
|||||||
|
|
||||||
return self.options.setdefault(url, options)
|
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):
|
def set_page(self, page):
|
||||||
log.debug("set_page: %s %s", type(page), page.endpoint)
|
log.debug("set_page: %s %s", type(page), page.endpoint)
|
||||||
self.pages_by_url[page.endpoint] = page
|
self.pages_by_url[page.endpoint] = page
|
||||||
|
|||||||
@@ -31,3 +31,18 @@ def remove_encrypted(value):
|
|||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
return {k: remove_encrypted(v) for k, v in value.items()}
|
return {k: remove_encrypted(v) for k, v in value.items()}
|
||||||
return value
|
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'])
|
||||||
|
|||||||
@@ -137,12 +137,12 @@ To retrieve your admin password
|
|||||||
|
|
||||||
To tail logs from the task containers
|
To tail logs from the task containers
|
||||||
```bash
|
```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
|
To tail logs from the web containers
|
||||||
```bash
|
```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.
|
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.
|
||||||
|
|||||||
@@ -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)
|
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-daemon>3.0.0
|
||||||
python-dsv-sdk
|
python-dsv-sdk
|
||||||
python-tss-sdk==1.0.0
|
python-tss-sdk==1.2.1
|
||||||
python-ldap
|
python-ldap
|
||||||
pyyaml>=6.0.1
|
pyyaml>=6.0.1
|
||||||
receptorctl==1.3.0
|
receptorctl==1.3.0
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ python-ldap==3.4.3
|
|||||||
# django-auth-ldap
|
# django-auth-ldap
|
||||||
python-string-utils==1.0.0
|
python-string-utils==1.0.0
|
||||||
# via openshift
|
# via openshift
|
||||||
python-tss-sdk==1.0.0
|
python-tss-sdk==1.2.1
|
||||||
# via -r /awx_devel/requirements/requirements.in
|
# via -r /awx_devel/requirements/requirements.in
|
||||||
python3-openid==3.2.0
|
python3-openid==3.2.0
|
||||||
# via social-auth-core
|
# via social-auth-core
|
||||||
|
|||||||
Reference in New Issue
Block a user