mirror of
https://github.com/ansible/awx.git
synced 2026-05-17 14:27:42 -02:30
Add OpenShift Virtualization Inventory source option (#15047)
Co-authored-by: Hao Liu <44379968+TheRealHaoLiu@users.noreply.github.com>
This commit is contained in:
@@ -14,7 +14,7 @@ __all__ = [
|
|||||||
'STANDARD_INVENTORY_UPDATE_ENV',
|
'STANDARD_INVENTORY_UPDATE_ENV',
|
||||||
]
|
]
|
||||||
|
|
||||||
CLOUD_PROVIDERS = ('azure_rm', 'ec2', 'gce', 'vmware', 'openstack', 'rhv', 'satellite6', 'controller', 'insights', 'terraform')
|
CLOUD_PROVIDERS = ('azure_rm', 'ec2', 'gce', 'vmware', 'openstack', 'rhv', 'satellite6', 'controller', 'insights', 'terraform', 'openshift_virtualization')
|
||||||
PRIVILEGE_ESCALATION_METHODS = [
|
PRIVILEGE_ESCALATION_METHODS = [
|
||||||
('sudo', _('Sudo')),
|
('sudo', _('Sudo')),
|
||||||
('su', _('Su')),
|
('su', _('Su')),
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Generated by Django 4.2.10 on 2024-06-12 19:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0193_alter_notification_notification_type_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inventorysource',
|
||||||
|
name='source',
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
('file', 'File, Directory or Script'),
|
||||||
|
('constructed', 'Template additional groups and hostvars at runtime'),
|
||||||
|
('scm', 'Sourced from a Project'),
|
||||||
|
('ec2', 'Amazon EC2'),
|
||||||
|
('gce', 'Google Compute Engine'),
|
||||||
|
('azure_rm', 'Microsoft Azure Resource Manager'),
|
||||||
|
('vmware', 'VMware vCenter'),
|
||||||
|
('satellite6', 'Red Hat Satellite 6'),
|
||||||
|
('openstack', 'OpenStack'),
|
||||||
|
('rhv', 'Red Hat Virtualization'),
|
||||||
|
('controller', 'Red Hat Ansible Automation Platform'),
|
||||||
|
('insights', 'Red Hat Insights'),
|
||||||
|
('terraform', 'Terraform State'),
|
||||||
|
('openshift_virtualization', 'OpenShift Virtualization'),
|
||||||
|
],
|
||||||
|
default=None,
|
||||||
|
max_length=32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inventoryupdate',
|
||||||
|
name='source',
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
('file', 'File, Directory or Script'),
|
||||||
|
('constructed', 'Template additional groups and hostvars at runtime'),
|
||||||
|
('scm', 'Sourced from a Project'),
|
||||||
|
('ec2', 'Amazon EC2'),
|
||||||
|
('gce', 'Google Compute Engine'),
|
||||||
|
('azure_rm', 'Microsoft Azure Resource Manager'),
|
||||||
|
('vmware', 'VMware vCenter'),
|
||||||
|
('satellite6', 'Red Hat Satellite 6'),
|
||||||
|
('openstack', 'OpenStack'),
|
||||||
|
('rhv', 'Red Hat Virtualization'),
|
||||||
|
('controller', 'Red Hat Ansible Automation Platform'),
|
||||||
|
('insights', 'Red Hat Insights'),
|
||||||
|
('terraform', 'Terraform State'),
|
||||||
|
('openshift_virtualization', 'OpenShift Virtualization'),
|
||||||
|
],
|
||||||
|
default=None,
|
||||||
|
max_length=32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -933,6 +933,7 @@ class InventorySourceOptions(BaseModel):
|
|||||||
('controller', _('Red Hat Ansible Automation Platform')),
|
('controller', _('Red Hat Ansible Automation Platform')),
|
||||||
('insights', _('Red Hat Insights')),
|
('insights', _('Red Hat Insights')),
|
||||||
('terraform', _('Terraform State')),
|
('terraform', _('Terraform State')),
|
||||||
|
('openshift_virtualization', _('OpenShift Virtualization')),
|
||||||
]
|
]
|
||||||
|
|
||||||
# From the options of the Django management base command
|
# From the options of the Django management base command
|
||||||
@@ -1042,7 +1043,7 @@ class InventorySourceOptions(BaseModel):
|
|||||||
def cloud_credential_validation(source, cred):
|
def cloud_credential_validation(source, cred):
|
||||||
if not source:
|
if not source:
|
||||||
return None
|
return None
|
||||||
if cred and source not in ('custom', 'scm'):
|
if cred and source not in ('custom', 'scm', 'openshift_virtualization'):
|
||||||
# If a credential was provided, it's important that it matches
|
# If a credential was provided, it's important that it matches
|
||||||
# the actual inventory source being used (Amazon requires Amazon
|
# the actual inventory source being used (Amazon requires Amazon
|
||||||
# credentials; Rackspace requires Rackspace credentials; etc...)
|
# credentials; Rackspace requires Rackspace credentials; etc...)
|
||||||
@@ -1051,12 +1052,14 @@ class InventorySourceOptions(BaseModel):
|
|||||||
# Allow an EC2 source to omit the credential. If Tower is running on
|
# Allow an EC2 source to omit the credential. If Tower is running on
|
||||||
# an EC2 instance with an IAM Role assigned, boto will use credentials
|
# an EC2 instance with an IAM Role assigned, boto will use credentials
|
||||||
# from the instance metadata instead of those explicitly provided.
|
# from the instance metadata instead of those explicitly provided.
|
||||||
elif source in CLOUD_PROVIDERS and source != 'ec2':
|
elif source in CLOUD_PROVIDERS and source not in ['ec2', 'openshift_virtualization']:
|
||||||
return _('Credential is required for a cloud source.')
|
return _('Credential is required for a cloud source.')
|
||||||
elif source == 'custom' and cred and cred.credential_type.kind in ('scm', 'ssh', 'insights', 'vault'):
|
elif source == 'custom' and cred and cred.credential_type.kind in ('scm', 'ssh', 'insights', 'vault'):
|
||||||
return _('Credentials of type machine, source control, insights and vault are disallowed for custom inventory sources.')
|
return _('Credentials of type machine, source control, insights and vault are disallowed for custom inventory sources.')
|
||||||
elif source == 'scm' and cred and cred.credential_type.kind in ('insights', 'vault'):
|
elif source == 'scm' and cred and cred.credential_type.kind in ('insights', 'vault'):
|
||||||
return _('Credentials of type insights and vault are disallowed for scm inventory sources.')
|
return _('Credentials of type insights and vault are disallowed for scm inventory sources.')
|
||||||
|
elif source == 'openshift_virtualization' and cred and cred.credential_type.kind != 'kubernetes':
|
||||||
|
return _('Credentials of type kubernetes is requred for openshift_virtualization inventory sources.')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_cloud_credential(self):
|
def get_cloud_credential(self):
|
||||||
@@ -1693,6 +1696,16 @@ class insights(PluginFileInjector):
|
|||||||
use_fqcn = True
|
use_fqcn = True
|
||||||
|
|
||||||
|
|
||||||
|
class openshift_virtualization(PluginFileInjector):
|
||||||
|
plugin_name = 'kubevirt'
|
||||||
|
base_injector = 'template'
|
||||||
|
namespace = 'kubevirt'
|
||||||
|
collection = 'core'
|
||||||
|
downstream_namespace = 'redhat'
|
||||||
|
downstream_collection = 'openshift_virtualization'
|
||||||
|
use_fqcn = True
|
||||||
|
|
||||||
|
|
||||||
class constructed(PluginFileInjector):
|
class constructed(PluginFileInjector):
|
||||||
plugin_name = 'constructed'
|
plugin_name = 'constructed'
|
||||||
namespace = 'ansible'
|
namespace = 'ansible'
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"K8S_AUTH_HOST": "https://foo.invalid",
|
||||||
|
"K8S_AUTH_API_KEY": "fooo",
|
||||||
|
"K8S_AUTH_VERIFY_SSL": "False"
|
||||||
|
}
|
||||||
@@ -46,6 +46,8 @@ def generate_fake_var(element):
|
|||||||
|
|
||||||
def credential_kind(source):
|
def credential_kind(source):
|
||||||
"""Given the inventory source kind, return expected credential kind"""
|
"""Given the inventory source kind, return expected credential kind"""
|
||||||
|
if source == 'openshift_virtualization':
|
||||||
|
return 'kubernetes_bearer_token'
|
||||||
return source.replace('ec2', 'aws')
|
return source.replace('ec2', 'aws')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -783,6 +783,11 @@ INSIGHTS_EXCLUDE_EMPTY_GROUPS = False
|
|||||||
TERRAFORM_INSTANCE_ID_VAR = 'id'
|
TERRAFORM_INSTANCE_ID_VAR = 'id'
|
||||||
TERRAFORM_EXCLUDE_EMPTY_GROUPS = True
|
TERRAFORM_EXCLUDE_EMPTY_GROUPS = True
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# OpenShift Virtualization
|
||||||
|
# ------------------------
|
||||||
|
OPENSHIFT_VIRTUALIZATION_EXCLUDE_EMPTY_GROUPS = True
|
||||||
|
|
||||||
# ---------------------
|
# ---------------------
|
||||||
# ----- Custom -----
|
# ----- Custom -----
|
||||||
# ---------------------
|
# ---------------------
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ describe('<InventorySourceAdd />', () => {
|
|||||||
['satellite6', 'Red Hat Satellite 6'],
|
['satellite6', 'Red Hat Satellite 6'],
|
||||||
['openstack', 'OpenStack'],
|
['openstack', 'OpenStack'],
|
||||||
['rhv', 'Red Hat Virtualization'],
|
['rhv', 'Red Hat Virtualization'],
|
||||||
|
[
|
||||||
|
'openshift_virtualization',
|
||||||
|
'Red Hat OpenShift Virtualization',
|
||||||
|
],
|
||||||
['controller', 'Red Hat Ansible Automation Platform'],
|
['controller', 'Red Hat Ansible Automation Platform'],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ const ansibleDocUrls = {
|
|||||||
'https://docs.ansible.com/ansible/latest/collections/ansible/builtin/constructed_inventory.html',
|
'https://docs.ansible.com/ansible/latest/collections/ansible/builtin/constructed_inventory.html',
|
||||||
terraform:
|
terraform:
|
||||||
'https://github.com/ansible-collections/cloud.terraform/blob/main/docs/cloud.terraform.terraform_state_inventory.rst',
|
'https://github.com/ansible-collections/cloud.terraform/blob/main/docs/cloud.terraform.terraform_state_inventory.rst',
|
||||||
|
openshift_virtualization:
|
||||||
|
'https://kubevirt.io/kubevirt.core/latest/plugins/kubevirt.html',
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInventoryHelpTextStrings = () => ({
|
const getInventoryHelpTextStrings = () => ({
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
TerraformSubForm,
|
TerraformSubForm,
|
||||||
VMwareSubForm,
|
VMwareSubForm,
|
||||||
VirtualizationSubForm,
|
VirtualizationSubForm,
|
||||||
|
OpenShiftVirtualizationSubForm,
|
||||||
} from './InventorySourceSubForms';
|
} from './InventorySourceSubForms';
|
||||||
|
|
||||||
const buildSourceChoiceOptions = (options) => {
|
const buildSourceChoiceOptions = (options) => {
|
||||||
@@ -231,6 +232,15 @@ const InventorySourceFormFields = ({
|
|||||||
sourceOptions={sourceOptions}
|
sourceOptions={sourceOptions}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
openshift_virtualization: (
|
||||||
|
<OpenShiftVirtualizationSubForm
|
||||||
|
autoPopulateCredential={
|
||||||
|
!source?.id ||
|
||||||
|
source?.source !== 'openshift_virtualization'
|
||||||
|
}
|
||||||
|
sourceOptions={sourceOptions}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}[sourceField.value]
|
}[sourceField.value]
|
||||||
}
|
}
|
||||||
</FormColumnLayout>
|
</FormColumnLayout>
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useField, useFormikContext } from 'formik';
|
||||||
|
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { useConfig } from 'contexts/Config';
|
||||||
|
import getDocsBaseUrl from 'util/getDocsBaseUrl';
|
||||||
|
import CredentialLookup from 'components/Lookup/CredentialLookup';
|
||||||
|
import { required } from 'util/validators';
|
||||||
|
import {
|
||||||
|
OptionsField,
|
||||||
|
VerbosityField,
|
||||||
|
EnabledVarField,
|
||||||
|
EnabledValueField,
|
||||||
|
HostFilterField,
|
||||||
|
SourceVarsField,
|
||||||
|
} from './SharedFields';
|
||||||
|
import getHelpText from '../Inventory.helptext';
|
||||||
|
|
||||||
|
const OpenShiftVirtualizationSubForm = ({ autoPopulateCredential }) => {
|
||||||
|
const helpText = getHelpText();
|
||||||
|
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||||
|
const [credentialField, credentialMeta, credentialHelpers] =
|
||||||
|
useField('credential');
|
||||||
|
const config = useConfig();
|
||||||
|
|
||||||
|
const handleCredentialUpdate = useCallback(
|
||||||
|
(value) => {
|
||||||
|
setFieldValue('credential', value);
|
||||||
|
setFieldTouched('credential', true, false);
|
||||||
|
},
|
||||||
|
[setFieldValue, setFieldTouched]
|
||||||
|
);
|
||||||
|
|
||||||
|
const docsBaseUrl = getDocsBaseUrl(config);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CredentialLookup
|
||||||
|
credentialTypeNamespace="kubernetes_bearer_token"
|
||||||
|
label={t`Credential`}
|
||||||
|
helperTextInvalid={credentialMeta.error}
|
||||||
|
isValid={!credentialMeta.touched || !credentialMeta.error}
|
||||||
|
onBlur={() => credentialHelpers.setTouched()}
|
||||||
|
onChange={handleCredentialUpdate}
|
||||||
|
value={credentialField.value}
|
||||||
|
required
|
||||||
|
autoPopulate={autoPopulateCredential}
|
||||||
|
validate={required(t`Select a value for this field`)}
|
||||||
|
/>
|
||||||
|
<VerbosityField />
|
||||||
|
<HostFilterField />
|
||||||
|
<EnabledVarField />
|
||||||
|
<EnabledValueField />
|
||||||
|
<OptionsField />
|
||||||
|
<SourceVarsField
|
||||||
|
popoverContent={helpText.sourceVars(
|
||||||
|
docsBaseUrl,
|
||||||
|
'openshift_virtualization'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OpenShiftVirtualizationSubForm;
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
import { CredentialsAPI } from 'api';
|
||||||
|
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
||||||
|
import VirtualizationSubForm from './VirtualizationSubForm';
|
||||||
|
|
||||||
|
jest.mock('../../../../api');
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
credential: null,
|
||||||
|
overwrite: false,
|
||||||
|
overwrite_vars: false,
|
||||||
|
source_path: '',
|
||||||
|
source_project: null,
|
||||||
|
source_script: null,
|
||||||
|
source_vars: '---\n',
|
||||||
|
update_cache_timeout: 0,
|
||||||
|
update_on_launch: true,
|
||||||
|
verbosity: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<VirtualizationSubForm />', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
CredentialsAPI.read.mockResolvedValue({
|
||||||
|
data: { count: 0, results: [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik initialValues={initialValues}>
|
||||||
|
<VirtualizationSubForm />
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render subform fields', () => {
|
||||||
|
expect(wrapper.find('FormGroup[label="Credential"]')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('FormGroup[label="Verbosity"]')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('FormGroup[label="Update options"]')).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('FormGroup[label="Cache timeout (seconds)"]')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('VariablesField[label="Source variables"]')
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should make expected api calls', () => {
|
||||||
|
expect(CredentialsAPI.read).toHaveBeenCalledTimes(1);
|
||||||
|
expect(CredentialsAPI.read).toHaveBeenCalledWith({
|
||||||
|
credential_type__namespace: 'rhv',
|
||||||
|
order_by: 'name',
|
||||||
|
page: 1,
|
||||||
|
page_size: 5,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,3 +9,4 @@ export { default as ControllerSubForm } from './ControllerSubForm';
|
|||||||
export { default as TerraformSubForm } from './TerraformSubForm';
|
export { default as TerraformSubForm } from './TerraformSubForm';
|
||||||
export { default as VMwareSubForm } from './VMwareSubForm';
|
export { default as VMwareSubForm } from './VMwareSubForm';
|
||||||
export { default as VirtualizationSubForm } from './VirtualizationSubForm';
|
export { default as VirtualizationSubForm } from './VirtualizationSubForm';
|
||||||
|
export { default as OpenShiftVirtualizationSubForm } from './OpenShiftVirtualizationSubForm';
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ options:
|
|||||||
source:
|
source:
|
||||||
description:
|
description:
|
||||||
- The source to use for this group.
|
- The source to use for this group.
|
||||||
choices: [ "scm", "ec2", "gce", "azure_rm", "vmware", "satellite6", "openstack", "rhv", "controller", "insights", "terraform" ]
|
choices: [ "scm", "ec2", "gce", "azure_rm", "vmware", "satellite6", "openstack", "rhv", "controller", "insights", "terraform",
|
||||||
|
"openshift_virtualization" ]
|
||||||
type: str
|
type: str
|
||||||
source_path:
|
source_path:
|
||||||
description:
|
description:
|
||||||
@@ -170,7 +171,22 @@ def main():
|
|||||||
#
|
#
|
||||||
# How do we handle manual and file? The controller does not seem to be able to activate them
|
# How do we handle manual and file? The controller does not seem to be able to activate them
|
||||||
#
|
#
|
||||||
source=dict(choices=["scm", "ec2", "gce", "azure_rm", "vmware", "satellite6", "openstack", "rhv", "controller", "insights", "terraform"]),
|
source=dict(
|
||||||
|
choices=[
|
||||||
|
"scm",
|
||||||
|
"ec2",
|
||||||
|
"gce",
|
||||||
|
"azure_rm",
|
||||||
|
"vmware",
|
||||||
|
"satellite6",
|
||||||
|
"openstack",
|
||||||
|
"rhv",
|
||||||
|
"controller",
|
||||||
|
"insights",
|
||||||
|
"terraform",
|
||||||
|
"openshift_virtualization",
|
||||||
|
]
|
||||||
|
),
|
||||||
source_path=dict(),
|
source_path=dict(),
|
||||||
source_vars=dict(type='dict'),
|
source_vars=dict(type='dict'),
|
||||||
enabled_var=dict(),
|
enabled_var=dict(),
|
||||||
|
|||||||
Reference in New Issue
Block a user