diff options
-rw-r--r-- | awx/api/serializers.py | 16 | ||||
-rw-r--r-- | awx/api/views/bulk.py | 37 | ||||
-rw-r--r-- | awxkit/awxkit/api/pages/__init__.py | 1 | ||||
-rw-r--r-- | awxkit/awxkit/api/pages/bulk.py | 12 | ||||
-rw-r--r-- | awxkit/awxkit/api/resources.py | 1 | ||||
-rw-r--r-- | awxkit/awxkit/cli/custom.py | 48 | ||||
-rw-r--r-- | awxkit/awxkit/cli/options.py | 33 |
7 files changed, 113 insertions, 35 deletions
diff --git a/awx/api/serializers.py b/awx/api/serializers.py index a6c7c09e68..e41a180f84 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -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 = { diff --git a/awx/api/views/bulk.py b/awx/api/views/bulk.py index e4dd1e62c2..2dd6f58ae3 100644 --- a/awx/api/views/bulk.py +++ b/awx/api/views/bulk.py @@ -16,9 +16,25 @@ from awx.api import ( ) +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): - _ignore_model_permissions = True 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'] diff --git a/awxkit/awxkit/api/pages/__init__.py b/awxkit/awxkit/api/pages/__init__.py index 628e5e186d..f5587fc211 100644 --- a/awxkit/awxkit/api/pages/__init__.py +++ b/awxkit/awxkit/api/pages/__init__.py @@ -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 diff --git a/awxkit/awxkit/api/pages/bulk.py b/awxkit/awxkit/api/pages/bulk.py new file mode 100644 index 0000000000..197b5c2585 --- /dev/null +++ b/awxkit/awxkit/api/pages/bulk.py @@ -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) diff --git a/awxkit/awxkit/api/resources.py b/awxkit/awxkit/api/resources.py index 5874b3d0de..31bf6c5829 100644 --- a/awxkit/awxkit/api/resources.py +++ b/awxkit/awxkit/api/resources.py @@ -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+/' diff --git a/awxkit/awxkit/cli/custom.py b/awxkit/awxkit/cli/custom.py index f1453562dd..a2764b15ba 100644 --- a/awxkit/awxkit/cli/custom.py +++ b/awxkit/awxkit/cli/custom.py @@ -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' diff --git a/awxkit/awxkit/cli/options.py b/awxkit/awxkit/cli/options.py index 7519ca90a8..fac14206fd 100644 --- a/awxkit/awxkit/cli/options.py +++ b/awxkit/awxkit/cli/options.py @@ -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') |