awxkit cli support

fixes for awx cli
This commit is contained in:
Seth Foster 2023-01-26 01:45:42 -05:00 committed by Elijah DeLee
parent 861ba8a727
commit 34834252ff
7 changed files with 114 additions and 36 deletions

View File

@ -1971,7 +1971,8 @@ class BulkHostCreateSerializer(serializers.Serializer):
inventory = serializers.PrimaryKeyRelatedField(
queryset=Inventory.objects.all(), required=True, write_only=True, help_text=_('Primary Key ID of inventory to add hosts to.')
)
hosts = serializers.ListField(child=BulkHostSerializer(), allow_empty=False, max_length=1000, write_only=True, help_text=_('Hosts to be created.'))
hosts_help_text = _('List of hosts to be created, JSON. e.g. [{"name": "example.com"}, {"name": "127.0.0.1"}]')
hosts = serializers.ListField(child=BulkHostSerializer(), allow_empty=False, max_length=1000, write_only=True, help_text=hosts_help_text)
class Meta:
fields = ('inventory', 'hosts')
@ -4582,8 +4583,9 @@ class BulkJobNodeSerializer(serializers.Serializer):
class BulkJobLaunchSerializer(BaseSerializer):
name = serializers.CharField(max_length=512, write_only=True, required=False) # limited by max name of jobs
jobs = BulkJobNodeSerializer(many=True, allow_empty=False, write_only=True, max_length=1000)
name = serializers.CharField(default='Bulk Job Launch', max_length=512, write_only=True, required=False, allow_blank=True) # limited by max name of jobs
job_node_help_text = _('List of jobs to be launched, JSON. e.g. [{"unified_job_template": 7}, {"unified_job_template": 10}]')
jobs = BulkJobNodeSerializer(many=True, allow_empty=False, write_only=True, max_length=1000, help_text=job_node_help_text)
description = serializers.CharField(write_only=True, required=False, allow_blank=False)
extra_vars = serializers.CharField(write_only=True, required=False, allow_blank=False)
organization = serializers.PrimaryKeyRelatedField(
@ -4673,11 +4675,11 @@ class BulkJobLaunchSerializer(BaseSerializer):
job_node_data = validated_data.pop('jobs')
# FIXME: Need to set organization on the WorkflowJob in order for users to be able to see it --
# normally their permission is sourced from the underlying WorkflowJobTemplate
# maybe we need to add Organization to WorkflowJob
if 'name' not in validated_data:
validated_data['name'] = 'Bulk Job Launch'
# maybe we need to add Organization to WorkflowJobd
wfj_limit = validated_data.pop('limit', None)
wfj = WorkflowJob.objects.create(**validated_data, is_bulk_job=True)
if wfj_limit:
wfj.limit = wfj_limit
nodes = []
node_m2m_objects = {}
node_m2m_object_types_to_through_model = {

View File

@ -16,9 +16,25 @@ from awx.api import (
)
class BulkJobLaunchView(GenericAPIView):
_ignore_model_permissions = True
class BulkView(APIView):
permission_classes = [IsAuthenticated]
renderer_classes = [
renderers.BrowsableAPIRenderer,
JSONRenderer,
]
allowed_methods = ['GET', 'OPTIONS']
def get(self, request, format=None):
'''List top level resources'''
data = OrderedDict()
data['host_create'] = reverse('api:bulk_host_create', request=request)
data['job_launch'] = reverse('api:bulk_job_launch', request=request)
return Response(data)
class BulkJobLaunchView(GenericAPIView):
permission_classes = [IsAuthenticated]
model = UnifiedJob
serializer_class = serializers.BulkJobLaunchSerializer
allowed_methods = ['GET', 'POST', 'OPTIONS']
@ -35,26 +51,9 @@ class BulkJobLaunchView(GenericAPIView):
return Response(bulkjob_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class BulkView(APIView):
_ignore_model_permissions = True
permission_classes = [IsAuthenticated]
renderer_classes = [
renderers.BrowsableAPIRenderer,
JSONRenderer,
]
allowed_methods = ['GET', 'OPTIONS']
def get(self, request, format=None):
'''List top level resources'''
data = OrderedDict()
data['bulk_host_create'] = reverse('api:bulk_host_create', request=request)
data['bulk_job_launch'] = reverse('api:bulk_job_launch', request=request)
return Response(data)
class BulkHostCreateView(GenericAPIView):
_ignore_model_permissions = True
permission_classes = [IsAuthenticated]
model = Host
serializer_class = serializers.BulkHostCreateSerializer
allowed_methods = ['GET', 'POST', 'OPTIONS']

View File

@ -1,6 +1,7 @@
# Order matters
from .page import * # NOQA
from .base import * # NOQA
from .bulk import * # NOQA
from .access_list import * # NOQA
from .api import * # NOQA
from .authtoken import * # NOQA

View File

@ -0,0 +1,12 @@
from awxkit.api.resources import resources
from . import base
from . import page
class Bulk(base.Base):
def get(self, **query_parameters):
request = self.connection.get(self.endpoint, query_parameters, headers={'Accept': 'application/json'})
return self.page_identity(request)
page.register_page([resources.bulk, (resources.bulk, 'get')], Bulk)

View File

@ -13,6 +13,7 @@ class Resources(object):
_applications = 'applications/'
_auth = 'auth/'
_authtoken = 'authtoken/'
_bulk = 'bulk/'
_config = 'config/'
_config_attach = 'config/attach/'
_credential = r'credentials/\d+/'

View File

@ -44,6 +44,10 @@ class CustomAction(metaclass=CustomActionRegistryMeta):
class Launchable(object):
@property
def options_endpoint(self):
return self.page.endpoint + '1/{}/'.format(self.action)
def add_arguments(self, parser, resource_options_parser, with_pk=True):
from .options import pk_or_name
@ -53,7 +57,7 @@ class Launchable(object):
parser.choices[self.action].add_argument('--action-timeout', type=int, help='If set with --monitor or --wait, time out waiting on job completion.')
parser.choices[self.action].add_argument('--wait', action='store_true', help='If set, waits until the launched job finishes.')
launch_time_options = self.page.connection.options(self.page.endpoint + '1/{}/'.format(self.action))
launch_time_options = self.page.connection.options(self.options_endpoint)
if launch_time_options.ok:
launch_time_options = launch_time_options.json()['actions']['POST']
resource_options_parser.options['LAUNCH'] = launch_time_options
@ -90,6 +94,48 @@ class JobTemplateLaunch(Launchable, CustomAction):
resource = 'job_templates'
class BulkJobLaunch(Launchable, CustomAction):
action = 'job_launch'
resource = 'bulk'
@property
def options_endpoint(self):
return self.page.endpoint + '{}/'.format(self.action)
def add_arguments(self, parser, resource_options_parser):
Launchable.add_arguments(self, parser, resource_options_parser, with_pk=False)
def perform(self, **kwargs):
monitor_kwargs = {
'monitor': kwargs.pop('monitor', False),
'wait': kwargs.pop('wait', False),
'action_timeout': kwargs.pop('action_timeout', False),
}
response = self.page.get().job_launch.post(kwargs)
self.monitor(response, **monitor_kwargs)
return response
class BulkHostCreate(CustomAction):
action = 'host_create'
resource = 'bulk'
@property
def options_endpoint(self):
return self.page.endpoint + '{}/'.format(self.action)
def add_arguments(self, parser, resource_options_parser):
options = self.page.connection.options(self.options_endpoint)
if options.ok:
options = options.json()['actions']['POST']
resource_options_parser.options['HOSTCREATEPOST'] = options
resource_options_parser.build_query_arguments(self.action, 'HOSTCREATEPOST')
def perform(self, **kwargs):
response = self.page.get().host_create.post(kwargs)
return response
class ProjectUpdate(Launchable, CustomAction):
action = 'update'
resource = 'projects'

View File

@ -163,7 +163,10 @@ class ResourceOptionsParser(object):
if method == 'list' and param.get('filterable') is False:
continue
def json_or_yaml(v):
def list_of_json_or_yaml(v):
return json_or_yaml(v, expected_type=list)
def json_or_yaml(v, expected_type=dict):
if v.startswith('@'):
v = open(os.path.expanduser(v[1:])).read()
try:
@ -174,15 +177,16 @@ class ResourceOptionsParser(object):
except Exception:
raise argparse.ArgumentTypeError("{} is not valid JSON or YAML".format(v))
if not isinstance(parsed, dict):
if not isinstance(parsed, expected_type):
raise argparse.ArgumentTypeError("{} is not valid JSON or YAML".format(v))
for k, v in parsed.items():
# add support for file reading at top-level JSON keys
# (to make things like SSH key data easier to work with)
if isinstance(v, str) and v.startswith('@'):
path = os.path.expanduser(v[1:])
parsed[k] = open(path).read()
if expected_type is dict:
for k, v in parsed.items():
# add support for file reading at top-level JSON keys
# (to make things like SSH key data easier to work with)
if isinstance(v, str) and v.startswith('@'):
path = os.path.expanduser(v[1:])
parsed[k] = open(path).read()
return parsed
@ -258,6 +262,19 @@ class ResourceOptionsParser(object):
if k == 'extra_vars':
args.append('-e')
# special handling for bulk endpoints
if self.resource == 'bulk':
if method == "host_create":
if k == "inventory":
kwargs['required'] = required = True
if k == 'hosts':
kwargs['type'] = list_of_json_or_yaml
kwargs['required'] = required = True
if method == "job_launch":
if k == 'jobs':
kwargs['type'] = list_of_json_or_yaml
kwargs['required'] = required = True
if required:
if required_group is None:
required_group = self.parser.choices[method].add_argument_group('required arguments')