From e92f1187d23f19685cb55b3110d948ac4e367a2e Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 2 Mar 2021 10:27:49 -0500 Subject: [PATCH 001/178] Use credential_type for prompted multicred select categories --- .../src/components/LaunchPrompt/steps/CredentialsStep.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/CredentialsStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/CredentialsStep.jsx index 1b736c6ad0..a3659d43f7 100644 --- a/awx/ui_next/src/components/LaunchPrompt/steps/CredentialsStep.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/steps/CredentialsStep.jsx @@ -167,9 +167,10 @@ function CredentialsStep({ i18n }) { const hasSameVaultID = val => val?.inputs?.vault_id !== undefined && val?.inputs?.vault_id === item?.inputs?.vault_id; - const hasSameKind = val => val.kind === item.kind; + const hasSameCredentialType = val => + val.credential_type === item.credential_type; const newItems = field.value.filter(i => - isVault ? !hasSameVaultID(i) : !hasSameKind(i) + isVault ? !hasSameVaultID(i) : !hasSameCredentialType(i) ); newItems.push(item); helpers.setValue(newItems); From 4456ae2d71ae4aecde01cf1178725c67605aba06 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 2 Mar 2021 10:40:58 -0500 Subject: [PATCH 002/178] if rsyslogd cannot be reached, note the failure in sys.stderr see: https://github.com/ansible/awx/issues/8505 --- awx/main/utils/external_logging.py | 2 +- awx/main/utils/handlers.py | 43 +++++++++++++++++------------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/awx/main/utils/external_logging.py b/awx/main/utils/external_logging.py index 7213f6f749..3c281719ff 100644 --- a/awx/main/utils/external_logging.py +++ b/awx/main/utils/external_logging.py @@ -32,7 +32,7 @@ def construct_rsyslog_conf_template(settings=settings): '$IncludeConfig /var/lib/awx/rsyslog/conf.d/*.conf', f'main_queue(queue.spoolDirectory="{spool_directory}" queue.maxdiskspace="{max_disk_space}g" queue.type="Disk" queue.filename="awx-external-logger-backlog")', # noqa 'module(load="imuxsock" SysSock.Use="off")', - 'input(type="imuxsock" Socket="' + settings.LOGGING['handlers']['external_logger']['address'] + '" unlink="on")', + 'input(type="imuxsock" Socket="' + settings.LOGGING['handlers']['external_logger']['address'] + '" unlink="on" RateLimit.Burst="0")', 'template(name="awx" type="string" string="%rawmsg-after-pri%")', ]) diff --git a/awx/main/utils/handlers.py b/awx/main/utils/handlers.py index f59ba17478..9ce1afabbe 100644 --- a/awx/main/utils/handlers.py +++ b/awx/main/utils/handlers.py @@ -3,7 +3,8 @@ # Python import logging -import os.path +import sys +import traceback # Django from django.conf import settings @@ -21,27 +22,31 @@ class RSysLogHandler(logging.handlers.SysLogHandler): super(RSysLogHandler, self)._connect_unixsocket(address) self.socket.setblocking(False) + def handleError(self, record): + # for any number of reasons, rsyslogd has gone to lunch; + # this usually means that it's just been restarted (due to + # a configuration change) unfortunately, we can't log that + # because...rsyslogd is down (and would just put us back down this + # code path) + # as a fallback, it makes the most sense to just write the + # messages to sys.stderr (which will end up in supervisord logs, + # and in containerized installs, cascaded down to pod logs) + # because the alternative is blocking the + # socket.send() in the Python process, which we definitely don't + # want to do) + msg = f'{record.asctime} ERROR rsyslogd was unresponsive: ' + exc = traceback.format_exc() + try: + msg += exc.splitlines()[-1] + except Exception: + msg += exc + msg = '\n'.join([msg, record.msg, '']) + sys.stderr.write(msg) + def emit(self, msg): if not settings.LOG_AGGREGATOR_ENABLED: return - if not os.path.exists(settings.LOGGING['handlers']['external_logger']['address']): - return - try: - return super(RSysLogHandler, self).emit(msg) - except ConnectionRefusedError: - # rsyslogd has gone to lunch; this generally means that it's just - # been restarted (due to a configuration change) - # unfortunately, we can't log that because...rsyslogd is down (and - # would just us back ddown this code path) - pass - except BlockingIOError: - # for , rsyslogd is no longer reading from the domain socket, and - # we're unable to write any more to it without blocking (we've seen this behavior - # from time to time when logging is totally misconfigured; - # in this scenario, it also makes more sense to just drop the messages, - # because the alternative is blocking the socket.send() in the - # Python process, which we definitely don't want to do) - pass + return super(RSysLogHandler, self).emit(msg) class SpecialInventoryHandler(logging.Handler): From efe03c396b71a0ce4cf0e80d0476b53120da2a63 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 3 Mar 2021 16:12:43 -0500 Subject: [PATCH 003/178] add some new fancy badges --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 45bec0d81c..da811aa676 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -[![Gated by Zuul](https://zuul-ci.org/gated.svg)](https://ansible.softwarefactory-project.io/zuul/status) +[![Gated by Zuul](https://zuul-ci.org/gated.svg)](https://ansible.softwarefactory-project.io/zuul/status) [![Code of Conduct](https://img.shields.io/badge/code%20of%20conduct-Ansible-yellow.svg)](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html) [![Apache v2 License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](https://github.com/ansible/awx/blob/devel/LICENSE.md) [![AWX Mailing List](https://img.shields.io/badge/mailing%20list-AWX-orange.svg)](https://groups.google.com/g/awx-project) +[![IRC Chat](https://img.shields.io/badge/IRC-%23ansible--awx-blueviolet.svg)](https://webchat.freenode.net/#ansible-awx) AWX @@ -38,8 +39,3 @@ We welcome your feedback and ideas. Here's how to reach us with feedback and que - Join the `#ansible-awx` channel on irc.freenode.net - Join the [mailing list](https://groups.google.com/forum/#!forum/awx-project) - -License -------- - -[Apache v2](./LICENSE.md) From 41613ff54487cb88a4efa9b7a0e1077c47bc2b81 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Fri, 26 Jun 2020 17:01:05 -0400 Subject: [PATCH 004/178] Add a new ExecutionEnvironment model --- .../migrations/0124_execution_environments.py | 69 +++++++++++++++++++ awx/main/models/__init__.py | 3 +- awx/main/models/activity_stream.py | 2 + awx/main/models/execution_environments.py | 37 ++++++++++ awx/main/models/mixins.py | 18 ++++- awx/main/models/organization.py | 9 +++ awx/main/models/unified_jobs.py | 6 +- 7 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 awx/main/migrations/0124_execution_environments.py create mode 100644 awx/main/models/execution_environments.py diff --git a/awx/main/migrations/0124_execution_environments.py b/awx/main/migrations/0124_execution_environments.py new file mode 100644 index 0000000000..982b1b21e8 --- /dev/null +++ b/awx/main/migrations/0124_execution_environments.py @@ -0,0 +1,69 @@ +# Generated by Django 2.2.11 on 2020-07-08 18:42 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.db.models.expressions +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('taggit', '0003_taggeditem_add_unique_index'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('main', '0123_drop_hg_support'), + ] + + operations = [ + migrations.AddField( + model_name='unifiedjob', + name='pull', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='unifiedjobtemplate', + name='pull', + field=models.BooleanField(default=True), + ), + migrations.CreateModel( + name='ExecutionEnvironment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=None, editable=False)), + ('modified', models.DateTimeField(default=None, editable=False)), + ('description', models.TextField(blank=True, default='')), + ('image', models.CharField(help_text='The registry location where the container is stored.', max_length=1024, verbose_name='image location')), + ('managed_by_tower', models.BooleanField(default=False, editable=False)), + ('created_by', models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{'class': 'executionenvironment', 'model_name': 'executionenvironment', 'app_label': 'main'}(class)s_created+", to=settings.AUTH_USER_MODEL)), + ('credential', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='executionenvironments', to='main.Credential')), + ('modified_by', models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{'class': 'executionenvironment', 'model_name': 'executionenvironment', 'app_label': 'main'}(class)s_modified+", to=settings.AUTH_USER_MODEL)), + ('organization', models.ForeignKey(blank=True, default=None, help_text='The organization used to determine access to this execution environment.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='executionenvironments', to='main.Organization')), + ('tags', taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')), + ], + options={ + 'ordering': (django.db.models.expressions.OrderBy(django.db.models.expressions.F('organization_id'), nulls_first=True), 'image'), + 'unique_together': {('organization', 'image')}, + }, + ), + migrations.AddField( + model_name='activitystream', + name='execution_environment', + field=models.ManyToManyField(blank=True, to='main.ExecutionEnvironment'), + ), + migrations.AddField( + model_name='organization', + name='default_environment', + field=models.ForeignKey(blank=True, default=None, help_text='The default execution environment for jobs run by this organization.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='main.ExecutionEnvironment'), + ), + migrations.AddField( + model_name='unifiedjob', + name='execution_environment', + field=models.ForeignKey(blank=True, default=None, help_text='The container image to be used for execution.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='unifiedjobs', to='main.ExecutionEnvironment'), + ), + migrations.AddField( + model_name='unifiedjobtemplate', + name='execution_environment', + field=models.ForeignKey(blank=True, default=None, help_text='The container image to be used for execution.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='unifiedjobtemplates', to='main.ExecutionEnvironment'), + ), + ] diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 87fa5d791f..7e3209bd6d 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -35,6 +35,7 @@ from awx.main.models.events import ( # noqa ) from awx.main.models.ad_hoc_commands import AdHocCommand # noqa from awx.main.models.schedules import Schedule # noqa +from awx.main.models.execution_environments import ExecutionEnvironment # noqa from awx.main.models.activity_stream import ActivityStream # noqa from awx.main.models.ha import ( # noqa Instance, InstanceGroup, TowerScheduleState, @@ -45,7 +46,7 @@ from awx.main.models.rbac import ( # noqa ROLE_SINGLETON_SYSTEM_AUDITOR, ) from awx.main.models.mixins import ( # noqa - CustomVirtualEnvMixin, ResourceMixin, SurveyJobMixin, + CustomVirtualEnvMixin, ExecutionEnvironmentMixin, ResourceMixin, SurveyJobMixin, SurveyJobTemplateMixin, TaskManagerInventoryUpdateMixin, TaskManagerJobMixin, TaskManagerProjectUpdateMixin, TaskManagerUnifiedJobMixin, diff --git a/awx/main/models/activity_stream.py b/awx/main/models/activity_stream.py index 85666e49d2..1c344692d6 100644 --- a/awx/main/models/activity_stream.py +++ b/awx/main/models/activity_stream.py @@ -61,6 +61,7 @@ class ActivityStream(models.Model): team = models.ManyToManyField("Team", blank=True) project = models.ManyToManyField("Project", blank=True) project_update = models.ManyToManyField("ProjectUpdate", blank=True) + execution_environment = models.ManyToManyField("ExecutionEnvironment", blank=True) job_template = models.ManyToManyField("JobTemplate", blank=True) job = models.ManyToManyField("Job", blank=True) workflow_job_template_node = models.ManyToManyField("WorkflowJobTemplateNode", blank=True) @@ -74,6 +75,7 @@ class ActivityStream(models.Model): ad_hoc_command = models.ManyToManyField("AdHocCommand", blank=True) schedule = models.ManyToManyField("Schedule", blank=True) custom_inventory_script = models.ManyToManyField("CustomInventoryScript", blank=True) + execution_environment = models.ManyToManyField("ExecutionEnvironment", blank=True) notification_template = models.ManyToManyField("NotificationTemplate", blank=True) notification = models.ManyToManyField("Notification", blank=True) label = models.ManyToManyField("Label", blank=True) diff --git a/awx/main/models/execution_environments.py b/awx/main/models/execution_environments.py new file mode 100644 index 0000000000..c4d6fcb155 --- /dev/null +++ b/awx/main/models/execution_environments.py @@ -0,0 +1,37 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from awx.main.models.base import PrimordialModel + + +__all__ = ['ExecutionEnvironment'] + + +class ExecutionEnvironment(PrimordialModel): + class Meta: + unique_together = ('organization', 'image') + ordering = (models.F('organization_id').asc(nulls_first=True), 'image') + + organization = models.ForeignKey( + 'Organization', + null=True, + default=None, + blank=True, + on_delete=models.CASCADE, + related_name='%(class)ss', + help_text=_('The organization used to determine access to this execution environment.'), + ) + image = models.CharField( + max_length=1024, + verbose_name=_('image location'), + help_text=_("The registry location where the container is stored."), + ) + managed_by_tower = models.BooleanField(default=False, editable=False) + credential = models.ForeignKey( + 'Credential', + related_name='%(class)ss', + blank=True, + null=True, + default=None, + on_delete=models.SET_NULL, + ) diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index ce6d3717a7..54c8a000a4 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -34,7 +34,7 @@ logger = logging.getLogger('awx.main.models.mixins') __all__ = ['ResourceMixin', 'SurveyJobTemplateMixin', 'SurveyJobMixin', 'TaskManagerUnifiedJobMixin', 'TaskManagerJobMixin', 'TaskManagerProjectUpdateMixin', - 'TaskManagerInventoryUpdateMixin', 'CustomVirtualEnvMixin'] + 'TaskManagerInventoryUpdateMixin', 'ExecutionEnvironmentMixin', 'CustomVirtualEnvMixin'] class ResourceMixin(models.Model): @@ -441,6 +441,22 @@ class TaskManagerInventoryUpdateMixin(TaskManagerUpdateOnLaunchMixin): abstract = True +class ExecutionEnvironmentMixin(models.Model): + class Meta: + abstract = True + + execution_environment = models.ForeignKey( + 'ExecutionEnvironment', + null=True, + blank=True, + default=None, + on_delete=models.SET_NULL, + related_name='%(class)ss', + help_text=_('The container image to be used for execution.'), + ) + pull = models.BooleanField(default=True) + + class CustomVirtualEnvMixin(models.Model): class Meta: abstract = True diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index bf2e07d255..3730fe9af1 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -61,6 +61,15 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi blank=True, related_name='%(class)s_notification_templates_for_approvals' ) + default_environment = models.ForeignKey( + 'ExecutionEnvironment', + null=True, + blank=True, + default=None, + on_delete=models.SET_NULL, + related_name='+', + help_text=_('The default execution environment for jobs run by this organization.'), + ) admin_role = ImplicitRoleField( parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 064585c6c1..f4a9e1ba45 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -39,7 +39,7 @@ from awx.main.models.base import ( from awx.main.dispatch import get_local_queuename from awx.main.dispatch.control import Control as ControlDispatcher from awx.main.registrar import activity_stream_registrar -from awx.main.models.mixins import ResourceMixin, TaskManagerUnifiedJobMixin +from awx.main.models.mixins import ResourceMixin, TaskManagerUnifiedJobMixin, ExecutionEnvironmentMixin from awx.main.utils import ( camelcase_to_underscore, get_model_for_type, encrypt_dict, decrypt_field, _inventory_updates, @@ -59,7 +59,7 @@ logger_job_lifecycle = logging.getLogger('awx.analytics.job_lifecycle') # NOTE: ACTIVE_STATES moved to constants because it is used by parent modules -class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, NotificationFieldsModel): +class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, ExecutionEnvironmentMixin, NotificationFieldsModel): ''' Concrete base class for unified job templates. ''' @@ -527,7 +527,7 @@ class StdoutMaxBytesExceeded(Exception): class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique, - UnifiedJobTypeStringMixin, TaskManagerUnifiedJobMixin): + UnifiedJobTypeStringMixin, TaskManagerUnifiedJobMixin, ExecutionEnvironmentMixin): ''' Concrete base class for unified job run by the task engine. ''' From 9697999ddd3143dd2317c39fccfb4c80102dfe89 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Wed, 1 Jul 2020 16:12:24 -0400 Subject: [PATCH 005/178] Create the RBAC access class for execution environments --- awx/main/access.py | 51 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 89a6c0607d..24e6bbc569 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -29,9 +29,9 @@ from awx.main.utils import ( ) from awx.main.models import ( ActivityStream, AdHocCommand, AdHocCommandEvent, Credential, CredentialType, - CredentialInputSource, CustomInventoryScript, Group, Host, Instance, InstanceGroup, - Inventory, InventorySource, InventoryUpdate, InventoryUpdateEvent, Job, JobEvent, - JobHostSummary, JobLaunchConfig, JobTemplate, Label, Notification, + CredentialInputSource, CustomInventoryScript, ExecutionEnvironment, Group, Host, Instance, + InstanceGroup, Inventory, InventorySource, InventoryUpdate, InventoryUpdateEvent, Job, + JobEvent, JobHostSummary, JobLaunchConfig, JobTemplate, Label, Notification, NotificationTemplate, Organization, Project, ProjectUpdate, ProjectUpdateEvent, Role, Schedule, SystemJob, SystemJobEvent, SystemJobTemplate, Team, UnifiedJob, UnifiedJobTemplate, WorkflowJob, @@ -1308,6 +1308,51 @@ class TeamAccess(BaseAccess): *args, **kwargs) +class ExecutionEnvironmentAccess(BaseAccess): + """ + I can see an execution environment when: + - I'm a superuser + - I'm a member of the organization + - it is a global ExecutionEnvironment + I can create/change an execution environment when: + - I'm a superuser + - I'm an admin for the organization(s) + """ + + model = ExecutionEnvironment + + def filtered_queryset(self): + return ExecutionEnvironment.objects.filter( + Q(organization__in=Organization.accessible_pk_qs(self.user, 'admin_role')) | + Q(organization__isnull=True) + ).distinct() + + @check_superuser + def can_add(self, data): + if not data: # So the browseable API will work + return Organization.accessible_objects(self.user, 'admin_role').exists() + return self.check_related('organization', Organization, data) + + @check_superuser + def can_change(self, obj, data): + if obj and obj.organization_id is None: + raise PermissionDenied + if self.user not in obj.organization.admin_role: + raise PermissionDenied + org_pk = get_pk_from_dict(data, 'organization') + if obj and obj.organization_id != org_pk: + # Prevent moving an EE to a different organization, unless a superuser or admin on both orgs. + if obj.organization_id is None or org_pk is None: + raise PermissionDenied + if self.user not in Organization.objects.get(id=org_pk).admin_role: + raise PermissionDenied + + return True + + def can_delete(self, obj): + return self.can_change(obj, None) + + class ProjectAccess(NotificationAttachMixin, BaseAccess): ''' I can see projects when: From 61cbd34586f687f4ef45d830ddfef2c962ba4755 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Wed, 1 Jul 2020 16:14:39 -0400 Subject: [PATCH 006/178] Add in the basic list and detail api views --- awx/api/serializers.py | 17 ++++++++++++++++- awx/api/urls/execution_environments.py | 14 ++++++++++++++ awx/api/urls/urls.py | 2 ++ awx/api/views/__init__.py | 12 ++++++++++++ awx/api/views/root.py | 1 + awx/main/models/execution_environments.py | 4 ++++ 6 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 awx/api/urls/execution_environments.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index d34c0d924a..cb47c99cb3 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -50,7 +50,7 @@ from awx.main.constants import ( ) from awx.main.models import ( ActivityStream, AdHocCommand, AdHocCommandEvent, Credential, CredentialInputSource, - CredentialType, CustomInventoryScript, Group, Host, Instance, + CredentialType, CustomInventoryScript, ExecutionEnvironment, Group, Host, Instance, InstanceGroup, Inventory, InventorySource, InventoryUpdate, InventoryUpdateEvent, Job, JobEvent, JobHostSummary, JobLaunchConfig, JobNotificationMixin, JobTemplate, Label, Notification, NotificationTemplate, @@ -1347,6 +1347,21 @@ class ProjectOptionsSerializer(BaseSerializer): return super(ProjectOptionsSerializer, self).validate(attrs) +class ExecutionEnvironmentSerializer(BaseSerializer): + class Meta: + model = ExecutionEnvironment + fields = ('*', '-name', 'organization', 'image', 'managed_by_tower', 'credential') + + def get_related(self, obj): + res = super(ExecutionEnvironmentSerializer, self).get_related(obj) + if obj.organization: + res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) + if obj.credential: + res['credential'] = self.reverse('api:credential_detail', + kwargs={'pk': obj.credential.pk}) + return res + + class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer): status = serializers.ChoiceField(choices=Project.PROJECT_STATUS_CHOICES, read_only=True) diff --git a/awx/api/urls/execution_environments.py b/awx/api/urls/execution_environments.py new file mode 100644 index 0000000000..6e59f8fc45 --- /dev/null +++ b/awx/api/urls/execution_environments.py @@ -0,0 +1,14 @@ +from django.conf.urls import url + +from awx.api.views import ( + ExecutionEnvironmentList, + ExecutionEnvironmentDetail, +) + + +urls = [ + url(r'^$', ExecutionEnvironmentList.as_view(), name='execution_environment_list'), + url(r'^(?P[0-9]+)/$', ExecutionEnvironmentDetail.as_view(), name='execution_environment_detail'), +] + +__all__ = ['urls'] diff --git a/awx/api/urls/urls.py b/awx/api/urls/urls.py index 636e68e4bd..2beeb47a47 100644 --- a/awx/api/urls/urls.py +++ b/awx/api/urls/urls.py @@ -42,6 +42,7 @@ from .user import urls as user_urls from .project import urls as project_urls from .project_update import urls as project_update_urls from .inventory import urls as inventory_urls +from .execution_environments import urls as execution_environment_urls from .team import urls as team_urls from .host import urls as host_urls from .group import urls as group_urls @@ -106,6 +107,7 @@ v2_urls = [ url(r'^schedules/', include(schedule_urls)), url(r'^organizations/', include(organization_urls)), url(r'^users/', include(user_urls)), + url(r'^execution_environments/', include(execution_environment_urls)), url(r'^projects/', include(project_urls)), url(r'^project_updates/', include(project_update_urls)), url(r'^teams/', include(team_urls)), diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 43e845af0c..ff4689f78e 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -685,6 +685,18 @@ class TeamAccessList(ResourceAccessList): parent_model = models.Team +class ExecutionEnvironmentList(ListCreateAPIView): + + model = models.ExecutionEnvironment + serializer_class = serializers.ExecutionEnvironmentSerializer + + +class ExecutionEnvironmentDetail(RetrieveUpdateDestroyAPIView): + + model = models.ExecutionEnvironment + serializer_class = serializers.ExecutionEnvironmentSerializer + + class ProjectList(ListCreateAPIView): model = models.Project diff --git a/awx/api/views/root.py b/awx/api/views/root.py index 0f5e7e6cdd..d6fc20d105 100644 --- a/awx/api/views/root.py +++ b/awx/api/views/root.py @@ -100,6 +100,7 @@ class ApiVersionRootView(APIView): data['dashboard'] = reverse('api:dashboard_view', request=request) data['organizations'] = reverse('api:organization_list', request=request) data['users'] = reverse('api:user_list', request=request) + data['execution_environments'] = reverse('api:execution_environment_list', request=request) data['projects'] = reverse('api:project_list', request=request) data['project_updates'] = reverse('api:project_update_list', request=request) data['teams'] = reverse('api:team_list', request=request) diff --git a/awx/main/models/execution_environments.py b/awx/main/models/execution_environments.py index c4d6fcb155..bdbe75eb49 100644 --- a/awx/main/models/execution_environments.py +++ b/awx/main/models/execution_environments.py @@ -1,6 +1,7 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ +from awx.api.versioning import reverse from awx.main.models.base import PrimordialModel @@ -35,3 +36,6 @@ class ExecutionEnvironment(PrimordialModel): default=None, on_delete=models.SET_NULL, ) + + def get_absolute_url(self, request=None): + return reverse('api:execution_environment_detail', kwargs={'pk': self.pk}, request=request) From 3c637cd54c15aa134de375700002d6ee0de13adf Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Thu, 2 Jul 2020 11:01:34 -0400 Subject: [PATCH 007/178] Change OrganizationSerializer to show and set default_environment --- awx/api/serializers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index cb47c99cb3..392a25659f 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -107,6 +107,7 @@ SUMMARIZABLE_FK_FIELDS = { 'insights_credential_id',), 'host': DEFAULT_SUMMARY_FIELDS, 'group': DEFAULT_SUMMARY_FIELDS, + 'default_environment': ('id', 'organization_id', 'image', 'description'), 'project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'), 'source_project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'), 'project_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',), @@ -1243,7 +1244,7 @@ class OrganizationSerializer(BaseSerializer): class Meta: model = Organization - fields = ('*', 'max_hosts', 'custom_virtualenv',) + fields = ('*', 'max_hosts', 'custom_virtualenv', 'default_environment',) def get_related(self, obj): res = super(OrganizationSerializer, self).get_related(obj) @@ -1268,6 +1269,9 @@ class OrganizationSerializer(BaseSerializer): instance_groups = self.reverse('api:organization_instance_groups_list', kwargs={'pk': obj.pk}), galaxy_credentials = self.reverse('api:organization_galaxy_credentials_list', kwargs={'pk': obj.pk}), )) + if obj.default_environment: + res['default_environment'] = self.reverse('api:execution_environment_detail', + kwargs={'pk': obj.default_environment_id}) return res def get_summary_fields(self, obj): From cb766c6a95b5d994f781e5f205efc71cb47ab6de Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Mon, 6 Jul 2020 11:00:54 -0400 Subject: [PATCH 008/178] Add execution_environment and pull to the fields for UJs and UJTs --- awx/api/serializers.py | 10 +++++++++- .../tests/unit/api/serializers/test_job_serializers.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 392a25659f..c5ea30e0f2 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -108,6 +108,7 @@ SUMMARIZABLE_FK_FIELDS = { 'host': DEFAULT_SUMMARY_FIELDS, 'group': DEFAULT_SUMMARY_FIELDS, 'default_environment': ('id', 'organization_id', 'image', 'description'), + 'execution_environment': ('id', 'organization_id', 'image', 'description'), 'project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'), 'source_project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'), 'project_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',), @@ -648,7 +649,7 @@ class UnifiedJobTemplateSerializer(BaseSerializer): class Meta: model = UnifiedJobTemplate fields = ('*', 'last_job_run', 'last_job_failed', - 'next_job_run', 'status') + 'next_job_run', 'status', 'execution_environment', 'pull') def get_related(self, obj): res = super(UnifiedJobTemplateSerializer, self).get_related(obj) @@ -658,6 +659,9 @@ class UnifiedJobTemplateSerializer(BaseSerializer): res['last_job'] = obj.last_job.get_absolute_url(request=self.context.get('request')) if obj.next_schedule: res['next_schedule'] = obj.next_schedule.get_absolute_url(request=self.context.get('request')) + if obj.execution_environment_id: + res['execution_environment'] = self.reverse('api:execution_environment_detail', + kwargs={'pk': obj.execution_environment_id}) return res def get_types(self): @@ -712,6 +716,7 @@ class UnifiedJobSerializer(BaseSerializer): class Meta: model = UnifiedJob fields = ('*', 'unified_job_template', 'launch_type', 'status', + 'execution_environment', 'pull', 'failed', 'started', 'finished', 'canceled_on', 'elapsed', 'job_args', 'job_cwd', 'job_env', 'job_explanation', 'execution_node', 'controller_node', @@ -749,6 +754,9 @@ class UnifiedJobSerializer(BaseSerializer): res['stdout'] = self.reverse('api:ad_hoc_command_stdout', kwargs={'pk': obj.pk}) if obj.workflow_job_id: res['source_workflow_job'] = self.reverse('api:workflow_job_detail', kwargs={'pk': obj.workflow_job_id}) + if obj.execution_environment_id: + res['execution_environment'] = self.reverse('api:execution_environment_detail', + kwargs={'pk': obj.execution_environment_id}) return res def get_summary_fields(self, obj): diff --git a/awx/main/tests/unit/api/serializers/test_job_serializers.py b/awx/main/tests/unit/api/serializers/test_job_serializers.py index e7b0ee7792..53cc07676d 100644 --- a/awx/main/tests/unit/api/serializers/test_job_serializers.py +++ b/awx/main/tests/unit/api/serializers/test_job_serializers.py @@ -40,7 +40,7 @@ def project_update(mocker): @pytest.fixture def job(mocker, job_template, project_update): return mocker.MagicMock(pk=5, job_template=job_template, project_update=project_update, - workflow_job_id=None) + workflow_job_id=None, execution_environment_id=None) @pytest.fixture From cc429f97411e59b1da91a2efc531bc9db0e10a34 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Mon, 6 Jul 2020 13:01:44 -0400 Subject: [PATCH 009/178] Expose an API view for all of the execution environments under an org --- awx/api/serializers.py | 5 +++-- awx/api/urls/organization.py | 2 ++ awx/api/views/__init__.py | 1 + awx/api/views/organization.py | 11 +++++++++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index c5ea30e0f2..fa15c45e53 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1256,7 +1256,8 @@ class OrganizationSerializer(BaseSerializer): def get_related(self, obj): res = super(OrganizationSerializer, self).get_related(obj) - res.update(dict( + res.update( + execution_environments = self.reverse('api:organization_execution_environments_list', kwargs={'pk': obj.pk}), projects = self.reverse('api:organization_projects_list', kwargs={'pk': obj.pk}), inventories = self.reverse('api:organization_inventories_list', kwargs={'pk': obj.pk}), job_templates = self.reverse('api:organization_job_templates_list', kwargs={'pk': obj.pk}), @@ -1276,7 +1277,7 @@ class OrganizationSerializer(BaseSerializer): access_list = self.reverse('api:organization_access_list', kwargs={'pk': obj.pk}), instance_groups = self.reverse('api:organization_instance_groups_list', kwargs={'pk': obj.pk}), galaxy_credentials = self.reverse('api:organization_galaxy_credentials_list', kwargs={'pk': obj.pk}), - )) + ) if obj.default_environment: res['default_environment'] = self.reverse('api:execution_environment_detail', kwargs={'pk': obj.default_environment_id}) diff --git a/awx/api/urls/organization.py b/awx/api/urls/organization.py index 12b2807905..9d8fecf4bc 100644 --- a/awx/api/urls/organization.py +++ b/awx/api/urls/organization.py @@ -9,6 +9,7 @@ from awx.api.views import ( OrganizationUsersList, OrganizationAdminsList, OrganizationInventoriesList, + OrganizationExecutionEnvironmentsList, OrganizationProjectsList, OrganizationJobTemplatesList, OrganizationWorkflowJobTemplatesList, @@ -34,6 +35,7 @@ urls = [ url(r'^(?P[0-9]+)/users/$', OrganizationUsersList.as_view(), name='organization_users_list'), url(r'^(?P[0-9]+)/admins/$', OrganizationAdminsList.as_view(), name='organization_admins_list'), url(r'^(?P[0-9]+)/inventories/$', OrganizationInventoriesList.as_view(), name='organization_inventories_list'), + url(r'^(?P[0-9]+)/execution_environments/$', OrganizationExecutionEnvironmentsList.as_view(), name='organization_execution_environments_list'), url(r'^(?P[0-9]+)/projects/$', OrganizationProjectsList.as_view(), name='organization_projects_list'), url(r'^(?P[0-9]+)/job_templates/$', OrganizationJobTemplatesList.as_view(), name='organization_job_templates_list'), url(r'^(?P[0-9]+)/workflow_job_templates/$', OrganizationWorkflowJobTemplatesList.as_view(), name='organization_workflow_job_templates_list'), diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index ff4689f78e..47a0e0a3a7 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -112,6 +112,7 @@ from awx.api.views.organization import ( # noqa OrganizationInventoriesList, OrganizationUsersList, OrganizationAdminsList, + OrganizationExecutionEnvironmentsList, OrganizationProjectsList, OrganizationJobTemplatesList, OrganizationWorkflowJobTemplatesList, diff --git a/awx/api/views/organization.py b/awx/api/views/organization.py index d03dfcc86f..b0955a3a42 100644 --- a/awx/api/views/organization.py +++ b/awx/api/views/organization.py @@ -15,6 +15,7 @@ from awx.main.models import ( Inventory, Host, Project, + ExecutionEnvironment, JobTemplate, WorkflowJobTemplate, Organization, @@ -45,6 +46,7 @@ from awx.api.serializers import ( RoleSerializer, NotificationTemplateSerializer, InstanceGroupSerializer, + ExecutionEnvironmentSerializer, ProjectSerializer, JobTemplateSerializer, WorkflowJobTemplateSerializer, CredentialSerializer ) @@ -141,6 +143,15 @@ class OrganizationProjectsList(SubListCreateAPIView): parent_key = 'organization' +class OrganizationExecutionEnvironmentsList(SubListCreateAttachDetachAPIView): + + model = ExecutionEnvironment + serializer_class = ExecutionEnvironmentSerializer + parent_model = Organization + relationship = 'executionenvironments' + parent_key = 'organization' + + class OrganizationJobTemplatesList(SubListCreateAPIView): model = JobTemplate From c05e4e07ee9bb8f264a257476423eaf191779758 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Mon, 6 Jul 2020 15:44:06 -0400 Subject: [PATCH 010/178] Expose execution environments in awxkit and awx-cli --- awxkit/awxkit/api/pages/__init__.py | 1 + awxkit/awxkit/api/pages/api.py | 2 ++ .../api/pages/execution_environments.py | 33 +++++++++++++++++++ awxkit/awxkit/api/resources.py | 3 ++ 4 files changed, 39 insertions(+) create mode 100644 awxkit/awxkit/api/pages/execution_environments.py diff --git a/awxkit/awxkit/api/pages/__init__.py b/awxkit/awxkit/api/pages/__init__.py index fafe5dc08f..1d78d4ba5e 100644 --- a/awxkit/awxkit/api/pages/__init__.py +++ b/awxkit/awxkit/api/pages/__init__.py @@ -14,6 +14,7 @@ from .teams import * # NOQA from .credentials import * # NOQA from .unified_jobs import * # NOQA from .unified_job_templates import * # NOQA +from .execution_environments import * # NOQA from .projects import * # NOQA from .inventory import * # NOQA from .system_job_templates import * # NOQA diff --git a/awxkit/awxkit/api/pages/api.py b/awxkit/awxkit/api/pages/api.py index 3209232352..4edc07857f 100644 --- a/awxkit/awxkit/api/pages/api.py +++ b/awxkit/awxkit/api/pages/api.py @@ -23,6 +23,7 @@ EXPORTABLE_RESOURCES = [ 'inventory_sources', 'job_templates', 'workflow_job_templates', + 'execution_environments', ] @@ -33,6 +34,7 @@ EXPORTABLE_RELATIONS = [ 'Credentials', 'Hosts', 'Groups', + 'ExecutionEnvironments', ] diff --git a/awxkit/awxkit/api/pages/execution_environments.py b/awxkit/awxkit/api/pages/execution_environments.py new file mode 100644 index 0000000000..e48ef324bd --- /dev/null +++ b/awxkit/awxkit/api/pages/execution_environments.py @@ -0,0 +1,33 @@ +import logging + +from awxkit.api.mixins import HasCreate +from awxkit.api.pages import ( + Credential, + Organization, +) +from awxkit.api.resources import resources + +from . import base +from . import page + + +log = logging.getLogger(__name__) + + +class ExecutionEnvironment(HasCreate, base.Base): + + dependencies = [Organization, Credential] + NATURAL_KEY = ('organization', 'image') + + +page.register_page([resources.execution_environment, + (resources.execution_environments, 'post'), + (resources.organization_execution_environments, 'post')], ExecutionEnvironment) + + +class ExecutionEnvironments(page.PageList, ExecutionEnvironment): + pass + + +page.register_page([resources.execution_environments, + resources.organization_execution_environments], ExecutionEnvironments) diff --git a/awxkit/awxkit/api/resources.py b/awxkit/awxkit/api/resources.py index d6340cd2d7..997ada1e70 100644 --- a/awxkit/awxkit/api/resources.py +++ b/awxkit/awxkit/api/resources.py @@ -28,6 +28,8 @@ class Resources(object): _credential_types = 'credential_types/' _credentials = 'credentials/' _dashboard = 'dashboard/' + _execution_environment = r'execution_environments/\d+/' + _execution_environments = 'execution_environments/' _fact_view = r'hosts/\d+/fact_view/' _group = r'groups/\d+/' _group_access_list = r'groups/\d+/access_list/' @@ -141,6 +143,7 @@ class Resources(object): _organization_access_list = r'organizations/\d+/access_list/' _organization_admins = r'organizations/\d+/admins/' _organization_applications = r'organizations/\d+/applications/' + _organization_execution_environments = r'organizations/\d+/execution_environments/' _organization_inventories = r'organizations/\d+/inventories/' _organization_users = r'organizations/\d+/users/' _organizations = 'organizations/' From 5ec7378135f072e762f4e501b2e65258b7053c01 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Mon, 6 Jul 2020 16:23:01 -0400 Subject: [PATCH 011/178] Add a new Swagger topic --- awx/api/views/__init__.py | 2 ++ awx/api/views/organization.py | 1 + 2 files changed, 3 insertions(+) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 47a0e0a3a7..0bfa00d18c 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -690,12 +690,14 @@ class ExecutionEnvironmentList(ListCreateAPIView): model = models.ExecutionEnvironment serializer_class = serializers.ExecutionEnvironmentSerializer + swagger_topic = "Execution Environments" class ExecutionEnvironmentDetail(RetrieveUpdateDestroyAPIView): model = models.ExecutionEnvironment serializer_class = serializers.ExecutionEnvironmentSerializer + swagger_topic = "Execution Environments" class ProjectList(ListCreateAPIView): diff --git a/awx/api/views/organization.py b/awx/api/views/organization.py index b0955a3a42..b33259a8ad 100644 --- a/awx/api/views/organization.py +++ b/awx/api/views/organization.py @@ -150,6 +150,7 @@ class OrganizationExecutionEnvironmentsList(SubListCreateAttachDetachAPIView): parent_model = Organization relationship = 'executionenvironments' parent_key = 'organization' + swagger_topic = "Execution Environments" class OrganizationJobTemplatesList(SubListCreateAPIView): From f9741b619c88085b96b749fa4a08215226fbdbeb Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Wed, 8 Jul 2020 11:49:23 -0400 Subject: [PATCH 012/178] Make changes to support capture by the activity stream Including exposing a new API view for a particular EE's activity stream objects. --- awx/api/serializers.py | 3 +++ awx/api/urls/execution_environments.py | 2 ++ awx/api/views/__init__.py | 16 ++++++++++++++++ awx/main/models/__init__.py | 1 + awx/main/signals.py | 1 + 5 files changed, 23 insertions(+) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index fa15c45e53..4aff55e926 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1367,6 +1367,9 @@ class ExecutionEnvironmentSerializer(BaseSerializer): def get_related(self, obj): res = super(ExecutionEnvironmentSerializer, self).get_related(obj) + res.update( + activity_stream = self.reverse('api:execution_environment_activity_stream_list', kwargs={'pk': obj.pk}), + ) if obj.organization: res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) if obj.credential: diff --git a/awx/api/urls/execution_environments.py b/awx/api/urls/execution_environments.py index 6e59f8fc45..e2310caec3 100644 --- a/awx/api/urls/execution_environments.py +++ b/awx/api/urls/execution_environments.py @@ -3,12 +3,14 @@ from django.conf.urls import url from awx.api.views import ( ExecutionEnvironmentList, ExecutionEnvironmentDetail, + ExecutionEnvironmentActivityStreamList, ) urls = [ url(r'^$', ExecutionEnvironmentList.as_view(), name='execution_environment_list'), url(r'^(?P[0-9]+)/$', ExecutionEnvironmentDetail.as_view(), name='execution_environment_detail'), + url(r'^(?P[0-9]+)/activity_stream/$', ExecutionEnvironmentActivityStreamList.as_view(), name='execution_environment_activity_stream_list'), ] __all__ = ['urls'] diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 0bfa00d18c..4c4ec799fe 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -700,6 +700,22 @@ class ExecutionEnvironmentDetail(RetrieveUpdateDestroyAPIView): swagger_topic = "Execution Environments" +class ExecutionEnvironmentActivityStreamList(SubListAPIView): + + model = models.ActivityStream + serializer_class = serializers.ActivityStreamSerializer + parent_model = models.ExecutionEnvironment + relationship = 'activitystream_set' + search_fields = ('changes',) + + def get_queryset(self): + parent = self.get_parent_object() + self.check_parent_access(parent) + + qs = self.request.user.get_queryset(self.model) + return qs.filter(execution_environment=parent) + + class ProjectList(ListCreateAPIView): model = models.Project diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 7e3209bd6d..52cabf3774 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -222,6 +222,7 @@ activity_stream_registrar.connect(CredentialType) activity_stream_registrar.connect(Team) activity_stream_registrar.connect(Project) #activity_stream_registrar.connect(ProjectUpdate) +activity_stream_registrar.connect(ExecutionEnvironment) activity_stream_registrar.connect(JobTemplate) activity_stream_registrar.connect(Job) activity_stream_registrar.connect(AdHocCommand) diff --git a/awx/main/signals.py b/awx/main/signals.py index 0a29fa9d6c..ac7a3d2301 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -368,6 +368,7 @@ def model_serializer_mapping(): models.Credential: serializers.CredentialSerializer, models.Team: serializers.TeamSerializer, models.Project: serializers.ProjectSerializer, + models.ExecutionEnvironment: serializers.ExecutionEnvironmentSerializer, models.JobTemplate: serializers.JobTemplateWithSpecSerializer, models.Job: serializers.JobSerializer, models.AdHocCommand: serializers.AdHocCommandSerializer, From 45a0084f78ebf650803967126bd12462b3d39141 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Wed, 8 Jul 2020 14:43:14 -0400 Subject: [PATCH 013/178] Add a sublist api view for the UJTs that use a given execution environment --- awx/api/serializers.py | 3 ++- awx/api/urls/execution_environments.py | 2 ++ awx/api/views/__init__.py | 8 ++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 4aff55e926..46e430a8e3 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1368,7 +1368,8 @@ class ExecutionEnvironmentSerializer(BaseSerializer): def get_related(self, obj): res = super(ExecutionEnvironmentSerializer, self).get_related(obj) res.update( - activity_stream = self.reverse('api:execution_environment_activity_stream_list', kwargs={'pk': obj.pk}), + activity_stream=self.reverse('api:execution_environment_activity_stream_list', kwargs={'pk': obj.pk}), + unified_job_templates=self.reverse('api:execution_environment_job_template_list', kwargs={'pk': obj.pk}), ) if obj.organization: res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) diff --git a/awx/api/urls/execution_environments.py b/awx/api/urls/execution_environments.py index e2310caec3..08f852be08 100644 --- a/awx/api/urls/execution_environments.py +++ b/awx/api/urls/execution_environments.py @@ -3,6 +3,7 @@ from django.conf.urls import url from awx.api.views import ( ExecutionEnvironmentList, ExecutionEnvironmentDetail, + ExecutionEnvironmentJobTemplateList, ExecutionEnvironmentActivityStreamList, ) @@ -10,6 +11,7 @@ from awx.api.views import ( urls = [ url(r'^$', ExecutionEnvironmentList.as_view(), name='execution_environment_list'), url(r'^(?P[0-9]+)/$', ExecutionEnvironmentDetail.as_view(), name='execution_environment_detail'), + url(r'^(?P[0-9]+)/unified_job_templates/$', ExecutionEnvironmentJobTemplateList.as_view(), name='execution_environment_job_template_list'), url(r'^(?P[0-9]+)/activity_stream/$', ExecutionEnvironmentActivityStreamList.as_view(), name='execution_environment_activity_stream_list'), ] diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 4c4ec799fe..7ec932c9ce 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -700,6 +700,14 @@ class ExecutionEnvironmentDetail(RetrieveUpdateDestroyAPIView): swagger_topic = "Execution Environments" +class ExecutionEnvironmentJobTemplateList(SubListAPIView): + + model = models.UnifiedJobTemplate + serializer_class = serializers.UnifiedJobTemplateSerializer + parent_model = models.ExecutionEnvironment + relationship = 'unifiedjobtemplates' + + class ExecutionEnvironmentActivityStreamList(SubListAPIView): model = models.ActivityStream From 3cbf384ad11aae7832fc0b4c41a7db66e798b3c3 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Thu, 6 Aug 2020 13:48:46 -0400 Subject: [PATCH 014/178] Run a receptor node in the dev environment --- tools/ansible/roles/dockerfile/templates/Dockerfile.j2 | 6 ++++++ tools/docker-compose-cluster.yml | 3 +++ .../ansible/roles/sources/templates/docker-compose.yml.j2 | 1 + tools/docker-compose/supervisor.conf | 8 ++++++++ 4 files changed, 18 insertions(+) diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index 7acf365c25..a03ac7d238 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -135,6 +135,11 @@ RUN python3 -m ensurepip && pip3 install "virtualenv < 20" supervisor {% if buil RUN rm -rf /root/.cache && rm -rf /tmp/* +# Install Receptor +RUN cd /usr/local/bin && \ + curl -L http://nightlies.testing.ansible.com/receptor/receptor --output receptor && \ + chmod a+x receptor + # Install OpenShift CLI RUN cd /usr/local/bin && \ curl -L https://github.com/openshift/origin/releases/download/v3.11.0/openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz | \ @@ -216,6 +221,7 @@ RUN for dir in \ /var/log/nginx \ /var/lib/postgresql \ /var/run/supervisor \ + /var/run/receptor \ /var/lib/nginx ; \ do mkdir -m 0775 -p $dir ; chmod g+rw $dir ; chgrp root $dir ; done && \ for file in \ diff --git a/tools/docker-compose-cluster.yml b/tools/docker-compose-cluster.yml index 8532b6e942..7a90aa88c9 100644 --- a/tools/docker-compose-cluster.yml +++ b/tools/docker-compose-cluster.yml @@ -32,6 +32,7 @@ services: - "./redis/redis_socket_ha_1:/var/run/redis/" - "./docker-compose/supervisor.conf:/etc/supervisord.conf" ports: + - "2222:2222" - "5899-5999:5899-5999" awx-2: user: ${CURRENT_UID} @@ -51,6 +52,7 @@ services: - "./redis/redis_socket_ha_2:/var/run/redis/" - "./docker-compose/supervisor.conf:/etc/supervisord.conf" ports: + - "2223:2222" - "7899-7999:7899-7999" awx-3: user: ${CURRENT_UID} @@ -70,6 +72,7 @@ services: - "./redis/redis_socket_ha_3:/var/run/redis/" - "./docker-compose/supervisor.conf:/etc/supervisord.conf" ports: + - "2224:2222" - "8899-8999:8899-8999" redis_1: user: ${CURRENT_UID} diff --git a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 index 72181cfb0b..0cdf4f6728 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 @@ -14,6 +14,7 @@ services: SDB_PORT: 7899 AWX_GROUP_QUEUES: tower ports: + - "2222:2222" - "8888:8888" - "8080:8080" - "8013:8013" diff --git a/tools/docker-compose/supervisor.conf b/tools/docker-compose/supervisor.conf index 82e8962a9b..04ddb66838 100644 --- a/tools/docker-compose/supervisor.conf +++ b/tools/docker-compose/supervisor.conf @@ -83,6 +83,14 @@ redirect_stderr=true stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 +[program:awx-receptor] +command = receptor --node id=%(ENV_HOSTNAME)s --control-service filename=/var/run/receptor/receptor.sock --tcp-listener port=2222 +autostart = true +autorestart = true +stopsignal = KILL +stopasgroup = true +killasgroup = true + [group:tower-processes] programs=awx-dispatcher,awx-receiver,awx-uwsgi,awx-daphne,awx-nginx,awx-wsbroadcast,awx-rsyslogd priority=5 From 297fecba3a43bb27ceb09476106c51029a4ea8b4 Mon Sep 17 00:00:00 2001 From: Kersom <9053044+nixocio@users.noreply.github.com> Date: Wed, 19 Aug 2020 11:28:03 -0400 Subject: [PATCH 015/178] Add execution environments files (#7909) Update navigation bar and routing system to add execution environments. Also, add stub files for the remaining related work. See: https://github.com/ansible/awx/issues/7885 Also: https://github.com/ansible/awx/issues/7884 --- awx/ui_next/src/routeConfig.js | 10 +++- .../ExecutionEnvironment.jsx | 25 +++++++++ .../ExecutionEnvironmentAdd.jsx | 14 +++++ .../ExecutionEnvironmentAdd/index.js | 1 + .../ExecutionEnvironmentDetails.jsx | 14 +++++ .../ExecutionEnvironmentDetails/index.js | 1 + .../ExecutionEnvironmentEdit.jsx | 14 +++++ .../ExecutionEnvironmentEdit/index.js | 1 + .../ExecutionEnvironmentList.jsx | 14 +++++ .../ExecutionEnvironmentList/index.js | 1 + .../ExecutionEnvironments.jsx | 53 +++++++++++++++++++ .../ExecutionEnvironments.test.jsx | 25 +++++++++ .../src/screens/ExecutionEnvironment/index.js | 1 + 13 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.jsx create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/index.js create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/index.js create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/index.js create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/index.js create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.jsx create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.test.jsx create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/index.js diff --git a/awx/ui_next/src/routeConfig.js b/awx/ui_next/src/routeConfig.js index a343a7d1e0..c0aff394f0 100644 --- a/awx/ui_next/src/routeConfig.js +++ b/awx/ui_next/src/routeConfig.js @@ -2,13 +2,13 @@ import { t } from '@lingui/macro'; import ActivityStream from './screens/ActivityStream'; import Applications from './screens/Application'; -import Credentials from './screens/Credential'; import CredentialTypes from './screens/CredentialType'; +import Credentials from './screens/Credential'; import Dashboard from './screens/Dashboard'; +import ExecutionEnvironments from './screens/ExecutionEnvironment'; import Hosts from './screens/Host'; import InstanceGroups from './screens/InstanceGroup'; import Inventory from './screens/Inventory'; -import { Jobs } from './screens/Job'; import ManagementJobs from './screens/ManagementJob'; import NotificationTemplates from './screens/NotificationTemplate'; import Organizations from './screens/Organization'; @@ -19,6 +19,7 @@ import Teams from './screens/Team'; import Templates from './screens/Template'; import Users from './screens/User'; import WorkflowApprovals from './screens/WorkflowApproval'; +import { Jobs } from './screens/Job'; // Ideally, this should just be a regular object that we export, but we // need the i18n. When lingui3 arrives, we will be able to import i18n @@ -138,6 +139,11 @@ function getRouteConfig(i18n) { path: '/applications', screen: Applications, }, + { + title: i18n._(t`Execution environments`), + path: '/execution_environments', + screen: ExecutionEnvironments, + }, ], }, { diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx new file mode 100644 index 0000000000..9575a3b568 --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Route, Redirect, Switch } from 'react-router-dom'; + +import ExecutionEnvironmentDetails from './ExecutionEnvironmentDetails'; +import ExecutionEnvironmentEdit from './ExecutionEnvironmentEdit'; + +function ExecutionEnvironment() { + return ( + + + + + + + + + + ); +} + +export default ExecutionEnvironment; diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.jsx new file mode 100644 index 0000000000..e188990878 --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Card, PageSection } from '@patternfly/react-core'; + +function ExecutionEnvironmentAdd() { + return ( + + +
Add Execution Environments
+
+
+ ); +} + +export default ExecutionEnvironmentAdd; diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/index.js b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/index.js new file mode 100644 index 0000000000..69765fcf3b --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/index.js @@ -0,0 +1 @@ +export { default } from './ExecutionEnvironmentAdd'; diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx new file mode 100644 index 0000000000..f6902d1735 --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Card, PageSection } from '@patternfly/react-core'; + +function ExecutionEnvironmentDetails() { + return ( + + +
Execution environments details
+
+
+ ); +} + +export default ExecutionEnvironmentDetails; diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/index.js b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/index.js new file mode 100644 index 0000000000..36121ea0d9 --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/index.js @@ -0,0 +1 @@ +export { default } from './ExecutionEnvironmentDetails'; diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx new file mode 100644 index 0000000000..91e3096ce3 --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Card, PageSection } from '@patternfly/react-core'; + +function ExecutionEnvironmentEdit() { + return ( + + +
Edit Execution environments
+
+
+ ); +} + +export default ExecutionEnvironmentEdit; diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/index.js b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/index.js new file mode 100644 index 0000000000..6ab135ca05 --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/index.js @@ -0,0 +1 @@ +export { default } from './ExecutionEnvironmentEdit'; diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx new file mode 100644 index 0000000000..96d2d07e10 --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Card, PageSection } from '@patternfly/react-core'; + +function ExecutionEnvironmentList() { + return ( + + +
List Execution environments
+
+
+ ); +} + +export default ExecutionEnvironmentList; diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/index.js b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/index.js new file mode 100644 index 0000000000..a8aa4263d7 --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/index.js @@ -0,0 +1 @@ +export { default } from './ExecutionEnvironmentList'; diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.jsx new file mode 100644 index 0000000000..7db470e2f7 --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.jsx @@ -0,0 +1,53 @@ +import React, { useState, useCallback } from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Route, Switch } from 'react-router-dom'; + +import ExecutionEnvironment from './ExecutionEnvironment'; +import ExecutionEnvironmentAdd from './ExecutionEnvironmentAdd'; +import ExecutionEnvironmentList from './ExecutionEnvironmentList'; +import Breadcrumbs from '../../components/Breadcrumbs'; + +function ExecutionEnvironments({ i18n }) { + const [breadcrumbConfig, setBreadcrumbConfig] = useState({ + '/execution_environments': i18n._(t`Execution environments`), + '/execution_environments/add': i18n._(t`Create Execution environments`), + }); + + const buildBreadcrumbConfig = useCallback( + executionEnvironments => { + if (!executionEnvironments) { + return; + } + setBreadcrumbConfig({ + '/execution_environments': i18n._(t`Execution environments`), + '/execution_environments/add': i18n._(t`Create Execution environments`), + [`/execution_environments/${executionEnvironments.id}`]: `${executionEnvironments.name}`, + [`/execution_environments/${executionEnvironments.id}/edit`]: i18n._( + t`Edit details` + ), + [`/execution_environments/${executionEnvironments.id}/details`]: i18n._( + t`Details` + ), + }); + }, + [i18n] + ); + return ( + <> + + + + + + + + + + + + + + ); +} +export default withI18n()(ExecutionEnvironments); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.test.jsx new file mode 100644 index 0000000000..5ceb36ac93 --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.test.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; + +import ExecutionEnvironments from './ExecutionEnvironments'; + +describe('', () => { + let pageWrapper; + let pageSections; + + beforeEach(() => { + pageWrapper = mountWithContexts(); + pageSections = pageWrapper.find('PageSection'); + }); + + afterEach(() => { + pageWrapper.unmount(); + }); + + test('initially renders without crashing', () => { + expect(pageWrapper.length).toBe(1); + expect(pageSections.length).toBe(1); + expect(pageSections.first().props().variant).toBe('light'); + }); +}); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/index.js b/awx/ui_next/src/screens/ExecutionEnvironment/index.js new file mode 100644 index 0000000000..f66a2b3cf3 --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/index.js @@ -0,0 +1 @@ +export { default } from './ExecutionEnvironments'; From 06d7a61ca11242f63a7d7cb8e7c108f35bde1b32 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Fri, 31 Jul 2020 09:43:46 -0400 Subject: [PATCH 016/178] Initial EE integration --- awx/main/tasks.py | 49 ++++++++++++++++--- awx/settings/defaults.py | 2 + requirements/requirements.txt | 2 +- requirements/requirements_git.txt | 1 + .../roles/dockerfile/templates/Dockerfile.j2 | 8 +++ .../sources/templates/docker-compose.yml.j2 | 1 + tools/docker-compose/entrypoint.sh | 18 +++++-- 7 files changed, 68 insertions(+), 13 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index b6ab905837..16f79ee5ef 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -887,6 +887,9 @@ class BaseTask(object): ''' return os.path.abspath(os.path.join(os.path.dirname(__file__), *args)) + def build_execution_environment_params(self, instance): + return {} + def build_private_data(self, instance, private_data_dir): ''' Return SSH private key data (only if stored in DB as ssh_key_data). @@ -1129,12 +1132,13 @@ class BaseTask(object): for hostname, hv in script_data.get('_meta', {}).get('hostvars', {}).items() } json_data = json.dumps(script_data) - handle, path = tempfile.mkstemp(dir=private_data_dir) - f = os.fdopen(handle, 'w') - f.write('#! /usr/bin/env python\n# -*- coding: utf-8 -*-\nprint(%r)\n' % json_data) - f.close() - os.chmod(path, stat.S_IRUSR | stat.S_IXUSR | stat.S_IWUSR) - return path + path = os.path.join(private_data_dir, 'inventory') + os.makedirs(path, mode=0o700) + fn = os.path.join(path, 'hosts') + with open(fn, 'w') as f: + os.chmod(fn, stat.S_IRUSR | stat.S_IXUSR | stat.S_IWUSR) + f.write('#! /usr/bin/env python3\n# -*- coding: utf-8 -*-\nprint(%r)\n' % json_data) + return fn def build_args(self, instance, private_data_dir, passwords): raise NotImplementedError @@ -1429,6 +1433,7 @@ class BaseTask(object): process_isolation_params = self.build_params_process_isolation(self.instance, private_data_dir, cwd) + execution_environment_params = self.build_execution_environment_params(self.instance) env = self.build_env(self.instance, private_data_dir, isolated, private_data_files=private_data_files) self.safe_env = build_safe_env(env) @@ -1463,7 +1468,8 @@ class BaseTask(object): 'settings': { 'job_timeout': self.get_instance_timeout(self.instance), 'suppress_ansible_output': True, - **process_isolation_params, + #**process_isolation_params, + **execution_environment_params, **resource_profiling_params, }, } @@ -2003,6 +2009,14 @@ class RunJob(BaseTask): if inventory is not None: update_inventory_computed_fields.delay(inventory.id) + def build_execution_environment_params(self, instance): + execution_environment_params = { + "container_image": settings.AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE, + "process_isolation": True + } + return execution_environment_params + + @task(queue=get_local_queuename) class RunProjectUpdate(BaseTask): @@ -2349,7 +2363,7 @@ class RunProjectUpdate(BaseTask): # the project update playbook is not in a git repo, but uses a vendoring directory # to be consistent with the ansible-runner model, - # that is moved into the runner projecct folder here + # that is moved into the runner project folder here awx_playbooks = self.get_path_to('..', 'playbooks') copy_tree(awx_playbooks, os.path.join(private_data_dir, 'project')) @@ -2484,6 +2498,18 @@ class RunProjectUpdate(BaseTask): ''' return getattr(settings, 'AWX_PROOT_ENABLED', False) + def build_execution_environment_params(self, instance): + project_path = os.path.dirname(instance.get_project_path(check_if_exists=False)) + execution_environment_params = { + "process_isolation": True, + "container_image": settings.AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE, + "container_volume_mounts": [ + f"{project_path}:{project_path}", + ] + + } + return execution_environment_params + @task(queue=get_local_queuename) class RunInventoryUpdate(BaseTask): @@ -2983,6 +3009,13 @@ class RunAdHocCommand(BaseTask): if isolated_manager_instance: isolated_manager_instance.cleanup() + def build_execution_environment_params(self, instance): + execution_environment_params = { + "container_image": settings.AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE, + "process_isolation": True + } + return execution_environment_params + @task(queue=get_local_queuename) class RunSystemJob(BaseTask): diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index e0c1db197b..849d6220bc 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -65,6 +65,8 @@ AWX_CONTAINER_GROUP_POD_LAUNCH_RETRY_DELAY = 5 AWX_CONTAINER_GROUP_DEFAULT_NAMESPACE = 'default' AWX_CONTAINER_GROUP_DEFAULT_IMAGE = 'ansible/ansible-runner' +AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE = 'quay.io/ansible/ansible-runner:devel' + # Internationalization # https://docs.djangoproject.com/en/dev/topics/i18n/ # diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 5829401c78..a6901fd3ef 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,7 +1,7 @@ adal==1.2.2 # via msrestazure aiohttp==3.6.2 # via -r /awx_devel/requirements/requirements.in aioredis==1.3.1 # via channels-redis -ansible-runner==1.4.7 # via -r /awx_devel/requirements/requirements.in +# ansible-runner==1.4.7 # via -r /awx_devel/requirements/requirements.in ansiconv==1.0.0 # via -r /awx_devel/requirements/requirements.in asciichartpy==1.5.25 # via -r /awx_devel/requirements/requirements.in asgiref==3.2.5 # via channels, channels-redis, daphne diff --git a/requirements/requirements_git.txt b/requirements/requirements_git.txt index 340cbfdcc7..04eac859a4 100644 --- a/requirements/requirements_git.txt +++ b/requirements/requirements_git.txt @@ -1 +1,2 @@ git+https://github.com/ansible/system-certifi.git@devel#egg=certifi +git+git://github.com/ansible/ansible-runner@devel#egg=ansible-runner diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index a03ac7d238..e056b10fb1 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -103,6 +103,7 @@ RUN dnf -y update && \ krb5-workstation \ libcgroup-tools \ nginx \ + podman \ @postgresql:12 \ python3-devel \ python3-libselinux \ @@ -216,6 +217,7 @@ RUN for dir in \ /var/lib/awx \ /var/lib/awx/rsyslog \ /var/lib/awx/rsyslog/conf.d \ + /var/lib/awx/.local/share/containers/storage \ /var/run/awx-rsyslog \ /var/log/tower \ /var/log/nginx \ @@ -225,6 +227,8 @@ RUN for dir in \ /var/lib/nginx ; \ do mkdir -m 0775 -p $dir ; chmod g+rw $dir ; chgrp root $dir ; done && \ for file in \ + /etc/subuid \ + /etc/subgid \ /etc/passwd \ /var/lib/awx/rsyslog/rsyslog.conf ; \ do touch $file ; chmod g+rw $file ; chgrp root $file ; done @@ -255,6 +259,8 @@ RUN ln -sf /dev/stdout /var/log/nginx/access.log && \ ln -sf /dev/stderr /var/log/nginx/error.log {% endif %} +RUN echo -e 'cgroup_manager = "cgroupfs"\nevents_logger = "file"' > /etc/containers/libpod.conf + ENV HOME="/var/lib/awx" ENV PATH="/usr/pgsql-10/bin:${PATH}" @@ -272,3 +278,5 @@ ENTRYPOINT ["/usr/bin/tini", "--"] CMD /usr/bin/launch_awx.sh VOLUME /var/lib/nginx {% endif %} + +VOLUME /var/lib/awx/.local/share/containers/storage diff --git a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 index 0cdf4f6728..754bc803f9 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 @@ -33,6 +33,7 @@ services: - "../../docker-compose/_sources/local_settings.py:/etc/tower/conf.d/local_settings.py" - "../../docker-compose/_sources/SECRET_KEY:/etc/tower/SECRET_KEY" - "redis_socket:/var/run/redis/:rw" + - "/sys/fs/cgroup:/sys/fs/cgroup" privileged: true tty: true # A useful container that simply passes through log messages to the console diff --git a/tools/docker-compose/entrypoint.sh b/tools/docker-compose/entrypoint.sh index 8ed9bf2abd..13c858b441 100755 --- a/tools/docker-compose/entrypoint.sh +++ b/tools/docker-compose/entrypoint.sh @@ -2,13 +2,23 @@ if [ `id -u` -ge 500 ] || [ -z "${CURRENT_UID}" ]; then - cat << EOF > /tmp/passwd +cat << EOF > /etc/passwd root:x:0:0:root:/root:/bin/bash -awx:x:`id -u`:`id -g`:,,,:/tmp:/bin/bash +awx:x:`id -u`:`id -g`:,,,:/var/lib/awx:/bin/bash +EOF + +cat < /etc/subuid +awx:100000:50001 +EOF + +cat < /etc/subgid +awx:100000:50001 EOF - cat /tmp/passwd > /etc/passwd - rm /tmp/passwd fi +# Required to get rootless podman working after +# writing out the sub*id files above +podman system migrate + exec $@ From 130bf076f468a60eec53d2f65c74e322042a10fc Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Mon, 24 Aug 2020 16:02:11 -0400 Subject: [PATCH 017/178] Add Z to volume mount Set ansible-runner back to main fork due to merge --- awx/main/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 16f79ee5ef..cdac35737d 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -2504,7 +2504,7 @@ class RunProjectUpdate(BaseTask): "process_isolation": True, "container_image": settings.AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE, "container_volume_mounts": [ - f"{project_path}:{project_path}", + f"{project_path}:{project_path}:Z", ] } From a3f0158a943f063296a24368427706fcff47e880 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Mon, 24 Aug 2020 16:02:11 -0400 Subject: [PATCH 018/178] Add Z to volume mount Update to AWX execution environment use the special 2.9 container image revert setting back for merge Fix another permission error by mapping 2 folders also create folders before running --- awx/main/tasks.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index cdac35737d..ed05c61828 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -2344,10 +2344,14 @@ class RunProjectUpdate(BaseTask): # re-create root project folder if a natural disaster has destroyed it if not os.path.exists(settings.PROJECTS_ROOT): os.mkdir(settings.PROJECTS_ROOT) + project_path = instance.project.get_project_path(check_if_exists=False) + if not os.path.exists(project_path): + os.makedirs(project_path) # used as container mount + self.acquire_lock(instance) + self.original_branch = None if instance.scm_type == 'git' and instance.branch_override: - project_path = instance.project.get_project_path(check_if_exists=False) if os.path.exists(project_path): git_repo = git.Repo(project_path) if git_repo.head.is_detached: @@ -2499,12 +2503,14 @@ class RunProjectUpdate(BaseTask): return getattr(settings, 'AWX_PROOT_ENABLED', False) def build_execution_environment_params(self, instance): - project_path = os.path.dirname(instance.get_project_path(check_if_exists=False)) + project_path = instance.get_project_path(check_if_exists=False) + cache_path = instance.get_cache_path() execution_environment_params = { "process_isolation": True, "container_image": settings.AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE, "container_volume_mounts": [ f"{project_path}:{project_path}:Z", + f"{cache_path}:{cache_path}:Z", ] } From 50433789ae505a20345c31683e02d5c7839e9b57 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Fri, 28 Aug 2020 22:16:34 -0400 Subject: [PATCH 019/178] Purge environment variables to work with ansible-runner changes Remove inventory scripts show because they no longer exist Remove reference to non-existent callback directory Remove more references to removed paths --- awx/main/tasks.py | 78 +++++++++++++++-------------------------------- 1 file changed, 25 insertions(+), 53 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index ed05c61828..b773031e10 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -74,7 +74,6 @@ from awx.main.utils import (update_scm_url, ignore_inventory_group_removal, extract_ansible_vars, schedule_task_manager, get_awx_version) from awx.main.utils.ansible import read_ansible_config -from awx.main.utils.common import get_custom_venv_choices from awx.main.utils.external_logging import reconfigure_rsyslog from awx.main.utils.safe_yaml import safe_dump, sanitize_jinja from awx.main.utils.reload import stop_local_services @@ -1066,30 +1065,18 @@ class BaseTask(object): os.chmod(path, stat.S_IRUSR) return path - def add_ansible_venv(self, venv_path, env, isolated=False): - env['VIRTUAL_ENV'] = venv_path - env['PATH'] = os.path.join(venv_path, "bin") + ":" + env['PATH'] - venv_libdir = os.path.join(venv_path, "lib") - - if not isolated and ( - not os.path.exists(venv_libdir) or - os.path.join(venv_path, '') not in get_custom_venv_choices() - ): - raise InvalidVirtualenvError(_( - 'Invalid virtual environment selected: {}'.format(venv_path) - )) - - isolated_manager.set_pythonpath(venv_libdir, env) - def add_awx_venv(self, env): env['VIRTUAL_ENV'] = settings.AWX_VENV_PATH - env['PATH'] = os.path.join(settings.AWX_VENV_PATH, "bin") + ":" + env['PATH'] + if 'PATH' in env: + env['PATH'] = os.path.join(settings.AWX_VENV_PATH, "bin") + ":" + env['PATH'] + else: + env['PATH'] = os.path.join(settings.AWX_VENV_PATH, "bin") def build_env(self, instance, private_data_dir, isolated, private_data_files=None): ''' Build environment dictionary for ansible-playbook. ''' - env = dict(os.environ.items()) + env = {} # Add ANSIBLE_* settings to the subprocess environment. for attr in dir(settings): if attr == attr.upper() and attr.startswith('ANSIBLE_'): @@ -1097,14 +1084,6 @@ class BaseTask(object): # Also set environment variables configured in AWX_TASK_ENV setting. for key, value in settings.AWX_TASK_ENV.items(): env[key] = str(value) - # Set environment variables needed for inventory and job event - # callbacks to work. - # Update PYTHONPATH to use local site-packages. - # NOTE: - # Derived class should call add_ansible_venv() or add_awx_venv() - if self.should_use_proot(instance): - env['PROOT_TMP_DIR'] = settings.AWX_PROOT_BASE_PATH - env['AWX_PRIVATE_DATA_DIR'] = private_data_dir return env def should_use_resource_profiling(self, job): @@ -1696,7 +1675,6 @@ class RunJob(BaseTask): private_data_files=private_data_files) if private_data_files is None: private_data_files = {} - self.add_ansible_venv(job.ansible_virtualenv_path, env, isolated=isolated) # Set environment variables needed for inventory and job event # callbacks to work. env['JOB_ID'] = str(job.pk) @@ -1715,7 +1693,8 @@ class RunJob(BaseTask): cp_dir = os.path.join(private_data_dir, 'cp') if not os.path.exists(cp_dir): os.mkdir(cp_dir, 0o700) - env['ANSIBLE_SSH_CONTROL_PATH_DIR'] = cp_dir + # FIXME: more elegant way to manage this path in container + env['ANSIBLE_SSH_CONTROL_PATH_DIR'] = '/runner/cp' # Set environment variables for cloud credentials. cred_files = private_data_files.get('credentials', {}) @@ -1752,7 +1731,8 @@ class RunJob(BaseTask): for path in config_values[config_setting].split(':'): if path not in paths: paths = [config_values[config_setting]] + paths - paths = [os.path.join(private_data_dir, folder)] + paths + # FIXME: again, figure out more elegant way for inside container + paths = [os.path.join('/runner', folder)] + paths env[env_key] = os.pathsep.join(paths) return env @@ -2082,7 +2062,6 @@ class RunProjectUpdate(BaseTask): env = super(RunProjectUpdate, self).build_env(project_update, private_data_dir, isolated=isolated, private_data_files=private_data_files) - self.add_ansible_venv(settings.ANSIBLE_VENV_PATH, env) env['ANSIBLE_RETRY_FILES_ENABLED'] = str(False) env['ANSIBLE_ASK_PASS'] = str(False) env['ANSIBLE_BECOME_ASK_PASS'] = str(False) @@ -2524,18 +2503,6 @@ class RunInventoryUpdate(BaseTask): event_model = InventoryUpdateEvent event_data_key = 'inventory_update_id' - # TODO: remove once inv updates run in containers - def should_use_proot(self, inventory_update): - ''' - Return whether this task should use proot. - ''' - return getattr(settings, 'AWX_PROOT_ENABLED', False) - - # TODO: remove once inv updates run in containers - @property - def proot_show_paths(self): - return [settings.AWX_ANSIBLE_COLLECTIONS_PATHS] - def build_private_data(self, inventory_update, private_data_dir): """ Return private data needed for inventory update. @@ -2562,17 +2529,18 @@ class RunInventoryUpdate(BaseTask): are accomplished by the inventory source injectors (in this method) or custom credential type injectors (in main run method). """ - env = super(RunInventoryUpdate, self).build_env(inventory_update, + base_env = super(RunInventoryUpdate, self).build_env(inventory_update, private_data_dir, isolated, private_data_files=private_data_files) + # TODO: this is able to run by turning off isolation + # the goal is to run it a container instead + env = dict(os.environ.items()) + env.update(base_env) + if private_data_files is None: private_data_files = {} - # TODO: remove once containers replace custom venvs - self.add_ansible_venv(inventory_update.ansible_virtualenv_path, env, isolated=isolated) - - # Legacy environment variables, were used as signal to awx-manage command - # now they are provided in case some scripts may be relying on them + # Pass inventory source ID to inventory script. env['INVENTORY_SOURCE_ID'] = str(inventory_update.inventory_source_id) env['INVENTORY_UPDATE_ID'] = str(inventory_update.pk) env.update(STANDARD_INVENTORY_UPDATE_ENV) @@ -2610,7 +2578,8 @@ class RunInventoryUpdate(BaseTask): for path in config_values[config_setting].split(':'): if path not in paths: paths = [config_values[config_setting]] + paths - paths = [os.path.join(private_data_dir, folder)] + paths + # FIXME: containers + paths = [os.path.join('/runner', folder)] + paths env[env_key] = os.pathsep.join(paths) return env @@ -2885,7 +2854,6 @@ class RunAdHocCommand(BaseTask): env = super(RunAdHocCommand, self).build_env(ad_hoc_command, private_data_dir, isolated=isolated, private_data_files=private_data_files) - self.add_ansible_venv(settings.ANSIBLE_VENV_PATH, env) # Set environment variables needed for inventory and ad hoc event # callbacks to work. env['AD_HOC_COMMAND_ID'] = str(ad_hoc_command.pk) @@ -2899,7 +2867,8 @@ class RunAdHocCommand(BaseTask): cp_dir = os.path.join(private_data_dir, 'cp') if not os.path.exists(cp_dir): os.mkdir(cp_dir, 0o700) - env['ANSIBLE_SSH_CONTROL_PATH'] = cp_dir + # FIXME: more elegant way to manage this path in container + env['ANSIBLE_SSH_CONTROL_PATH'] = '/runner/cp' return env @@ -3061,10 +3030,13 @@ class RunSystemJob(BaseTask): return path def build_env(self, instance, private_data_dir, isolated=False, private_data_files=None): - env = super(RunSystemJob, self).build_env(instance, private_data_dir, + base_env = super(RunSystemJob, self).build_env(instance, private_data_dir, isolated=isolated, private_data_files=private_data_files) - self.add_awx_venv(env) + # TODO: this is able to run by turning off isolation + # the goal is to run it a container instead + env = dict(os.environ.items()) + env.update(base_env) return env def build_cwd(self, instance, private_data_dir): From 6e2010ca402d31a2ab55c414e4dfbfa063e342c4 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Wed, 2 Sep 2020 16:43:44 -0400 Subject: [PATCH 020/178] Respect user proot show paths when using containers --- awx/main/tasks.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index b773031e10..b929c231b1 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -887,7 +887,12 @@ class BaseTask(object): return os.path.abspath(os.path.join(os.path.dirname(__file__), *args)) def build_execution_environment_params(self, instance): - return {} + params = {} + if settings.AWX_PROOT_SHOW_PATHS: + params['container_volume_mounts'] = [] + for this_path in settings.AWX_PROOT_SHOW_PATHS: + params['container_volume_mounts'].append(f'{this_path}:{this_path}:Z') + return params def build_private_data(self, instance, private_data_dir): ''' @@ -1990,11 +1995,12 @@ class RunJob(BaseTask): update_inventory_computed_fields.delay(inventory.id) def build_execution_environment_params(self, instance): - execution_environment_params = { + params = super(RunJob, self).build_execution_environment_params(instance) + params.update({ "container_image": settings.AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE, "process_isolation": True - } - return execution_environment_params + }) + return params @@ -2482,18 +2488,17 @@ class RunProjectUpdate(BaseTask): return getattr(settings, 'AWX_PROOT_ENABLED', False) def build_execution_environment_params(self, instance): + params = super(RunProjectUpdate, self).build_execution_environment_params(instance) project_path = instance.get_project_path(check_if_exists=False) cache_path = instance.get_cache_path() - execution_environment_params = { - "process_isolation": True, - "container_image": settings.AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE, - "container_volume_mounts": [ - f"{project_path}:{project_path}:Z", - f"{cache_path}:{cache_path}:Z", - ] - - } - return execution_environment_params + params['process_isolation'] = True + params['container_image'] = settings.AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE + params.setdefault('container_volume_mounts', []) + params['container_volume_mounts'].extend([ + f"{project_path}:{project_path}:Z", + f"{cache_path}:{cache_path}:Z", + ]) + return params @task(queue=get_local_queuename) From 73418e41f30ae3d4cd26e6622dc439fb543f1d9c Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Thu, 3 Sep 2020 11:02:08 -0400 Subject: [PATCH 021/178] Fix pathing issue with custom credentials also fix some minor flake8 issues --- awx/main/models/credential/__init__.py | 7 ++++++- awx/main/tasks.py | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index e8a2884083..15afd55018 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -564,7 +564,12 @@ class CredentialType(CommonModelNameNotUnique): if extra_vars: path = build_extra_vars_file(extra_vars, private_data_dir) - args.extend(['-e', '@%s' % path]) + # FIXME: develop some better means of referencing paths inside containers + container_path = os.path.join( + '/runner', + os.path.basename(path) + ) + args.extend(['-e', '@%s' % container_path]) class ManagedCredentialType(SimpleNamespace): diff --git a/awx/main/tasks.py b/awx/main/tasks.py index b929c231b1..dcd752f72c 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1414,9 +1414,10 @@ class BaseTask(object): cwd = self.build_cwd(self.instance, private_data_dir) resource_profiling_params = self.build_params_resource_profiling(self.instance, private_data_dir) - process_isolation_params = self.build_params_process_isolation(self.instance, - private_data_dir, - cwd) + # TODO: Remove if fully replaced with containerized runs + # process_isolation_params = self.build_params_process_isolation(self.instance, + # private_data_dir, + # cwd) execution_environment_params = self.build_execution_environment_params(self.instance) env = self.build_env(self.instance, private_data_dir, isolated, private_data_files=private_data_files) @@ -2534,10 +2535,9 @@ class RunInventoryUpdate(BaseTask): are accomplished by the inventory source injectors (in this method) or custom credential type injectors (in main run method). """ - base_env = super(RunInventoryUpdate, self).build_env(inventory_update, - private_data_dir, - isolated, - private_data_files=private_data_files) + base_env = super(RunInventoryUpdate, self).build_env( + inventory_update, private_data_dir, isolated, + private_data_files=private_data_files) # TODO: this is able to run by turning off isolation # the goal is to run it a container instead env = dict(os.environ.items()) @@ -3035,9 +3035,9 @@ class RunSystemJob(BaseTask): return path def build_env(self, instance, private_data_dir, isolated=False, private_data_files=None): - base_env = super(RunSystemJob, self).build_env(instance, private_data_dir, - isolated=isolated, - private_data_files=private_data_files) + base_env = super(RunSystemJob, self).build_env( + instance, private_data_dir, isolated=isolated, + private_data_files=private_data_files) # TODO: this is able to run by turning off isolation # the goal is to run it a container instead env = dict(os.environ.items()) From 64f45da4d27e3ed6d22978ec2b4193cf277870d7 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Thu, 3 Sep 2020 15:48:10 -0400 Subject: [PATCH 022/178] Fix pathing issue for credential file references --- awx/main/models/credential/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index 15afd55018..ebab3bc22f 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -528,15 +528,20 @@ class CredentialType(CommonModelNameNotUnique): with open(path, 'w') as f: f.write(data) os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) + # FIXME: develop some better means of referencing paths inside containers + container_path = os.path.join( + '/runner', + os.path.basename(path) + ) # determine if filename indicates single file or many if file_label.find('.') == -1: - tower_namespace.filename = path + tower_namespace.filename = container_path else: if not hasattr(tower_namespace, 'filename'): tower_namespace.filename = TowerNamespace() file_label = file_label.split('.')[1] - setattr(tower_namespace.filename, file_label, path) + setattr(tower_namespace.filename, file_label, container_path) injector_field = self._meta.get_field('injectors') for env_var, tmpl in self.injectors.get('env', {}).items(): From 9660e27246bb4edf61414bd7dcbbbea76d4f9f1d Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Fri, 4 Sep 2020 07:50:21 -0400 Subject: [PATCH 023/178] Fix project folder deletion Fix another absolute path reference in containers --- awx/main/tasks.py | 4 +++- awx/playbooks/project_update.yml | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index dcd752f72c..c81900c9c6 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1038,6 +1038,8 @@ class BaseTask(object): results_dir = os.path.join(private_data_dir, 'artifacts/playbook_profiling') if not os.path.isdir(results_dir): os.makedirs(results_dir, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC) + # FIXME: develop some better means of referencing paths inside containers + container_results_dir = os.path.join('/runner', 'artifacts/playbook_profiling') logger.debug('Collected the following resource profiling intervals: cpu: {} mem: {} pid: {}' .format(cpu_poll_interval, mem_poll_interval, pid_poll_interval)) @@ -1047,7 +1049,7 @@ class BaseTask(object): 'resource_profiling_cpu_poll_interval': cpu_poll_interval, 'resource_profiling_memory_poll_interval': mem_poll_interval, 'resource_profiling_pid_poll_interval': pid_poll_interval, - 'resource_profiling_results_dir': results_dir}) + 'resource_profiling_results_dir': container_results_dir}) return resource_profiling_params diff --git a/awx/playbooks/project_update.yml b/awx/playbooks/project_update.yml index a7b7007d56..49618909fb 100644 --- a/awx/playbooks/project_update.yml +++ b/awx/playbooks/project_update.yml @@ -24,9 +24,7 @@ tasks: - name: delete project directory before update - file: - path: "{{project_path|quote}}" - state: absent + command: "rm -rf {{project_path}}/*" # volume mounted, cannot delete folder itself tags: - delete From 332c802317da54e7ca3615f867f3ce00b9b02601 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Tue, 8 Sep 2020 10:30:10 -0400 Subject: [PATCH 024/178] Deal with missing HOME env var --- awx/main/tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index c81900c9c6..b95015dc5c 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1463,7 +1463,8 @@ class BaseTask(object): if containerized: # We don't want HOME passed through to container groups. - params['envvars'].pop('HOME') + # TODO: remove this conditional after everything is containerized + params['envvars'].pop('HOME', None) if isinstance(self.instance, AdHocCommand): params['module'] = self.build_module_name(self.instance) From 9d806ddb8202dde389f98466e555e0964a473c82 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Wed, 9 Sep 2020 09:05:23 -0400 Subject: [PATCH 025/178] Initial minimal hooking up of JT EEs to jobs --- awx/main/models/jobs.py | 2 +- awx/main/tasks.py | 37 ++++++++++++++++++------------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 638954e53c..31f4784962 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -284,7 +284,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour def _get_unified_job_field_names(cls): return set(f.name for f in JobOptions._meta.fields) | set( ['name', 'description', 'organization', 'survey_passwords', 'labels', 'credentials', - 'job_slice_number', 'job_slice_count'] + 'job_slice_number', 'job_slice_count', 'execution_environment'] ) @property diff --git a/awx/main/tasks.py b/awx/main/tasks.py index b95015dc5c..50230c0e98 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -887,7 +887,18 @@ class BaseTask(object): return os.path.abspath(os.path.join(os.path.dirname(__file__), *args)) def build_execution_environment_params(self, instance): - params = {} + if getattr(instance, 'execution_environment', None): + # TODO: process heirarchy, JT-project-org, maybe here + # or maybe in create_unified_job + logger.info('using custom image {}'.format(instance.execution_environment.image)) + image = instance.execution_environment.image + else: + logger.info('using default image') + image = settings.AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE + params = { + "container_image": image, + "process_isolation": True + } if settings.AWX_PROOT_SHOW_PATHS: params['container_volume_mounts'] = [] for this_path in settings.AWX_PROOT_SHOW_PATHS: @@ -1998,15 +2009,6 @@ class RunJob(BaseTask): if inventory is not None: update_inventory_computed_fields.delay(inventory.id) - def build_execution_environment_params(self, instance): - params = super(RunJob, self).build_execution_environment_params(instance) - params.update({ - "container_image": settings.AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE, - "process_isolation": True - }) - return params - - @task(queue=get_local_queuename) class RunProjectUpdate(BaseTask): @@ -2495,8 +2497,6 @@ class RunProjectUpdate(BaseTask): params = super(RunProjectUpdate, self).build_execution_environment_params(instance) project_path = instance.get_project_path(check_if_exists=False) cache_path = instance.get_cache_path() - params['process_isolation'] = True - params['container_image'] = settings.AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE params.setdefault('container_volume_mounts', []) params['container_volume_mounts'].extend([ f"{project_path}:{project_path}:Z", @@ -2531,6 +2531,9 @@ class RunInventoryUpdate(BaseTask): injector = InventorySource.injectors[inventory_update.source]() return injector.build_private_data(inventory_update, private_data_dir) + def build_execution_environment_params(self, inventory_update): + return {} # TODO: containerize inventory updates + def build_env(self, inventory_update, private_data_dir, isolated, private_data_files=None): """Build environment dictionary for ansible-inventory. @@ -2992,13 +2995,6 @@ class RunAdHocCommand(BaseTask): if isolated_manager_instance: isolated_manager_instance.cleanup() - def build_execution_environment_params(self, instance): - execution_environment_params = { - "container_image": settings.AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE, - "process_isolation": True - } - return execution_environment_params - @task(queue=get_local_queuename) class RunSystemJob(BaseTask): @@ -3007,6 +3003,9 @@ class RunSystemJob(BaseTask): event_model = SystemJobEvent event_data_key = 'system_job_id' + def build_execution_environment_params(self, system_job): + return {} + def build_args(self, system_job, private_data_dir, passwords): args = ['awx-manage', system_job.job_type] try: From b7209d16940594d594f3129047f9fcad9612f3df Mon Sep 17 00:00:00 2001 From: Kersom <9053044+nixocio@users.noreply.github.com> Date: Tue, 15 Sep 2020 20:25:23 -0400 Subject: [PATCH 026/178] Add list Execution Environments (#8148) See: https://github.com/ansible/awx/issues/7886 --- awx/ui_next/src/api/index.js | 3 + .../src/api/models/ExecutionEnvironments.js | 10 + .../ExecutionEnviromentList.test.jsx | 103 +++++++++ .../ExecutionEnvironmentList.jsx | 206 +++++++++++++++++- .../ExecutionEnvironmentListItem.jsx | 85 ++++++++ .../ExecutionEnvironmentListItem.test.jsx | 52 +++++ awx/ui_next/src/types.js | 10 + 7 files changed, 461 insertions(+), 8 deletions(-) create mode 100644 awx/ui_next/src/api/models/ExecutionEnvironments.js create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.test.jsx diff --git a/awx/ui_next/src/api/index.js b/awx/ui_next/src/api/index.js index 3160ebd907..d048237b74 100644 --- a/awx/ui_next/src/api/index.js +++ b/awx/ui_next/src/api/index.js @@ -7,6 +7,7 @@ import CredentialInputSources from './models/CredentialInputSources'; import CredentialTypes from './models/CredentialTypes'; import Credentials from './models/Credentials'; import Dashboard from './models/Dashboard'; +import ExecutionEnvironments from './models/ExecutionEnvironments'; import Groups from './models/Groups'; import Hosts from './models/Hosts'; import InstanceGroups from './models/InstanceGroups'; @@ -50,6 +51,7 @@ const CredentialInputSourcesAPI = new CredentialInputSources(); const CredentialTypesAPI = new CredentialTypes(); const CredentialsAPI = new Credentials(); const DashboardAPI = new Dashboard(); +const ExecutionEnvironmentsAPI = new ExecutionEnvironments(); const GroupsAPI = new Groups(); const HostsAPI = new Hosts(); const InstanceGroupsAPI = new InstanceGroups(); @@ -94,6 +96,7 @@ export { CredentialTypesAPI, CredentialsAPI, DashboardAPI, + ExecutionEnvironmentsAPI, GroupsAPI, HostsAPI, InstanceGroupsAPI, diff --git a/awx/ui_next/src/api/models/ExecutionEnvironments.js b/awx/ui_next/src/api/models/ExecutionEnvironments.js new file mode 100644 index 0000000000..2df933d53a --- /dev/null +++ b/awx/ui_next/src/api/models/ExecutionEnvironments.js @@ -0,0 +1,10 @@ +import Base from '../Base'; + +class ExecutionEnvironments extends Base { + constructor(http) { + super(http); + this.baseUrl = '/api/v2/execution_environments/'; + } +} + +export default ExecutionEnvironments; diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx new file mode 100644 index 0000000000..8b04233404 --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx @@ -0,0 +1,103 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; + +import { + mountWithContexts, + waitForElement, +} from '../../../../testUtils/enzymeHelpers'; + +import { ExecutionEnvironmentsAPI } from '../../../api'; +import ExecutionEnvironmentList from './ExecutionEnvironmentList'; + +jest.mock('../../../api/models/ExecutionEnvironments'); + +const executionEnvironments = { + data: { + results: [ + { + id: 1, + image: 'https://registry.com/r/image/manifest', + organization: null, + credential: null, + }, + { + id: 2, + image: 'https://registry.com/r/image2/manifest', + organization: null, + credential: null, + }, + ], + count: 2, + }, +}; + +const options = { data: { actions: { POST: true } } }; + +describe('', () => { + let wrapper; + + test('should mount successfully', async () => { + await act(async () => { + wrapper = mountWithContexts(); + }); + await waitForElement( + wrapper, + 'ExecutionEnvironmentList', + el => el.length > 0 + ); + }); + + test('should have data fetched and render 2 rows', async () => { + ExecutionEnvironmentsAPI.read.mockResolvedValue(executionEnvironments); + ExecutionEnvironmentsAPI.readOptions.mockResolvedValue(options); + + await act(async () => { + wrapper = mountWithContexts(); + }); + await waitForElement( + wrapper, + 'ExecutionEnvironmentList', + el => el.length > 0 + ); + + expect(wrapper.find('ExecutionEnvironmentListItem').length).toBe(2); + expect(ExecutionEnvironmentsAPI.read).toBeCalled(); + expect(ExecutionEnvironmentsAPI.readOptions).toBeCalled(); + }); + + test('should thrown content error', async () => { + ExecutionEnvironmentsAPI.read.mockRejectedValue( + new Error({ + response: { + config: { + method: 'GET', + url: '/api/v2/execution_environments', + }, + data: 'An error occurred', + }, + }) + ); + ExecutionEnvironmentsAPI.readOptions.mockResolvedValue(options); + await act(async () => { + wrapper = mountWithContexts(); + }); + await waitForElement( + wrapper, + 'ExecutionEnvironmentList', + el => el.length > 0 + ); + expect(wrapper.find('ContentError').length).toBe(1); + }); + + test('should not render add button', async () => { + ExecutionEnvironmentsAPI.read.mockResolvedValue(executionEnvironments); + ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({ + data: { actions: { POST: false } }, + }); + await act(async () => { + wrapper = mountWithContexts(); + }); + waitForElement(wrapper, 'ExecutionEnvironmentList', el => el.length > 0); + expect(wrapper.find('ToolbarAddButton').length).toBe(0); + }); +}); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx index 96d2d07e10..452fc4fbdb 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx @@ -1,14 +1,204 @@ -import React from 'react'; +import React, { useEffect, useCallback } from 'react'; +import { useLocation, useRouteMatch } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; import { Card, PageSection } from '@patternfly/react-core'; -function ExecutionEnvironmentList() { +import { ExecutionEnvironmentsAPI } from '../../../api'; +import { getQSConfig, parseQueryString } from '../../../util/qs'; +import useRequest, { useDeleteItems } from '../../../util/useRequest'; +import useSelected from '../../../util/useSelected'; +import PaginatedDataList, { + ToolbarDeleteButton, + ToolbarAddButton, +} from '../../../components/PaginatedDataList'; +import ErrorDetail from '../../../components/ErrorDetail'; +import AlertModal from '../../../components/AlertModal'; +import DatalistToolbar from '../../../components/DataListToolbar'; + +import ExecutionEnvironmentsListItem from './ExecutionEnvironmentListItem'; + +const QS_CONFIG = getQSConfig('execution_environments', { + page: 1, + page_size: 20, + managed_by_tower: false, + order_by: 'image', +}); + +function ExecutionEnvironmentList({ i18n }) { + const location = useLocation(); + const match = useRouteMatch(); + + const { + error: contentError, + isLoading, + request: fetchExecutionEnvironments, + result: { + executionEnvironments, + executionEnvironmentsCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, + } = useRequest( + useCallback(async () => { + const params = parseQueryString(QS_CONFIG, location.search); + + const [response, responseActions] = await Promise.all([ + ExecutionEnvironmentsAPI.read(params), + ExecutionEnvironmentsAPI.readOptions(), + ]); + + return { + executionEnvironments: response.data.results, + executionEnvironmentsCount: response.data.count, + actions: responseActions.data.actions, + relatedSearchableKeys: ( + responseActions?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + responseActions.data.actions?.GET || {} + ).filter(key => responseActions.data.actions?.GET[key].filterable), + }; + }, [location]), + { + executionEnvironments: [], + executionEnvironmentsCount: 0, + actions: {}, + relatedSearchableKeys: [], + searchableKeys: [], + } + ); + + useEffect(() => { + fetchExecutionEnvironments(); + }, [fetchExecutionEnvironments]); + + const { selected, isAllSelected, handleSelect, setSelected } = useSelected( + executionEnvironments + ); + + const { + isLoading: deleteLoading, + deletionError, + deleteItems: deleteExecutionEnvironments, + clearDeletionError, + } = useDeleteItems( + useCallback(async () => { + await Promise.all( + selected.map(({ id }) => ExecutionEnvironmentsAPI.destroy(id)) + ); + }, [selected]), + { + qsConfig: QS_CONFIG, + allItemsSelected: isAllSelected, + fetchItems: fetchExecutionEnvironments, + } + ); + + const handleDelete = async () => { + await deleteExecutionEnvironments(); + setSelected([]); + }; + + const canAdd = actions && actions.POST; + return ( - - -
List Execution environments
-
-
+ <> + + + ( + + setSelected(isSelected ? [...executionEnvironments] : []) + } + qsConfig={QS_CONFIG} + additionalControls={[ + ...(canAdd + ? [ + , + ] + : []), + , + ]} + /> + )} + renderItem={executionEnvironment => ( + handleSelect(executionEnvironment)} + isSelected={selected.some( + row => row.id === executionEnvironment.id + )} + /> + )} + emptyStateControls={ + canAdd && ( + + ) + } + /> + + + + {i18n._(t`Failed to delete one or more execution environments`)} + + + ); } -export default ExecutionEnvironmentList; +export default withI18n()(ExecutionEnvironmentList); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx new file mode 100644 index 0000000000..7789facfbb --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { string, bool, func } from 'prop-types'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Link } from 'react-router-dom'; +import { + Button, + DataListAction, + DataListCheck, + DataListItem, + DataListItemRow, + DataListItemCells, + Tooltip, +} from '@patternfly/react-core'; +import { PencilAltIcon } from '@patternfly/react-icons'; + +import DataListCell from '../../../components/DataListCell'; +import { ExecutionEnvironment } from '../../../types'; + +function ExecutionEnvironmentListItem({ + executionEnvironment, + detailUrl, + isSelected, + onSelect, + i18n, +}) { + const labelId = `check-action-${executionEnvironment.id}`; + + return ( + + + + + + {executionEnvironment.image} + + , + ]} + /> + + + + + + + + ); +} + +ExecutionEnvironmentListItem.prototype = { + executionEnvironment: ExecutionEnvironment.isRequired, + detailUrl: string.isRequired, + isSelected: bool.isRequired, + onSelect: func.isRequired, +}; + +export default withI18n()(ExecutionEnvironmentListItem); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.test.jsx new file mode 100644 index 0000000000..4f51a51672 --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.test.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; + +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; + +import ExecutionEnvironmentListItem from './ExecutionEnvironmentListItem'; + +describe('', () => { + let wrapper; + const executionEnvironment = { + id: 1, + image: 'https://registry.com/r/image/manifest', + organization: null, + credential: null, + }; + + test('should mount successfully', async () => { + await act(async () => { + wrapper = mountWithContexts( + {}} + /> + ); + }); + expect(wrapper.find('ExecutionEnvironmentListItem').length).toBe(1); + }); + + test('should render the proper data', async () => { + await act(async () => { + wrapper = mountWithContexts( + {}} + /> + ); + }); + expect( + wrapper + .find('DataListCell[aria-label="execution environment image"]') + .text() + ).toBe(executionEnvironment.image); + expect(wrapper.find('PencilAltIcon').length).toBe(1); + expect( + wrapper.find('input#select-execution-environment-1').prop('checked') + ).toBe(false); + }); +}); diff --git a/awx/ui_next/src/types.js b/awx/ui_next/src/types.js index 760ecf4ed2..53c7ed3b3d 100644 --- a/awx/ui_next/src/types.js +++ b/awx/ui_next/src/types.js @@ -407,3 +407,13 @@ export const WorkflowApproval = shape({ approval_expiration: string, timed_out: bool, }); + +export const ExecutionEnvironment = shape({ + id: number.isRequired, + organization: number, + credential: number, + image: string.isRequired, + url: string, + summary_fields: shape({}), + description: string, +}); From 9530c6ca50b9e9afd15b39e4efc22a969e03d00d Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Wed, 16 Sep 2020 11:34:24 -0400 Subject: [PATCH 027/178] Changes to get execution environments factories working (#8126) --- .../api/pages/execution_environments.py | 26 ++++++++++++++++++- awxkit/awxkit/api/pages/job_templates.py | 21 +++++++-------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/awxkit/awxkit/api/pages/execution_environments.py b/awxkit/awxkit/api/pages/execution_environments.py index e48ef324bd..87225d1052 100644 --- a/awxkit/awxkit/api/pages/execution_environments.py +++ b/awxkit/awxkit/api/pages/execution_environments.py @@ -1,11 +1,12 @@ import logging -from awxkit.api.mixins import HasCreate +from awxkit.api.mixins import DSAdapter, HasCreate from awxkit.api.pages import ( Credential, Organization, ) from awxkit.api.resources import resources +from awxkit.utils import random_title, PseudoNamespace from . import base from . import page @@ -19,6 +20,29 @@ class ExecutionEnvironment(HasCreate, base.Base): dependencies = [Organization, Credential] NATURAL_KEY = ('organization', 'image') + # fields are image, organization, managed_by_tower, credential + def create(self, image='quay.io/ansible/ansible-runner:devel', credential=None, **kwargs): + # we do not want to make a credential by default + payload = self.create_payload(image=image, credential=credential, **kwargs) + ret = self.update_identity(ExecutionEnvironments(self.connection).post(payload)) + return ret + + def create_payload(self, organization=Organization, **kwargs): + self.create_and_update_dependencies(organization) + payload = self.payload(organization=self.ds.organization, **kwargs) + payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store) + return payload + + def payload(self, image=None, organization=None, credential=None, **kwargs): + payload = PseudoNamespace( + image=image or random_title(10), + organization=organization.id if organization else None, + credential=credential.id if credential else None, + **kwargs + ) + + return payload + page.register_page([resources.execution_environment, (resources.execution_environments, 'post'), diff --git a/awxkit/awxkit/api/pages/job_templates.py b/awxkit/awxkit/api/pages/job_templates.py index cd45fc0c87..9f008078a9 100644 --- a/awxkit/awxkit/api/pages/job_templates.py +++ b/awxkit/awxkit/api/pages/job_templates.py @@ -101,18 +101,17 @@ class JobTemplate( if kwargs.get('project'): payload.update(project=kwargs.get('project').id, playbook=playbook) - if kwargs.get('inventory'): - payload.update(inventory=kwargs.get('inventory').id) - if kwargs.get('credential'): - payload.update(credential=kwargs.get('credential').id) - if kwargs.get('webhook_credential'): - webhook_cred = kwargs.get('webhook_credential') - if isinstance(webhook_cred, int): - payload.update(webhook_credential=int(webhook_cred)) - elif hasattr(webhook_cred, 'id'): - payload.update(webhook_credential=webhook_cred.id) + + for fk_field in ('inventory', 'credential', 'webhook_credential', 'execution_environment'): + rel_obj = kwargs.get(fk_field) + if rel_obj is None: + continue + elif isinstance(rel_obj, int): + payload.update(**{fk_field: int(rel_obj)}) + elif hasattr(rel_obj, 'id'): + payload.update(**{fk_field: rel_obj.id}) else: - raise AttributeError("Webhook credential must either be integer of pkid or Credential object") + raise AttributeError(f'Related field {fk_field} must be either integer of pkid or object') return payload From 684b9bd47aa56111466b4d11e682bc1d470c5a78 Mon Sep 17 00:00:00 2001 From: Kersom <9053044+nixocio@users.noreply.github.com> Date: Fri, 18 Sep 2020 08:40:29 -0400 Subject: [PATCH 028/178] Add feature to Add/Edit Execution Environments (#8165) * Add feature to Add/Edit Execution Environments Add feature to Add/Edit Execution Environments. Also, add key for `ExecutionEnvironmentsList`. See: https://github.com/ansible/awx/issues/7887 * Update registry credential label --- awx/ui_next/src/routeConfig.js | 2 +- .../ExecutionEnvironment.jsx | 133 +++++++++++++++--- .../ExecutionEnvironmentAdd.jsx | 33 ++++- .../ExecutionEnvironmentAdd.test.jsx | 80 +++++++++++ .../ExecutionEnvironmentEdit.jsx | 42 ++++-- .../ExecutionEnvironmentEdit.test.jsx | 103 ++++++++++++++ .../ExecutionEnviromentList.test.jsx | 2 + .../ExecutionEnvironmentList.jsx | 1 + .../ExecutionEnvironments.jsx | 2 +- .../shared/ExecutionEnvironmentForm.jsx | 86 +++++++++++ .../shared/ExecutionEnvironmentForm.test.jsx | 115 +++++++++++++++ 11 files changed, 570 insertions(+), 29 deletions(-) create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.test.jsx create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.test.jsx diff --git a/awx/ui_next/src/routeConfig.js b/awx/ui_next/src/routeConfig.js index c0aff394f0..507bc4e6d7 100644 --- a/awx/ui_next/src/routeConfig.js +++ b/awx/ui_next/src/routeConfig.js @@ -140,7 +140,7 @@ function getRouteConfig(i18n) { screen: Applications, }, { - title: i18n._(t`Execution environments`), + title: i18n._(t`Execution Environments`), path: '/execution_environments', screen: ExecutionEnvironments, }, diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx index 9575a3b568..78ead63048 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx @@ -1,25 +1,124 @@ -import React from 'react'; -import { Route, Redirect, Switch } from 'react-router-dom'; +import React, { useEffect, useCallback } from 'react'; +import { + Link, + Redirect, + Route, + Switch, + useLocation, + useParams, +} from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Card, PageSection } from '@patternfly/react-core'; +import { CaretLeftIcon } from '@patternfly/react-icons'; + +import useRequest from '../../util/useRequest'; +import { ExecutionEnvironmentsAPI } from '../../api'; +import RoutedTabs from '../../components/RoutedTabs'; +import ContentError from '../../components/ContentError'; +import ContentLoading from '../../components/ContentLoading'; import ExecutionEnvironmentDetails from './ExecutionEnvironmentDetails'; import ExecutionEnvironmentEdit from './ExecutionEnvironmentEdit'; -function ExecutionEnvironment() { +function ExecutionEnvironment({ i18n, setBreadcrumb }) { + const { id } = useParams(); + const { pathname } = useLocation(); + + const { + isLoading, + error: contentError, + request: fetchExecutionEnvironments, + result: executionEnvironment, + } = useRequest( + useCallback(async () => { + const { data } = await ExecutionEnvironmentsAPI.readDetail(id); + return data; + }, [id]), + null + ); + + useEffect(() => { + fetchExecutionEnvironments(); + }, [fetchExecutionEnvironments, pathname]); + + useEffect(() => { + if (executionEnvironment) { + setBreadcrumb(executionEnvironment); + } + }, [executionEnvironment, setBreadcrumb]); + + const tabsArray = [ + { + name: ( + <> + + {i18n._(t`Back to execution environments`)} + + ), + link: '/execution_environments', + id: 99, + }, + { + name: i18n._(t`Details`), + link: `/execution_environments/${id}/details`, + id: 0, + }, + ]; + + if (!isLoading && contentError) { + return ( + + + + {contentError.response?.status === 404 && ( + + {i18n._(t`Execution environment not found.`)}{' '} + + {i18n._(t`View all execution environments`)} + + + )} + + + + ); + } + + let cardHeader = ; + if (pathname.endsWith('edit')) { + cardHeader = null; + } + return ( - - - - - - - - - + + + {cardHeader} + {isLoading && } + {!isLoading && executionEnvironment && ( + + + {executionEnvironment && ( + <> + + + + + + + + )} + + )} + + ); } -export default ExecutionEnvironment; +export default withI18n()(ExecutionEnvironment); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.jsx index e188990878..5f162f352e 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.jsx @@ -1,11 +1,40 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Card, PageSection } from '@patternfly/react-core'; +import { useHistory } from 'react-router-dom'; + +import ExecutionEnvironmentForm from '../shared/ExecutionEnvironmentForm'; +import { CardBody } from '../../../components/Card'; +import { ExecutionEnvironmentsAPI } from '../../../api'; function ExecutionEnvironmentAdd() { + const history = useHistory(); + const [submitError, setSubmitError] = useState(null); + + const handleSubmit = async values => { + try { + const { data: response } = await ExecutionEnvironmentsAPI.create({ + ...values, + credential: values?.credential?.id, + }); + history.push(`/execution_environments/${response.id}/details`); + } catch (error) { + setSubmitError(error); + } + }; + + const handleCancel = () => { + history.push(`/execution_environments`); + }; return ( -
Add Execution Environments
+ + +
); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx new file mode 100644 index 0000000000..781501b19e --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; + +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { ExecutionEnvironmentsAPI } from '../../../api'; +import ExecutionEnvironmentAdd from './ExecutionEnvironmentAdd'; + +jest.mock('../../../api'); + +const executionEnvironmentData = { + credential: 4, + description: 'A simple EE', + image: 'https://registry.com/image/container', +}; + +ExecutionEnvironmentsAPI.create.mockResolvedValue({ + data: { + id: 42, + }, +}); + +describe('', () => { + let wrapper; + let history; + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/execution_environments'], + }); + await act(async () => { + wrapper = mountWithContexts(, { + context: { router: { history } }, + }); + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + wrapper.unmount(); + }); + + test('handleSubmit should call the api and redirect to details page', async () => { + await act(async () => { + wrapper.find('ExecutionEnvironmentForm').prop('onSubmit')({ + executionEnvironmentData, + }); + }); + wrapper.update(); + expect(ExecutionEnvironmentsAPI.create).toHaveBeenCalledWith({ + executionEnvironmentData, + }); + expect(history.location.pathname).toBe( + '/execution_environments/42/details' + ); + }); + + test('handleCancel should return the user back to the execution environments list', async () => { + wrapper.find('Button[aria-label="Cancel"]').simulate('click'); + expect(history.location.pathname).toEqual('/execution_environments'); + }); + + test('failed form submission should show an error message', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + ExecutionEnvironmentsAPI.create.mockImplementationOnce(() => + Promise.reject(error) + ); + await act(async () => { + wrapper.find('ExecutionEnvironmentForm').invoke('onSubmit')( + executionEnvironmentData + ); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx index 91e3096ce3..6d8cbc9520 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx @@ -1,13 +1,39 @@ -import React from 'react'; -import { Card, PageSection } from '@patternfly/react-core'; +import React, { useState } from 'react'; +import { useHistory } from 'react-router-dom'; -function ExecutionEnvironmentEdit() { +import { CardBody } from '../../../components/Card'; +import { ExecutionEnvironmentsAPI } from '../../../api'; +import ExecutionEnvironmentForm from '../shared/ExecutionEnvironmentForm'; + +function ExecutionEnvironmentEdit({ executionEnvironment }) { + const history = useHistory(); + const [submitError, setSubmitError] = useState(null); + const detailsUrl = `/execution_environments/${executionEnvironment.id}/details`; + + const handleSubmit = async values => { + try { + await ExecutionEnvironmentsAPI.update(executionEnvironment.id, { + ...values, + credential: values.credential ? values.credential.id : null, + }); + history.push(detailsUrl); + } catch (error) { + setSubmitError(error); + } + }; + + const handleCancel = () => { + history.push(detailsUrl); + }; return ( - - -
Edit Execution environments
-
-
+ + + ); } diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.test.jsx new file mode 100644 index 0000000000..2d4f916aba --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.test.jsx @@ -0,0 +1,103 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; + +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { ExecutionEnvironmentsAPI } from '../../../api'; + +import ExecutionEnvironmentEdit from './ExecutionEnvironmentEdit'; + +jest.mock('../../../api'); + +const executionEnvironmentData = { + id: 42, + credential: { id: 4 }, + description: 'A simple EE', + image: 'https://registry.com/image/container', +}; + +const updateExecutionEnvironmentData = { + image: 'https://registry.com/image/container2', + description: 'Updated new description', +}; + +describe('', () => { + let wrapper; + let history; + + beforeAll(async () => { + history = createMemoryHistory(); + await act(async () => { + wrapper = mountWithContexts( + , + { + context: { router: { history } }, + } + ); + }); + }); + + afterAll(() => { + jest.clearAllMocks(); + wrapper.unmount(); + }); + + test('handleSubmit should call the api and redirect to details page', async () => { + await act(async () => { + wrapper.find('ExecutionEnvironmentForm').invoke('onSubmit')( + updateExecutionEnvironmentData + ); + wrapper.update(); + expect(ExecutionEnvironmentsAPI.update).toHaveBeenCalledWith(42, { + ...updateExecutionEnvironmentData, + credential: null, + }); + }); + + expect(history.location.pathname).toEqual( + '/execution_environments/42/details' + ); + }); + + test('should navigate to execution environments details when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').prop('onClick')(); + }); + expect(history.location.pathname).toEqual( + '/execution_environments/42/details' + ); + }); + + test('should navigate to execution environments detail after successful submission', async () => { + await act(async () => { + wrapper.find('ExecutionEnvironmentForm').invoke('onSubmit')({ + updateExecutionEnvironmentData, + }); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(history.location.pathname).toEqual( + '/execution_environments/42/details' + ); + }); + + test('failed form submission should show an error message', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + ExecutionEnvironmentsAPI.update.mockImplementationOnce(() => + Promise.reject(error) + ); + await act(async () => { + wrapper.find('ExecutionEnvironmentForm').invoke('onSubmit')( + updateExecutionEnvironmentData + ); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx index 8b04233404..4371d4c727 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx @@ -19,12 +19,14 @@ const executionEnvironments = { image: 'https://registry.com/r/image/manifest', organization: null, credential: null, + url: '/api/v2/execution_environments/1/', }, { id: 2, image: 'https://registry.com/r/image2/manifest', organization: null, credential: null, + url: '/api/v2/execution_environments/2/', }, ], count: 2, diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx index 452fc4fbdb..02ce49ee9f 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx @@ -171,6 +171,7 @@ function ExecutionEnvironmentList({ i18n }) { )} renderItem={executionEnvironment => ( handleSelect(executionEnvironment)} diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.jsx index 7db470e2f7..7db0baaedc 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.jsx @@ -22,7 +22,7 @@ function ExecutionEnvironments({ i18n }) { setBreadcrumbConfig({ '/execution_environments': i18n._(t`Execution environments`), '/execution_environments/add': i18n._(t`Create Execution environments`), - [`/execution_environments/${executionEnvironments.id}`]: `${executionEnvironments.name}`, + [`/execution_environments/${executionEnvironments.id}`]: `${executionEnvironments.image}`, [`/execution_environments/${executionEnvironments.id}/edit`]: i18n._( t`Edit details` ), diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx new file mode 100644 index 0000000000..60988ab278 --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { func, shape } from 'prop-types'; +import { Formik, useField } from 'formik'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; + +import { Form } from '@patternfly/react-core'; +import FormField, { FormSubmitError } from '../../../components/FormField'; +import FormActionGroup from '../../../components/FormActionGroup'; +import CredentialLookup from '../../../components/Lookup/CredentialLookup'; +import { url } from '../../../util/validators'; +import { FormColumnLayout } from '../../../components/FormLayout'; + +function ExecutionEnvironmentFormFields({ i18n }) { + const [credentialField, , credentialHelpers] = useField('credential'); + return ( + <> + + + credentialHelpers.setValue(value)} + value={credentialField.value || null} + /> + + ); +} + +function ExecutionEnvironmentForm({ + executionEnvironment = {}, + onSubmit, + onCancel, + submitError, + ...rest +}) { + const initialValues = { + image: executionEnvironment.image || '', + description: executionEnvironment.description || '', + credential: executionEnvironment?.summary_fields?.credential || null, + }; + return ( + onSubmit(values)}> + {formik => ( +
+ + + {submitError && } + + +
+ )} +
+ ); +} + +ExecutionEnvironmentForm.propTypes = { + executionEnvironment: shape({}), + onCancel: func.isRequired, + onSubmit: func.isRequired, + submitError: shape({}), +}; + +ExecutionEnvironmentForm.defaultProps = { + executionEnvironment: {}, + submitError: null, +}; + +export default withI18n()(ExecutionEnvironmentForm); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.test.jsx new file mode 100644 index 0000000000..717b264354 --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.test.jsx @@ -0,0 +1,115 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; + +import ExecutionEnvironmentForm from './ExecutionEnvironmentForm'; + +jest.mock('../../../api'); + +const executionEnvironment = { + id: 16, + type: 'execution_environment', + url: '/api/v2/execution_environments/16/', + related: { + created_by: '/api/v2/users/1/', + modified_by: '/api/v2/users/1/', + activity_stream: '/api/v2/execution_environments/16/activity_stream/', + unified_job_templates: + '/api/v2/execution_environments/16/unified_job_templates/', + credential: '/api/v2/credentials/4/', + }, + summary_fields: { + credential: { + id: 4, + name: 'Container Registry', + }, + }, + created: '2020-09-17T16:06:57.346128Z', + modified: '2020-09-17T16:06:57.346147Z', + description: 'A simple EE', + organization: null, + image: 'https://registry.com/image/container', + managed_by_tower: false, + credential: 4, +}; + +describe('', () => { + let wrapper; + let onCancel; + let onSubmit; + + beforeEach(async () => { + onCancel = jest.fn(); + onSubmit = jest.fn(); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + wrapper.unmount(); + }); + + test('Initially renders successfully', () => { + expect(wrapper.length).toBe(1); + }); + + test('should display form fields properly', () => { + expect(wrapper.find('FormGroup[label="Image"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="Description"]').length).toBe(1); + expect(wrapper.find('CredentialLookup').length).toBe(1); + }); + + test('should call onSubmit when form submitted', async () => { + expect(onSubmit).not.toHaveBeenCalled(); + await act(async () => { + wrapper.find('button[aria-label="Save"]').simulate('click'); + }); + expect(onSubmit).toHaveBeenCalledTimes(1); + }); + + test('should update form values', () => { + act(() => { + wrapper.find('input#execution-environment-image').simulate('change', { + target: { + value: 'https://registry.com/image/container2', + name: 'image', + }, + }); + wrapper + .find('input#execution-environment-description') + .simulate('change', { + target: { value: 'New description', name: 'description' }, + }); + wrapper.find('CredentialLookup').invoke('onBlur')(); + wrapper.find('CredentialLookup').invoke('onChange')({ + id: 99, + name: 'credential', + }); + }); + wrapper.update(); + expect( + wrapper.find('input#execution-environment-image').prop('value') + ).toEqual('https://registry.com/image/container2'); + expect( + wrapper.find('input#execution-environment-description').prop('value') + ).toEqual('New description'); + expect(wrapper.find('CredentialLookup').prop('value')).toEqual({ + id: 99, + name: 'credential', + }); + }); + + test('should call handleCancel when Cancel button is clicked', async () => { + expect(onCancel).not.toHaveBeenCalled(); + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + expect(onCancel).toBeCalled(); + }); +}); From 54d0f173dce26c03fb4d3a783b02dfe9b214337b Mon Sep 17 00:00:00 2001 From: Kersom <9053044+nixocio@users.noreply.github.com> Date: Fri, 18 Sep 2020 09:41:09 -0400 Subject: [PATCH 029/178] Add details page for Execution Environments (#8172) * Add feature to Add/Edit Execution Environments Add feature to Add/Edit Execution Environments. Also, add key for `ExecutionEnvironmentsList`. See: https://github.com/ansible/awx/issues/7887 * Add details page for execution environments Add details page for execution environments See: https://github.com/ansible/awx/issues/8171 --- .../ExecutionEnvironment.jsx | 4 +- .../ExecutionEnvironmentDetails.jsx | 100 ++++++++++++++++-- .../ExecutionEnvironmentDetails.test.jsx | 98 +++++++++++++++++ .../shared/ExecutionEnvironmentForm.jsx | 6 +- 4 files changed, 195 insertions(+), 13 deletions(-) create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.test.jsx diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx index 78ead63048..55a3228e13 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx @@ -110,7 +110,9 @@ function ExecutionEnvironment({ i18n, setBreadcrumb }) { /> - + )} diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx index f6902d1735..abb6cf5ddc 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx @@ -1,14 +1,96 @@ -import React from 'react'; -import { Card, PageSection } from '@patternfly/react-core'; +import React, { useCallback } from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Link, useHistory } from 'react-router-dom'; +import { Button, Label } from '@patternfly/react-core'; + +import AlertModal from '../../../components/AlertModal'; +import { CardBody, CardActionsRow } from '../../../components/Card'; +import DeleteButton from '../../../components/DeleteButton'; +import { + Detail, + DetailList, + UserDateDetail, +} from '../../../components/DetailList'; +import useRequest, { useDismissableError } from '../../../util/useRequest'; +import { ExecutionEnvironmentsAPI } from '../../../api'; + +function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) { + const history = useHistory(); + const { id, image, description } = executionEnvironment; + + const { + request: deleteExecutionEnvironment, + isLoading, + error: deleteError, + } = useRequest( + useCallback(async () => { + await ExecutionEnvironmentsAPI.destroy(id); + history.push(`/execution_environments`); + }, [id, history]) + ); + + const { error, dismissError } = useDismissableError(deleteError); -function ExecutionEnvironmentDetails() { return ( - - -
Execution environments details
-
-
+ + + + + {executionEnvironment.summary_fields.credential && ( + + {executionEnvironment.summary_fields.credential.name} + + } + dataCy="execution-environment-credential" + /> + )} + + + + + + + {i18n._(t`Delete`)} + + + + {error && ( + + )} + ); } -export default ExecutionEnvironmentDetails; +export default withI18n()(ExecutionEnvironmentDetails); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.test.jsx new file mode 100644 index 0000000000..6e264964d4 --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.test.jsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; + +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { ExecutionEnvironmentsAPI } from '../../../api'; + +import ExecutionEnvironmentDetails from './ExecutionEnvironmentDetails'; + +jest.mock('../../../api'); + +const executionEnvironment = { + id: 17, + type: 'execution_environment', + url: '/api/v2/execution_environments/17/', + related: { + created_by: '/api/v2/users/1/', + modified_by: '/api/v2/users/1/', + activity_stream: '/api/v2/execution_environments/17/activity_stream/', + unified_job_templates: + '/api/v2/execution_environments/17/unified_job_templates/', + credential: '/api/v2/credentials/4/', + }, + summary_fields: { + credential: { + id: 4, + name: 'Container Registry', + }, + created_by: { + id: 1, + username: 'admin', + first_name: '', + last_name: '', + }, + modified_by: { + id: 1, + username: 'admin', + first_name: '', + last_name: '', + }, + }, + created: '2020-09-17T20:14:15.408782Z', + modified: '2020-09-17T20:14:15.408802Z', + description: 'Foo', + organization: null, + image: 'https://localhost:90/12345/ma', + managed_by_tower: false, + credential: 4, +}; + +describe('', () => { + let wrapper; + test('should render details properly', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + wrapper.update(); + + expect(wrapper.find('Detail[label="Image"]').prop('value')).toEqual( + executionEnvironment.image + ); + expect(wrapper.find('Detail[label="Description"]').prop('value')).toEqual( + 'Foo' + ); + expect( + wrapper.find('Detail[label="Credential"]').prop('value').props.children + ).toEqual(executionEnvironment.summary_fields.credential.name); + const dates = wrapper.find('UserDateDetail'); + expect(dates).toHaveLength(2); + expect(dates.at(0).prop('date')).toEqual(executionEnvironment.created); + expect(dates.at(1).prop('date')).toEqual(executionEnvironment.modified); + }); + + test('expected api call is made for delete', async () => { + const history = createMemoryHistory({ + initialEntries: ['/execution_environments/42/details'], + }); + await act(async () => { + wrapper = mountWithContexts( + , + { + context: { router: { history } }, + } + ); + }); + await act(async () => { + wrapper.find('DeleteButton').invoke('onConfirm')(); + }); + expect(ExecutionEnvironmentsAPI.destroy).toHaveBeenCalledTimes(1); + expect(history.location.pathname).toBe('/execution_environments'); + }); +}); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx index 60988ab278..a5170ae3bb 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx @@ -17,7 +17,7 @@ function ExecutionEnvironmentFormFields({ i18n }) { <> credentialHelpers.setValue(value)} - value={credentialField.value || null} + value={credentialField.value} /> ); From 3d233faed842005cdaa15270e93d86924e049db7 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Tue, 22 Sep 2020 16:40:32 -0400 Subject: [PATCH 030/178] Expose the user capabilities dict for EEs (#8208) --- awx/api/serializers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 46e430a8e3..435f2ad444 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1361,6 +1361,8 @@ class ProjectOptionsSerializer(BaseSerializer): class ExecutionEnvironmentSerializer(BaseSerializer): + show_capabilities = ['edit', 'delete'] + class Meta: model = ExecutionEnvironment fields = ('*', '-name', 'organization', 'image', 'managed_by_tower', 'credential') From 6e6cd51b4de4986cb28a7a88b9b286006fb64ed9 Mon Sep 17 00:00:00 2001 From: Kersom <9053044+nixocio@users.noreply.github.com> Date: Wed, 23 Sep 2020 12:44:08 -0400 Subject: [PATCH 031/178] Update usage of summary_fields for execution environments (#8217) Update usage of summary_fields for execution environments. Also, update unit-tests to cover this change. See: https://github.com/ansible/awx/issues/8216 --- .../PaginatedDataList/ToolbarDeleteButton.jsx | 25 ++++-- .../ExecutionEnviromentList.test.jsx | 77 +++++++++++++++++++ .../shared/ExecutionEnvironmentForm.test.jsx | 2 +- 3 files changed, 96 insertions(+), 8 deletions(-) diff --git a/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx b/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx index f9afa31179..4874ab9f64 100644 --- a/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx +++ b/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx @@ -20,11 +20,11 @@ const WarningMessage = styled(Alert)` margin-top: 10px; `; -const requireNameOrUsername = props => { - const { name, username } = props; - if (!name && !username) { +const requiredField = props => { + const { name, username, image } = props; + if (!name && !username && !image) { return new Error( - `One of 'name' or 'username' is required by ItemToDelete component.` + `One of 'name', 'username' or 'image' is required by ItemToDelete component.` ); } if (name) { @@ -47,13 +47,24 @@ const requireNameOrUsername = props => { 'ItemToDelete' ); } + if (image) { + checkPropTypes( + { + image: string, + }, + { image: props.image }, + 'prop', + 'ItemToDelete' + ); + } return null; }; const ItemToDelete = shape({ id: number.isRequired, - name: requireNameOrUsername, - username: requireNameOrUsername, + name: requiredField, + username: requiredField, + image: requiredField, summary_fields: shape({ user_capabilities: shape({ delete: bool.isRequired, @@ -171,7 +182,7 @@ function ToolbarDeleteButton({
{i18n._(t`This action will delete the following:`)}
{itemsToDelete.map(item => ( - {item.name || item.username} + {item.name || item.username || item.image}
))} diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx index 4371d4c727..475dd1a8b5 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx @@ -20,6 +20,7 @@ const executionEnvironments = { organization: null, credential: null, url: '/api/v2/execution_environments/1/', + summary_fields: { user_capabilities: { edit: true, delete: true } }, }, { id: 2, @@ -27,6 +28,7 @@ const executionEnvironments = { organization: null, credential: null, url: '/api/v2/execution_environments/2/', + summary_fields: { user_capabilities: { edit: false, delete: true } }, }, ], count: 2, @@ -67,6 +69,81 @@ describe('', () => { expect(ExecutionEnvironmentsAPI.readOptions).toBeCalled(); }); + test('should delete item successfully', async () => { + ExecutionEnvironmentsAPI.read.mockResolvedValue(executionEnvironments); + ExecutionEnvironmentsAPI.readOptions.mockResolvedValue(options); + + await act(async () => { + wrapper = mountWithContexts(); + }); + await waitForElement( + wrapper, + 'ExecutionEnvironmentList', + el => el.length > 0 + ); + + wrapper + .find('input#select-execution-environment-1') + .simulate('change', executionEnvironments.data.results[0]); + wrapper.update(); + + expect( + wrapper.find('input#select-execution-environment-1').prop('checked') + ).toBe(true); + + await act(async () => { + wrapper.find('Button[aria-label="Delete"]').prop('onClick')(); + }); + wrapper.update(); + + await act(async () => { + wrapper.find('Button[aria-label="confirm delete"]').prop('onClick')(); + }); + + expect(ExecutionEnvironmentsAPI.destroy).toBeCalledWith( + executionEnvironments.data.results[0].id + ); + }); + + test('should render deletion error modal', async () => { + ExecutionEnvironmentsAPI.destroy.mockRejectedValue( + new Error({ + response: { + config: { + method: 'DELETE', + url: '/api/v2/execution_environments', + }, + data: 'An error occurred', + }, + }) + ); + ExecutionEnvironmentsAPI.read.mockResolvedValue(executionEnvironments); + ExecutionEnvironmentsAPI.readOptions.mockResolvedValue(options); + await act(async () => { + wrapper = mountWithContexts(); + }); + waitForElement(wrapper, 'ExecutionEnvironmentList', el => el.length > 0); + + wrapper + .find('input#select-execution-environment-1') + .simulate('change', 'a'); + wrapper.update(); + expect( + wrapper.find('input#select-execution-environment-1').prop('checked') + ).toBe(true); + + await act(async () => + wrapper.find('Button[aria-label="Delete"]').prop('onClick')() + ); + wrapper.update(); + + await act(async () => + wrapper.find('Button[aria-label="confirm delete"]').prop('onClick')() + ); + wrapper.update(); + expect(wrapper.find('ErrorDetail').length).toBe(1); + }); + test('should thrown content error', async () => { ExecutionEnvironmentsAPI.read.mockRejectedValue( new Error({ diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.test.jsx index 717b264354..98164b6964 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.test.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.test.jsx @@ -62,7 +62,7 @@ describe('', () => { }); test('should display form fields properly', () => { - expect(wrapper.find('FormGroup[label="Image"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="Image name"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Description"]').length).toBe(1); expect(wrapper.find('CredentialLookup').length).toBe(1); }); From 7c6975baec64acf89728b02a2a3e7e0b9fc25382 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Tue, 29 Sep 2020 23:17:42 -0400 Subject: [PATCH 032/178] Collections volume permission fix, and container group fix Use same image for both types of container isolation Inventory move fix related to container groups --- awx/main/tasks.py | 15 ++++++++++----- awx/playbooks/project_update.yml | 3 +++ awx/settings/defaults.py | 6 +++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 50230c0e98..1c5b04fff9 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1467,7 +1467,6 @@ class BaseTask(object): 'job_timeout': self.get_instance_timeout(self.instance), 'suppress_ansible_output': True, #**process_isolation_params, - **execution_environment_params, **resource_profiling_params, }, } @@ -1476,6 +1475,10 @@ class BaseTask(object): # We don't want HOME passed through to container groups. # TODO: remove this conditional after everything is containerized params['envvars'].pop('HOME', None) + else: + # TODO: container group jobs will not work with container isolation settings + # but both will run with same settings when worker_in and worker_out are added + params['settings'].update(execution_environment_params) if isinstance(self.instance, AdHocCommand): params['module'] = self.build_module_name(self.instance) @@ -1503,10 +1506,12 @@ class BaseTask(object): module_args = ansible_runner.utils.args2cmdline( params.get('module_args'), ) - shutil.move( - params.pop('inventory'), - os.path.join(private_data_dir, 'inventory') - ) + # TODO on merge: delete if https://github.com/ansible/awx/pull/8185 is merged + if not os.path.exists(os.path.join(private_data_dir, 'inventory')): + shutil.move( + params.pop('inventory'), + os.path.join(private_data_dir, 'inventory') + ) ansible_runner.utils.dump_artifacts(params) isolated_manager_instance = isolated_manager.IsolatedManager( diff --git a/awx/playbooks/project_update.yml b/awx/playbooks/project_update.yml index 49618909fb..664f189a28 100644 --- a/awx/playbooks/project_update.yml +++ b/awx/playbooks/project_update.yml @@ -204,6 +204,9 @@ ANSIBLE_FORCE_COLOR: false ANSIBLE_COLLECTIONS_PATHS: "{{projects_root}}/.__awx_cache/{{local_path}}/stage/requirements_collections" GIT_SSH_COMMAND: "ssh -o StrictHostKeyChecking=no" + # Put the local tmp directory in same volume as collection destination + # otherwise, files cannot be moved accross volumes and will cause error + ANSIBLE_LOCAL_TEMP: "{{projects_root}}/.__awx_cache/{{local_path}}/stage/tmp" when: - "ansible_version.full is version_compare('2.9', '>=')" diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 849d6220bc..0a2a7043d6 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -59,13 +59,13 @@ DATABASES = { } } +AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE = 'quay.io/ansible/ansible-runner:devel' + AWX_CONTAINER_GROUP_K8S_API_TIMEOUT = 10 AWX_CONTAINER_GROUP_POD_LAUNCH_RETRIES = 100 AWX_CONTAINER_GROUP_POD_LAUNCH_RETRY_DELAY = 5 AWX_CONTAINER_GROUP_DEFAULT_NAMESPACE = 'default' -AWX_CONTAINER_GROUP_DEFAULT_IMAGE = 'ansible/ansible-runner' - -AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE = 'quay.io/ansible/ansible-runner:devel' +AWX_CONTAINER_GROUP_DEFAULT_IMAGE = AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE # Internationalization # https://docs.djangoproject.com/en/dev/topics/i18n/ From 87b13ead12d0da31985d70da2b7929ecfffecffb Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Thu, 8 Oct 2020 13:40:51 -0400 Subject: [PATCH 033/178] REVERT ME: Install ansible/devel for now --- .../roles/dockerfile/templates/Dockerfile.j2 | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index e056b10fb1..6fa2c08cd4 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -6,10 +6,13 @@ # Locations - set globally to be used across stages ARG COLLECTION_BASE="/var/lib/awx/vendor/awx_ansible_collections" +ARG ANSIBLE_BRANCH=devel # Build container FROM centos:8 as builder +ARG ANSIBLE_BRANCH + ENV LANG en_US.UTF-8 ENV LANGUAGE en_US:en ENV LC_ALL en_US.UTF-8 @@ -22,7 +25,7 @@ RUN dnf -y update && \ dnf -y install epel-release 'dnf-command(config-manager)' && \ dnf module -y enable 'postgresql:12' && \ dnf config-manager --set-enabled powertools && \ - dnf -y install ansible \ + dnf -y install \ gcc \ gcc-c++ \ git-core \ @@ -46,7 +49,8 @@ RUN dnf -y update && \ xmlsec1-devel \ xmlsec1-openssl-devel -RUN python3 -m ensurepip && pip3 install "virtualenv < 20" +RUN python3 -m ensurepip && pip3 install "virtualenv < 20" && \ + pip3 install --no-cache-dir https://github.com/ansible/ansible/archive/${ANSIBLE_BRANCH}.tar.gz # Install & build requirements ADD Makefile /tmp/Makefile @@ -81,6 +85,7 @@ RUN make sdist && \ FROM centos:8 ARG COLLECTION_BASE +ARG ANSIBLE_BRANCH ENV LANG en_US.UTF-8 ENV LANGUAGE en_US:en @@ -95,7 +100,6 @@ RUN dnf -y update && \ dnf module -y enable 'postgresql:12' && \ dnf config-manager --set-enabled powertools && \ dnf -y install acl \ - ansible \ bubblewrap \ git-core \ git-lfs \ @@ -132,7 +136,8 @@ RUN curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master RUN curl -L -o /usr/bin/tini https://github.com/krallin/tini/releases/download/v0.19.0/tini-{{ tini_architecture | default('amd64') }} && \ chmod +x /usr/bin/tini -RUN python3 -m ensurepip && pip3 install "virtualenv < 20" supervisor {% if build_dev|bool %}flake8{% endif %} +RUN python3 -m ensurepip && pip3 install "virtualenv < 20" supervisor {% if build_dev|bool %}flake8{% endif %} && \ + pip3 install --no-cache-dir https://github.com/ansible/ansible/archive/${ANSIBLE_BRANCH}.tar.gz RUN rm -rf /root/.cache && rm -rf /tmp/* From efb25b7b9e75040380d954bb5faa1ce69066efcf Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Thu, 8 Oct 2020 13:46:16 -0400 Subject: [PATCH 034/178] Use WIP version of collections_requirements.yml Pulled from ansible-builder/test/data/awx --- requirements/collections_requirements.yml | 25 +++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/requirements/collections_requirements.yml b/requirements/collections_requirements.yml index 2a8f46fa24..f750aafce4 100644 --- a/requirements/collections_requirements.yml +++ b/requirements/collections_requirements.yml @@ -1,12 +1,21 @@ --- +# from https://github.com/ansible/awx/blob/devel/ +# requirements/collections_requirements.yml collections: - name: awx.awx - - name: azure.azcollection - - name: amazon.aws - - name: theforeman.foreman - - name: google.cloud - - name: openstack.cloud - - name: community.vmware + - name: azure.azcollection # PR 220 is in 1.1.0 + - name: amazon.aws # PR 125 is in 1.1.0 + - name: theforeman.foreman # has requirements.txt (which -r to another file) + - name: google.cloud # has requirements.txt, mainly for google-auth + # forked from opendev.org + - name: https://github.com/AlanCoding/ansible-collections-openstack.git + version: ee_req_install + type: git + - name: community.vmware # has requirements.txt, but may add pyvcloud - name: ovirt.ovirt - - name: community.kubernetes # required for isolated management playbooks - - name: ansible.posix # required for isolated management playbooks + - name: https://github.com/ansible-collections/community.kubernetes.git + type: git + # adds openshift python lib + # needs kubectl for yum / dnf / apt-get + # needs to install snap, then use snap to install helm + - name: ansible.posix From 46f5cb6b7af8e9f1b0fb84f0de407aa33d7f74ba Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Thu, 8 Oct 2020 13:47:19 -0400 Subject: [PATCH 035/178] Install receptorctl in awx venv --- requirements/requirements_git.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/requirements_git.txt b/requirements/requirements_git.txt index 04eac859a4..74f1fb4a9e 100644 --- a/requirements/requirements_git.txt +++ b/requirements/requirements_git.txt @@ -1,2 +1,3 @@ git+https://github.com/ansible/system-certifi.git@devel#egg=certifi git+git://github.com/ansible/ansible-runner@devel#egg=ansible-runner +git+https://github.com/project-receptor/receptor.git@#egg=receptorctl&subdirectory=receptorctl From 490f719fd922f0deddcc50b9d77bb896035d19c7 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Thu, 8 Oct 2020 13:49:05 -0400 Subject: [PATCH 036/178] Add new ee container --- .../roles/sources/templates/docker-compose.yml.j2 | 14 ++++++++++++-- tools/docker-compose/receptor.cfg | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 tools/docker-compose/receptor.cfg diff --git a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 index 754bc803f9..ea330d66f8 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 @@ -33,6 +33,7 @@ services: - "../../docker-compose/_sources/local_settings.py:/etc/tower/conf.d/local_settings.py" - "../../docker-compose/_sources/SECRET_KEY:/etc/tower/SECRET_KEY" - "redis_socket:/var/run/redis/:rw" + - "receptor:/var/run/receptor/" - "/sys/fs/cgroup:/sys/fs/cgroup" privileged: true tty: true @@ -42,8 +43,16 @@ services: # build: # context: ./docker-compose # dockerfile: Dockerfile-logstash - - # Postgres Database Container + ee: + image: quay.io/awx/ee + user: ${CURRENT_UID} + volumes: + - "./docker-compose/receptor.cfg:/receptor.cfg" + - "receptor:/var/run/receptor/" + command: + - receptor + - --config + - /receptor.cfg postgres: image: postgres:12 container_name: tools_postgres_1 @@ -66,3 +75,4 @@ services: volumes: awx_db: redis_socket: + receptor: diff --git a/tools/docker-compose/receptor.cfg b/tools/docker-compose/receptor.cfg new file mode 100644 index 0000000000..137d15cdf6 --- /dev/null +++ b/tools/docker-compose/receptor.cfg @@ -0,0 +1,15 @@ +--- +- log-level: debug + +- control-service: + service: control + filename: /var/run/receptor/receptor.sock + +- tcp-listener: + port: 2222 + +- work-command: + worktype: worker + command: ansible-runner + params: worker + allowruntimeparams: true From 82a641e173e3e6e2db8e9c32eb45483848d6b0d1 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Thu, 8 Oct 2020 13:49:30 -0400 Subject: [PATCH 037/178] Add AWX EE definition --- execution-environment.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 execution-environment.yml diff --git a/execution-environment.yml b/execution-environment.yml new file mode 100644 index 0000000000..79efbcbdb8 --- /dev/null +++ b/execution-environment.yml @@ -0,0 +1,10 @@ +--- +version: 1 +dependencies: + galaxy: requirements/collections_requirements.yml +additional_build_steps: + prepend: + - RUN pip3 install --upgrade pip setuptools + append: + - COPY --from=quay.io/shanemcd/receptor /usr/bin/receptor /usr/bin/receptor + - RUN mkdir -m 0770 /var/run/receptor From fd9373a9ec1f4cfff59b5bcd0fc661e11e897734 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Thu, 8 Oct 2020 16:46:31 -0400 Subject: [PATCH 038/178] Use official receptor image --- execution-environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/execution-environment.yml b/execution-environment.yml index 79efbcbdb8..ad1005cc2f 100644 --- a/execution-environment.yml +++ b/execution-environment.yml @@ -6,5 +6,5 @@ additional_build_steps: prepend: - RUN pip3 install --upgrade pip setuptools append: - - COPY --from=quay.io/shanemcd/receptor /usr/bin/receptor /usr/bin/receptor + - COPY --from=quay.io/project-receptor/receptor /usr/bin/receptor /usr/bin/receptor - RUN mkdir -m 0770 /var/run/receptor From 5c2b2dea0c280e9a9dc1843f413f3b3c4fa32e65 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Thu, 8 Oct 2020 18:33:43 -0400 Subject: [PATCH 039/178] REVERT ME: Install community.general in image This is needed for the wait_fors in the launch scripts to work --- tools/ansible/roles/dockerfile/templates/Dockerfile.j2 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index 6fa2c08cd4..ecdcb32733 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -139,6 +139,9 @@ RUN curl -L -o /usr/bin/tini https://github.com/krallin/tini/releases/download/v RUN python3 -m ensurepip && pip3 install "virtualenv < 20" supervisor {% if build_dev|bool %}flake8{% endif %} && \ pip3 install --no-cache-dir https://github.com/ansible/ansible/archive/${ANSIBLE_BRANCH}.tar.gz +# TODO: Remove this once launch script removes need for postres modules +RUN ansible-galaxy collection install --collections-path /usr/share/ansible/collections community.general + RUN rm -rf /root/.cache && rm -rf /tmp/* # Install Receptor From f554f45288a23acc25529c8542e4312ac55c4719 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Fri, 9 Oct 2020 19:16:24 -0400 Subject: [PATCH 040/178] Add license for receptor --- docs/licenses/receptor.txt | 168 +++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 docs/licenses/receptor.txt diff --git a/docs/licenses/receptor.txt b/docs/licenses/receptor.txt new file mode 100644 index 0000000000..bb0a7c7983 --- /dev/null +++ b/docs/licenses/receptor.txt @@ -0,0 +1,168 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. From 966bb6fc74979c75d3c90c3978d6b8c93e5545af Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Fri, 9 Oct 2020 19:17:55 -0400 Subject: [PATCH 041/178] Back to green --- awx/main/models/credential/injectors.py | 4 +- awx/main/models/inventory.py | 2 +- awx/main/tasks.py | 3 + .../test_inventory_source_injectors.py | 5 +- awx/main/tests/unit/test_tasks.py | 65 +++++++------------ 5 files changed, 34 insertions(+), 45 deletions(-) diff --git a/awx/main/models/credential/injectors.py b/awx/main/models/credential/injectors.py index 90615f2d66..4d7ef26054 100644 --- a/awx/main/models/credential/injectors.py +++ b/awx/main/models/credential/injectors.py @@ -35,8 +35,8 @@ def gce(cred, env, private_data_dir): json.dump(json_cred, f, indent=2) f.close() os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) - env['GCE_CREDENTIALS_FILE_PATH'] = path - env['GCP_SERVICE_ACCOUNT_FILE'] = path + env['GCE_CREDENTIALS_FILE_PATH'] = os.path.join('/runner', os.path.basename(path)) + env['GCP_SERVICE_ACCOUNT_FILE'] = os.path.join('/runner', os.path.basename(path)) # Handle env variables for new module types. # This includes gcp_compute inventory plugin and diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 5305e6e532..94ddcc4e90 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1544,7 +1544,7 @@ class openstack(PluginFileInjector): env = super(openstack, self).get_plugin_env(inventory_update, private_data_dir, private_data_files) credential = inventory_update.get_cloud_credential() cred_data = private_data_files['credentials'] - env['OS_CLIENT_CONFIG_FILE'] = cred_data[credential] + env['OS_CLIENT_CONFIG_FILE'] = os.path.join('/runner', os.path.basename(cred_data[credential])) return env diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 1c5b04fff9..bd4c5bd81c 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1102,6 +1102,9 @@ class BaseTask(object): # Also set environment variables configured in AWX_TASK_ENV setting. for key, value in settings.AWX_TASK_ENV.items(): env[key] = str(value) + + env['AWX_PRIVATE_DATA_DIR'] = private_data_dir + return env def should_use_resource_profiling(self, job): diff --git a/awx/main/tests/functional/test_inventory_source_injectors.py b/awx/main/tests/functional/test_inventory_source_injectors.py index fc28c92294..84660c79e3 100644 --- a/awx/main/tests/functional/test_inventory_source_injectors.py +++ b/awx/main/tests/functional/test_inventory_source_injectors.py @@ -110,7 +110,8 @@ def read_content(private_data_dir, raw_env, inventory_update): continue # Ansible runner abs_file_path = os.path.join(private_data_dir, filename) file_aliases[abs_file_path] = filename - if abs_file_path in inverse_env: + runner_path = os.path.join('/runner', os.path.basename(abs_file_path)) + if runner_path in inverse_env: referenced_paths.add(abs_file_path) alias = 'file_reference' for i in range(10): @@ -121,7 +122,7 @@ def read_content(private_data_dir, raw_env, inventory_update): raise RuntimeError('Test not able to cope with >10 references by env vars. ' 'Something probably went very wrong.') file_aliases[abs_file_path] = alias - for env_key in inverse_env[abs_file_path]: + for env_key in inverse_env[runner_path]: env[env_key] = '{{{{ {} }}}}'.format(alias) try: with open(abs_file_path, 'r') as f: diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 053745cc64..6b6811c86f 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -347,11 +347,12 @@ def pytest_generate_tests(metafunc): ) -def parse_extra_vars(args): +def parse_extra_vars(args, private_data_dir): extra_vars = {} for chunk in args: - if chunk.startswith('@/tmp/'): - with open(chunk.strip('@'), 'r') as f: + if chunk.startswith('@/runner/'): + local_path = os.path.join(private_data_dir, os.path.basename(chunk.strip('@'))) + with open(local_path, 'r') as f: extra_vars.update(yaml.load(f, Loader=SafeLoader)) return extra_vars @@ -597,7 +598,7 @@ class TestGenericRun(): assert resource_profiling_params['resource_profiling_cpu_poll_interval'] == '0.25' assert resource_profiling_params['resource_profiling_memory_poll_interval'] == '0.25' assert resource_profiling_params['resource_profiling_pid_poll_interval'] == '0.25' - assert resource_profiling_params['resource_profiling_results_dir'] == '/fake_private_data_dir/artifacts/playbook_profiling' + assert resource_profiling_params['resource_profiling_results_dir'] == '/runner/artifacts/playbook_profiling' @pytest.mark.parametrize("scenario, profiling_enabled", [ @@ -656,30 +657,6 @@ class TestGenericRun(): env = task.build_env(job, private_data_dir) assert env['FOO'] == 'BAR' - def test_valid_custom_virtualenv(self, patch_Job, private_data_dir): - job = Job(project=Project(), inventory=Inventory()) - - with TemporaryDirectory(dir=settings.BASE_VENV_PATH) as tempdir: - job.project.custom_virtualenv = tempdir - os.makedirs(os.path.join(tempdir, 'lib')) - os.makedirs(os.path.join(tempdir, 'bin', 'activate')) - - task = tasks.RunJob() - env = task.build_env(job, private_data_dir) - - assert env['PATH'].startswith(os.path.join(tempdir, 'bin')) - assert env['VIRTUAL_ENV'] == tempdir - - def test_invalid_custom_virtualenv(self, patch_Job, private_data_dir): - job = Job(project=Project(), inventory=Inventory()) - job.project.custom_virtualenv = '/var/lib/awx/venv/missing' - task = tasks.RunJob() - - with pytest.raises(tasks.InvalidVirtualenvError) as e: - task.build_env(job, private_data_dir) - - assert 'Invalid virtual environment selected: /var/lib/awx/venv/missing' == str(e.value) - class TestAdhocRun(TestJobExecution): @@ -1203,7 +1180,9 @@ class TestJobCredentials(TestJobExecution): credential.credential_type.inject_credential( credential, env, safe_env, [], private_data_dir ) - json_data = json.load(open(env['GCE_CREDENTIALS_FILE_PATH'], 'rb')) + runner_path = env['GCE_CREDENTIALS_FILE_PATH'] + local_path = os.path.join(private_data_dir, os.path.basename(runner_path)) + json_data = json.load(open(local_path, 'rb')) assert json_data['type'] == 'service_account' assert json_data['private_key'] == self.EXAMPLE_PRIVATE_KEY assert json_data['client_email'] == 'bob' @@ -1344,7 +1323,7 @@ class TestJobCredentials(TestJobExecution): ) config = configparser.ConfigParser() - config.read(env['OVIRT_INI_PATH']) + config.read(os.path.join(private_data_dir, os.path.basename(env['OVIRT_INI_PATH']))) assert config.get('ovirt', 'ovirt_url') == 'some-ovirt-host.example.org' assert config.get('ovirt', 'ovirt_username') == 'bob' assert config.get('ovirt', 'ovirt_password') == 'some-pass' @@ -1577,7 +1556,7 @@ class TestJobCredentials(TestJobExecution): credential.credential_type.inject_credential( credential, {}, {}, args, private_data_dir ) - extra_vars = parse_extra_vars(args) + extra_vars = parse_extra_vars(args, private_data_dir) assert extra_vars["api_token"] == "ABC123" assert hasattr(extra_vars["api_token"], '__UNSAFE__') @@ -1612,7 +1591,7 @@ class TestJobCredentials(TestJobExecution): credential.credential_type.inject_credential( credential, {}, {}, args, private_data_dir ) - extra_vars = parse_extra_vars(args) + extra_vars = parse_extra_vars(args, private_data_dir) assert extra_vars["turbo_button"] == "True" return ['successful', 0] @@ -1647,7 +1626,7 @@ class TestJobCredentials(TestJobExecution): credential.credential_type.inject_credential( credential, {}, {}, args, private_data_dir ) - extra_vars = parse_extra_vars(args) + extra_vars = parse_extra_vars(args, private_data_dir) assert extra_vars["turbo_button"] == "FAST!" @@ -1687,7 +1666,7 @@ class TestJobCredentials(TestJobExecution): credential, {}, {}, args, private_data_dir ) - extra_vars = parse_extra_vars(args) + extra_vars = parse_extra_vars(args, private_data_dir) assert extra_vars["password"] == "SUPER-SECRET-123" def test_custom_environment_injectors_with_file(self, private_data_dir): @@ -1722,7 +1701,8 @@ class TestJobCredentials(TestJobExecution): credential, env, {}, [], private_data_dir ) - assert open(env['MY_CLOUD_INI_FILE'], 'r').read() == '[mycloud]\nABC123' + path = os.path.join(private_data_dir, os.path.basename(env['MY_CLOUD_INI_FILE'])) + assert open(path, 'r').read() == '[mycloud]\nABC123' def test_custom_environment_injectors_with_unicode_content(self, private_data_dir): value = 'Iñtërnâtiônàlizætiøn' @@ -1746,7 +1726,8 @@ class TestJobCredentials(TestJobExecution): credential, env, {}, [], private_data_dir ) - assert open(env['MY_CLOUD_INI_FILE'], 'r').read() == value + path = os.path.join(private_data_dir, os.path.basename(env['MY_CLOUD_INI_FILE'])) + assert open(path, 'r').read() == value def test_custom_environment_injectors_with_files(self, private_data_dir): some_cloud = CredentialType( @@ -1786,8 +1767,10 @@ class TestJobCredentials(TestJobExecution): credential, env, {}, [], private_data_dir ) - assert open(env['MY_CERT_INI_FILE'], 'r').read() == '[mycert]\nCERT123' - assert open(env['MY_KEY_INI_FILE'], 'r').read() == '[mykey]\nKEY123' + cert_path = os.path.join(private_data_dir, os.path.basename(env['MY_CERT_INI_FILE'])) + key_path = os.path.join(private_data_dir, os.path.basename(env['MY_KEY_INI_FILE'])) + assert open(cert_path, 'r').read() == '[mycert]\nCERT123' + assert open(key_path, 'r').read() == '[mykey]\nKEY123' def test_multi_cloud(self, private_data_dir): gce = CredentialType.defaults['gce']() @@ -1826,7 +1809,8 @@ class TestJobCredentials(TestJobExecution): assert env['AZURE_AD_USER'] == 'bob' assert env['AZURE_PASSWORD'] == 'secret' - json_data = json.load(open(env['GCE_CREDENTIALS_FILE_PATH'], 'rb')) + path = os.path.join(private_data_dir, os.path.basename(env['GCE_CREDENTIALS_FILE_PATH'])) + json_data = json.load(open(path, 'rb')) assert json_data['type'] == 'service_account' assert json_data['private_key'] == self.EXAMPLE_PRIVATE_KEY assert json_data['client_email'] == 'bob' @@ -2307,7 +2291,8 @@ class TestInventoryUpdateCredentials(TestJobExecution): private_data_files = task.build_private_data_files(inventory_update, private_data_dir) env = task.build_env(inventory_update, private_data_dir, False, private_data_files) - shade_config = open(env['OS_CLIENT_CONFIG_FILE'], 'r').read() + path = os.path.join(private_data_dir, os.path.basename(env['OS_CLIENT_CONFIG_FILE'])) + shade_config = open(path, 'r').read() assert '\n'.join([ 'clouds:', ' devstack:', From 1f4a45a6987c59892af00f4b9d47244431ac6ae7 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Sat, 10 Oct 2020 11:01:40 -0400 Subject: [PATCH 042/178] Remove "pull" field from EE mixin I think this should go on the EE definition itself --- awx/api/serializers.py | 4 ++-- awx/main/migrations/0124_execution_environments.py | 10 ---------- awx/main/models/mixins.py | 1 - 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 435f2ad444..dcf403c47e 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -649,7 +649,7 @@ class UnifiedJobTemplateSerializer(BaseSerializer): class Meta: model = UnifiedJobTemplate fields = ('*', 'last_job_run', 'last_job_failed', - 'next_job_run', 'status', 'execution_environment', 'pull') + 'next_job_run', 'status', 'execution_environment') def get_related(self, obj): res = super(UnifiedJobTemplateSerializer, self).get_related(obj) @@ -716,7 +716,7 @@ class UnifiedJobSerializer(BaseSerializer): class Meta: model = UnifiedJob fields = ('*', 'unified_job_template', 'launch_type', 'status', - 'execution_environment', 'pull', + 'execution_environment', 'failed', 'started', 'finished', 'canceled_on', 'elapsed', 'job_args', 'job_cwd', 'job_env', 'job_explanation', 'execution_node', 'controller_node', diff --git a/awx/main/migrations/0124_execution_environments.py b/awx/main/migrations/0124_execution_environments.py index 982b1b21e8..18aad9a174 100644 --- a/awx/main/migrations/0124_execution_environments.py +++ b/awx/main/migrations/0124_execution_environments.py @@ -16,16 +16,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AddField( - model_name='unifiedjob', - name='pull', - field=models.BooleanField(default=True), - ), - migrations.AddField( - model_name='unifiedjobtemplate', - name='pull', - field=models.BooleanField(default=True), - ), migrations.CreateModel( name='ExecutionEnvironment', fields=[ diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index 54c8a000a4..1cd1366a92 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -454,7 +454,6 @@ class ExecutionEnvironmentMixin(models.Model): related_name='%(class)ss', help_text=_('The container image to be used for execution.'), ) - pull = models.BooleanField(default=True) class CustomVirtualEnvMixin(models.Model): From ee1d322336e233e4f320444166a560c044eef1f6 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Sat, 10 Oct 2020 11:02:16 -0400 Subject: [PATCH 043/178] WIP: Module for EEs --- .../plugins/modules/tower_ad_hoc_command.py | 5 ++ .../modules/tower_execution_environment.py | 86 +++++++++++++++++++ .../plugins/modules/tower_inventory_source.py | 4 + .../plugins/modules/tower_job_template.py | 4 + .../plugins/modules/tower_organization.py | 4 + .../plugins/modules/tower_project.py | 4 + .../modules/tower_workflow_job_template.py | 4 + awx_collection/test/awx/test_completeness.py | 1 + 8 files changed, 112 insertions(+) create mode 100644 awx_collection/plugins/modules/tower_execution_environment.py diff --git a/awx_collection/plugins/modules/tower_ad_hoc_command.py b/awx_collection/plugins/modules/tower_ad_hoc_command.py index 00f16d9f13..2d099b2b1d 100644 --- a/awx_collection/plugins/modules/tower_ad_hoc_command.py +++ b/awx_collection/plugins/modules/tower_ad_hoc_command.py @@ -28,6 +28,11 @@ options: - Job_type to use for the ad hoc command. type: str choices: [ 'run', 'check' ] + execution_environment: + description: + - Execution Environment to use for the ad hoc command. + required: False + type: str inventory: description: - Inventory to use for the ad hoc command. diff --git a/awx_collection/plugins/modules/tower_execution_environment.py b/awx_collection/plugins/modules/tower_execution_environment.py new file mode 100644 index 0000000000..978d23298c --- /dev/null +++ b/awx_collection/plugins/modules/tower_execution_environment.py @@ -0,0 +1,86 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2020, Shane McDonald +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_execution_environment +author: "Shane McDonald" +short_description: create, update, or destroy Execution Environments in Ansible Tower. +description: + - Create, update, or destroy Execution Environments in Ansible Tower. See + U(https://www.ansible.com/tower) for an overview. +options: + image: + description: + - The fully qualified name of the container image + required: True + type: str + state: + description: + - Desired state of the resource. + choices: ["present", "absent"] + default: "present" + type: str + credential: + description: + - Name of the credential to use for the job template. + - Deprecated, use 'credentials'. + type: str + description: + description: + - Description to use for the job template. + type: str + organization: + description: + - TODO + type: str +extends_documentation_fragment: awx.awx.auth +''' + + +EXAMPLES = ''' +- name: Add EE to Tower + tower_execution_environment: + image: quay.io/awx/ee +''' + + +from ..module_utils.tower_api import TowerAPIModule +import json + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + image=dict(required=True), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + image = module.params.get('image') + state = module.params.get('state') + + existing_item = module.get_one('execution_environments', name_or_id=image) + + if state == 'absent': + module.delete_if_needed(image) + + module.create_or_update_if_needed(existing_item, image, endpoint='execution_environments', item_type='execution_environment') + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_inventory_source.py b/awx_collection/plugins/modules/tower_inventory_source.py index 5945d411d8..ceb0e8b5a6 100644 --- a/awx_collection/plugins/modules/tower_inventory_source.py +++ b/awx_collection/plugins/modules/tower_inventory_source.py @@ -73,6 +73,10 @@ options: description: - Credential to use for the source. type: str + execution_environment: + description: + - Execution Environment to use for the source. + type: str overwrite: description: - Delete child groups and hosts not found in source. diff --git a/awx_collection/plugins/modules/tower_job_template.py b/awx_collection/plugins/modules/tower_job_template.py index 787c145a20..b2b1530d6f 100644 --- a/awx_collection/plugins/modules/tower_job_template.py +++ b/awx_collection/plugins/modules/tower_job_template.py @@ -60,6 +60,10 @@ options: description: - Path to the playbook to use for the job template within the project provided. type: str + execution_environment: + description: + - Execution Environment to use for the JT. + type: str credential: description: - Name of the credential to use for the job template. diff --git a/awx_collection/plugins/modules/tower_organization.py b/awx_collection/plugins/modules/tower_organization.py index 0402056bbf..7d88d2a421 100644 --- a/awx_collection/plugins/modules/tower_organization.py +++ b/awx_collection/plugins/modules/tower_organization.py @@ -36,6 +36,10 @@ options: - Local absolute file path containing a custom Python virtualenv to use. type: str default: '' + default_environment: + description: + - Default Execution Environment to use for the Organization. + type: str max_hosts: description: - The max hosts allowed in this organizations diff --git a/awx_collection/plugins/modules/tower_project.py b/awx_collection/plugins/modules/tower_project.py index 76cef63f10..f6ab7d144c 100644 --- a/awx_collection/plugins/modules/tower_project.py +++ b/awx_collection/plugins/modules/tower_project.py @@ -31,6 +31,10 @@ options: description: - Description to use for the project. type: str + execution_environment: + description: + - Execution Environment to use for the project. + type: str scm_type: description: - Type of SCM resource. diff --git a/awx_collection/plugins/modules/tower_workflow_job_template.py b/awx_collection/plugins/modules/tower_workflow_job_template.py index 7836b42cc4..54b6695b03 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template.py @@ -40,6 +40,10 @@ options: description: - Variables which will be made available to jobs ran inside the workflow. type: dict + execution_environment: + description: + - Execution Environment to use for the WFJT. + type: str organization: description: - Organization the workflow job template exists in. diff --git a/awx_collection/test/awx/test_completeness.py b/awx_collection/test/awx/test_completeness.py index 467ee9357b..7ff6e22a31 100644 --- a/awx_collection/test/awx/test_completeness.py +++ b/awx_collection/test/awx/test_completeness.py @@ -138,6 +138,7 @@ def determine_state(module_id, endpoint, module, parameter, api_option, module_o if not api_option and module_option and module_option.get('type', 'str') == 'list': return "OK, Field appears to be relation" # TODO, at some point try and check the object model to confirm its actually a relation + return cause_error('Failed, option mismatch') # We made it through all of the checks so we are ok From ecaa66c13be4b0f2ad865fabd485798a4f5560cc Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Sat, 10 Oct 2020 11:09:40 -0400 Subject: [PATCH 044/178] Fix linter --- awx/main/tests/unit/test_tasks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 6b6811c86f..2fff642eef 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -6,7 +6,6 @@ import os import shutil import tempfile -from backports.tempfile import TemporaryDirectory import fcntl from unittest import mock import pytest From 9786dc08d3f1bde17b5ebdb633ea96771e8e259a Mon Sep 17 00:00:00 2001 From: nixocio Date: Fri, 2 Oct 2020 17:27:12 -0400 Subject: [PATCH 045/178] Add organization as part of creating/editing an execution environments Add organization as part of creating/editing an execution environments If one is a `system admin` the Organization is an optional field. Not providing an Organization makes the execution environment globally available. If one is a `org admin` the Organization is a required field. See: https://github.com/ansible/awx/issues/7887 --- .../components/Lookup/OrganizationLookup.jsx | 2 + .../ExecutionEnvironmentAdd.jsx | 23 ++++--- .../ExecutionEnvironmentAdd.test.jsx | 7 +- .../ExecutionEnvironmentEdit.jsx | 19 +++-- .../ExecutionEnvironmentEdit.test.jsx | 7 ++ .../shared/ExecutionEnvironmentForm.jsx | 69 +++++++++++++++---- .../shared/ExecutionEnvironmentForm.test.jsx | 21 +++++- 7 files changed, 118 insertions(+), 30 deletions(-) diff --git a/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx b/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx index 3fb443426e..8252c9035c 100644 --- a/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx +++ b/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx @@ -30,6 +30,7 @@ function OrganizationLookup({ history, autoPopulate, isDisabled, + helperText, }) { const autoPopulateLookup = useAutoPopulateLookup(onChange); @@ -79,6 +80,7 @@ function OrganizationLookup({ isRequired={required} validated={isValid ? 'default' : 'error'} label={i18n._(t`Organization`)} + helperText={helperText} > - + + {({ me }) => ( + + )} + diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx index 781501b19e..5396746223 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx @@ -8,6 +8,11 @@ import ExecutionEnvironmentAdd from './ExecutionEnvironmentAdd'; jest.mock('../../../api'); +const mockMe = { + is_superuser: true, + is_system_auditor: false, +}; + const executionEnvironmentData = { credential: 4, description: 'A simple EE', @@ -29,7 +34,7 @@ describe('', () => { initialEntries: ['/execution_environments'], }); await act(async () => { - wrapper = mountWithContexts(, { + wrapper = mountWithContexts(, { context: { router: { history } }, }); }); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx index 6d8cbc9520..ea4943b2da 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx @@ -4,6 +4,7 @@ import { useHistory } from 'react-router-dom'; import { CardBody } from '../../../components/Card'; import { ExecutionEnvironmentsAPI } from '../../../api'; import ExecutionEnvironmentForm from '../shared/ExecutionEnvironmentForm'; +import { Config } from '../../../contexts/Config'; function ExecutionEnvironmentEdit({ executionEnvironment }) { const history = useHistory(); @@ -15,6 +16,7 @@ function ExecutionEnvironmentEdit({ executionEnvironment }) { await ExecutionEnvironmentsAPI.update(executionEnvironment.id, { ...values, credential: values.credential ? values.credential.id : null, + organization: values.organization ? values.organization.id : null, }); history.push(detailsUrl); } catch (error) { @@ -27,12 +29,17 @@ function ExecutionEnvironmentEdit({ executionEnvironment }) { }; return ( - + + {({ me }) => ( + + )} + ); } diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.test.jsx index 2d4f916aba..94eff7616f 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.test.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.test.jsx @@ -9,6 +9,11 @@ import ExecutionEnvironmentEdit from './ExecutionEnvironmentEdit'; jest.mock('../../../api'); +const mockMe = { + is_superuser: true, + is_system_auditor: false, +}; + const executionEnvironmentData = { id: 42, credential: { id: 4 }, @@ -31,6 +36,7 @@ describe('', () => { wrapper = mountWithContexts( , { context: { router: { history } }, @@ -53,6 +59,7 @@ describe('', () => { expect(ExecutionEnvironmentsAPI.update).toHaveBeenCalledWith(42, { ...updateExecutionEnvironmentData, credential: null, + organization: null, }); }); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx index a5170ae3bb..5d7a16d217 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx @@ -1,18 +1,42 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { func, shape } from 'prop-types'; -import { Formik, useField } from 'formik'; +import { Formik, useField, useFormikContext } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; - import { Form } from '@patternfly/react-core'; -import FormField, { FormSubmitError } from '../../../components/FormField'; -import FormActionGroup from '../../../components/FormActionGroup'; -import CredentialLookup from '../../../components/Lookup/CredentialLookup'; -import { url } from '../../../util/validators'; -import { FormColumnLayout } from '../../../components/FormLayout'; -function ExecutionEnvironmentFormFields({ i18n }) { - const [credentialField, , credentialHelpers] = useField('credential'); +import CredentialLookup from '../../../components/Lookup/CredentialLookup'; +import FormActionGroup from '../../../components/FormActionGroup'; +import FormField, { FormSubmitError } from '../../../components/FormField'; +import { FormColumnLayout } from '../../../components/FormLayout'; +import { OrganizationLookup } from '../../../components/Lookup'; +import { required, url } from '../../../util/validators'; + +function ExecutionEnvironmentFormFields({ i18n, me, executionEnvironment }) { + const [credentialField] = useField('credential'); + const [organizationField, organizationMeta, organizationHelpers] = useField({ + name: 'organization', + validate: + !me?.is_superuser && + required(i18n._(t`Select a value for this field`), i18n), + }); + + const { setFieldValue } = useFormikContext(); + + const onCredentialChange = useCallback( + value => { + setFieldValue('credential', value); + }, + [setFieldValue] + ); + + const onOrganizationChange = useCallback( + value => { + setFieldValue('organization', value); + }, + [setFieldValue] + ); + return ( <> + organizationHelpers.setTouched()} + onChange={onOrganizationChange} + value={organizationField.value} + required={!me.is_superuser} + helperText={ + me?.is_superuser + ? i18n._( + t`Leave this field blank to make the execution environment globally available.` + ) + : null + } + autoPopulate={!me?.is_superuser ? !executionEnvironment?.id : null} + /> + credentialHelpers.setValue(value)} + onChange={onCredentialChange} value={credentialField.value} /> @@ -46,19 +87,21 @@ function ExecutionEnvironmentForm({ onSubmit, onCancel, submitError, + me, ...rest }) { const initialValues = { image: executionEnvironment.image || '', description: executionEnvironment.description || '', - credential: executionEnvironment?.summary_fields?.credential || null, + credential: executionEnvironment.summary_fields?.credential || null, + organization: executionEnvironment.summary_fields?.organization || null, }; return ( onSubmit(values)}> {formik => (
- + {submitError && } ', () => { onCancel={onCancel} onSubmit={onSubmit} executionEnvironment={executionEnvironment} + me={mockMe} /> ); }); @@ -75,8 +81,8 @@ describe('', () => { expect(onSubmit).toHaveBeenCalledTimes(1); }); - test('should update form values', () => { - act(() => { + test('should update form values', async () => { + await act(async () => { wrapper.find('input#execution-environment-image').simulate('change', { target: { value: 'https://registry.com/image/container2', @@ -93,8 +99,19 @@ describe('', () => { id: 99, name: 'credential', }); + + wrapper.find('OrganizationLookup').invoke('onBlur')(); + wrapper.find('OrganizationLookup').invoke('onChange')({ + id: 3, + name: 'organization', + }); }); + wrapper.update(); + expect(wrapper.find('OrganizationLookup').prop('value')).toEqual({ + id: 3, + name: 'organization', + }); expect( wrapper.find('input#execution-environment-image').prop('value') ).toEqual('https://registry.com/image/container2'); From 6ff1424e8c924709b40ee30612eb8e0403164355 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 30 Nov 2020 18:53:15 -0500 Subject: [PATCH 046/178] Fix tests after rebasing in inventory update refactor --- awx/main/tests/functional/test_inventory_source_injectors.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/awx/main/tests/functional/test_inventory_source_injectors.py b/awx/main/tests/functional/test_inventory_source_injectors.py index 84660c79e3..bb26b7c029 100644 --- a/awx/main/tests/functional/test_inventory_source_injectors.py +++ b/awx/main/tests/functional/test_inventory_source_injectors.py @@ -215,9 +215,6 @@ def test_inventory_update_injected_content(this_kind, inventory, fake_credential f"'{inventory_filename}' file not found in inventory update runtime files {content.keys()}" env.pop('ANSIBLE_COLLECTIONS_PATHS', None) # collection paths not relevant to this test - env.pop('PYTHONPATH') - env.pop('VIRTUAL_ENV') - env.pop('PROOT_TMP_DIR') base_dir = os.path.join(DATA, 'plugins') if not os.path.exists(base_dir): os.mkdir(base_dir) From 14a8e3da5eeee42084890d59f7d6d23f0176a586 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Thu, 3 Dec 2020 11:52:25 -0500 Subject: [PATCH 047/178] WIP: containerized inventory updates. Thanks ALAN!! --- awx/main/models/inventory.py | 1 - awx/main/tasks.py | 9 ++++----- awx/settings/defaults.py | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 94ddcc4e90..28f4b09948 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1441,7 +1441,6 @@ class PluginFileInjector(object): def get_plugin_env(self, inventory_update, private_data_dir, private_data_files): env = self._get_shared_env(inventory_update, private_data_dir, private_data_files) - env['ANSIBLE_COLLECTIONS_PATHS'] = settings.AWX_ANSIBLE_COLLECTIONS_PATHS return env def build_private_data(self, inventory_update, private_data_dir): diff --git a/awx/main/tasks.py b/awx/main/tasks.py index bd4c5bd81c..0e793498a3 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -2539,9 +2539,6 @@ class RunInventoryUpdate(BaseTask): injector = InventorySource.injectors[inventory_update.source]() return injector.build_private_data(inventory_update, private_data_dir) - def build_execution_environment_params(self, inventory_update): - return {} # TODO: containerize inventory updates - def build_env(self, inventory_update, private_data_dir, isolated, private_data_files=None): """Build environment dictionary for ansible-inventory. @@ -2631,7 +2628,7 @@ class RunInventoryUpdate(BaseTask): args.append(source_location) args.append('--output') - args.append(os.path.join(private_data_dir, 'artifacts', 'output.json')) + args.append(os.path.join('/runner', 'artifacts', 'output.json')) if os.path.isdir(source_location): playbook_dir = source_location @@ -2667,8 +2664,10 @@ class RunInventoryUpdate(BaseTask): with open(inventory_path, 'w') as f: f.write(content) os.chmod(inventory_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + + inventory_path = os.path.join('/runner', injector.filename) elif src == 'scm': - inventory_path = os.path.join(private_data_dir, 'project', inventory_update.source_path) + inventory_path = os.path.join('/runner', 'project', inventory_update.source_path) elif src == 'custom': handle, inventory_path = tempfile.mkstemp(dir=private_data_dir) f = os.fdopen(handle, 'w') diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 0a2a7043d6..b6631165a1 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -59,7 +59,7 @@ DATABASES = { } } -AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE = 'quay.io/ansible/ansible-runner:devel' +AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE = 'quay.io/shanemcd/ee' AWX_CONTAINER_GROUP_K8S_API_TIMEOUT = 10 AWX_CONTAINER_GROUP_POD_LAUNCH_RETRIES = 100 From 69dcbe08656a046d91ffdb514dde991326408326 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Fri, 4 Dec 2020 11:26:56 -0500 Subject: [PATCH 048/178] More inventory update containerization fixes --- awx/main/tasks.py | 30 ++++++++++++++++-------------- awx/main/tests/unit/test_tasks.py | 3 ++- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 0e793498a3..433c7fb2f8 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -2546,13 +2546,9 @@ class RunInventoryUpdate(BaseTask): are accomplished by the inventory source injectors (in this method) or custom credential type injectors (in main run method). """ - base_env = super(RunInventoryUpdate, self).build_env( + env = super(RunInventoryUpdate, self).build_env( inventory_update, private_data_dir, isolated, private_data_files=private_data_files) - # TODO: this is able to run by turning off isolation - # the goal is to run it a container instead - env = dict(os.environ.items()) - env.update(base_env) if private_data_files is None: private_data_files = {} @@ -2623,17 +2619,20 @@ class RunInventoryUpdate(BaseTask): args = ['ansible-inventory', '--list', '--export'] # Add arguments for the source inventory file/script/thing - source_location = self.pseudo_build_inventory(inventory_update, private_data_dir) + rel_path = self.pseudo_build_inventory(inventory_update, private_data_dir) + container_location = os.path.join('/runner', rel_path) # TODO: make container paths elegant + source_location = os.path.join(private_data_dir, rel_path) + args.append('-i') - args.append(source_location) + args.append(container_location) args.append('--output') args.append(os.path.join('/runner', 'artifacts', 'output.json')) if os.path.isdir(source_location): - playbook_dir = source_location + playbook_dir = container_location else: - playbook_dir = os.path.dirname(source_location) + playbook_dir = os.path.dirname(container_location) args.extend(['--playbook-dir', playbook_dir]) if inventory_update.verbosity: @@ -2665,9 +2664,9 @@ class RunInventoryUpdate(BaseTask): f.write(content) os.chmod(inventory_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) - inventory_path = os.path.join('/runner', injector.filename) + rel_path = injector.filename elif src == 'scm': - inventory_path = os.path.join('/runner', 'project', inventory_update.source_path) + rel_path = os.path.join('project', inventory_update.source_path) elif src == 'custom': handle, inventory_path = tempfile.mkstemp(dir=private_data_dir) f = os.fdopen(handle, 'w') @@ -2676,7 +2675,9 @@ class RunInventoryUpdate(BaseTask): f.write(inventory_update.source_script.script) f.close() os.chmod(inventory_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) - return inventory_path + + rel_path = os.path.split(inventory_path)[-1] + return rel_path def build_cwd(self, inventory_update, private_data_dir): ''' @@ -2685,9 +2686,10 @@ class RunInventoryUpdate(BaseTask): - SCM, where source needs to live in the project folder ''' src = inventory_update.source + container_dir = '/runner' # TODO: make container paths elegant if src == 'scm' and inventory_update.source_project_update: - return os.path.join(private_data_dir, 'project') - return private_data_dir + return os.path.join(container_dir, 'project') + return container_dir def build_playbook_path_relative_to_cwd(self, inventory_update, private_data_dir): return None diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 2fff642eef..d6e70fe3fd 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -2090,7 +2090,8 @@ class TestInventoryUpdateCredentials(TestJobExecution): assert '-i' in ' '.join(args) script = args[args.index('-i') + 1] - with open(script, 'r') as f: + host_script = script.replace('/runner', private_data_dir) + with open(host_script, 'r') as f: assert f.read() == inventory_update.source_script.script assert env['FOO'] == 'BAR' if with_credential: From b716e2b099375a4e0df210f46e37a4fbc6032bf5 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Mon, 7 Dec 2020 15:11:29 -0500 Subject: [PATCH 049/178] Make insights integration tests pass again --- awx/main/tasks.py | 13 +++++++++++++ awx/settings/defaults.py | 2 ++ 2 files changed, 15 insertions(+) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 433c7fb2f8..80ae17d474 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1893,6 +1893,19 @@ class RunJob(BaseTask): return False return getattr(settings, 'AWX_PROOT_ENABLED', False) + def build_execution_environment_params(self, instance): + params = super(RunJob, self).build_execution_environment_params(instance) + # If this has an insights agent and it is not already mounted then show it + insights_dir = os.path.dirname(settings.INSIGHTS_SYSTEM_ID_FILE) + if instance.use_fact_cache and os.path.exists(insights_dir): + logger.info('not parent of others') + params.setdefault('container_volume_mounts', []) + params['container_volume_mounts'].extend([ + f"{insights_dir}:{insights_dir}:Z", + ]) + + return params + def pre_run_hook(self, job, private_data_dir): super(RunJob, self).pre_run_hook(job, private_data_dir) if job.inventory is None: diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index b6631165a1..058d6aeaee 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -785,6 +785,8 @@ TOWER_URL_BASE = "https://towerhost" INSIGHTS_URL_BASE = "https://example.org" INSIGHTS_AGENT_MIME = 'application/example' +# See https://github.com/ansible/awx-facts-playbooks +INSIGHTS_SYSTEM_ID_FILE='/etc/redhat-access-insights/machine-id' TOWER_SETTINGS_MANIFEST = {} From 54681eb0555d77cfbb434195e5a16268db7d8e44 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Tue, 8 Dec 2020 20:52:35 -0500 Subject: [PATCH 050/178] Add utility method to get controller private_data_dir --- awxkit/awxkit/api/pages/unified_jobs.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/awxkit/awxkit/api/pages/unified_jobs.py b/awxkit/awxkit/api/pages/unified_jobs.py index 8e07b71de9..20c6175ed3 100644 --- a/awxkit/awxkit/api/pages/unified_jobs.py +++ b/awxkit/awxkit/api/pages/unified_jobs.py @@ -135,6 +135,28 @@ class UnifiedJob(HasStatus, base.Base): raise return args + @property + def controller_dir(self): + """Returns the path to the private_data_dir on the controller node for the job + This can be used if trying to shell in and inspect the files used by the job + Cannot use job_cwd, because that is path inside EE container + """ + self.get() + job_args = self.job_args + expected_prefix = '/tmp/awx_{}'.format(self.id) + for arg1, arg2 in zip(job_args[:-1], job_args[1:]): + if arg1 == '-v': + if ':' in arg2: + host_loc = arg2.split(':')[0] + if host_loc.startswith(expected_prefix): + return host_loc + raise RuntimeError( + 'Could not find a controller private_data_dir for this job. ' + 'Searched for volume mount to {} inside of args {}'.format( + expected_prefix, job_args + ) + ) + class UnifiedJobs(page.PageList, UnifiedJob): From 2302496724e1c061dd0ed59babd2148d8b98140b Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Wed, 9 Dec 2020 09:31:59 -0500 Subject: [PATCH 051/178] Add back in the subversion requirement --- execution-environment.yml | 1 + requirements/bindep_requirements.txt | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 requirements/bindep_requirements.txt diff --git a/execution-environment.yml b/execution-environment.yml index ad1005cc2f..4e4d10cb36 100644 --- a/execution-environment.yml +++ b/execution-environment.yml @@ -2,6 +2,7 @@ version: 1 dependencies: galaxy: requirements/collections_requirements.yml + system: requirements/bindep_requirements.txt additional_build_steps: prepend: - RUN pip3 install --upgrade pip setuptools diff --git a/requirements/bindep_requirements.txt b/requirements/bindep_requirements.txt new file mode 100644 index 0000000000..fe8e3c519b --- /dev/null +++ b/requirements/bindep_requirements.txt @@ -0,0 +1,2 @@ +subversion [platform:rpm] +subversion [platform:dpkg] From 7a433f4e8f41b19039e2a55b75e36a0ed6fdc19e Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Wed, 9 Dec 2020 16:36:48 -0500 Subject: [PATCH 052/178] Change the shebang back to just python --- awx/main/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 80ae17d474..e7a6aa711a 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1137,7 +1137,7 @@ class BaseTask(object): fn = os.path.join(path, 'hosts') with open(fn, 'w') as f: os.chmod(fn, stat.S_IRUSR | stat.S_IXUSR | stat.S_IWUSR) - f.write('#! /usr/bin/env python3\n# -*- coding: utf-8 -*-\nprint(%r)\n' % json_data) + f.write('#! /usr/bin/env python\n# -*- coding: utf-8 -*-\nprint(%r)\n' % json_data) return fn def build_args(self, instance, private_data_dir, passwords): From c0faa39b537a60343b2f4d0adb1683df29a9116c Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Thu, 10 Dec 2020 09:34:40 -0500 Subject: [PATCH 053/178] Remove files moved to the ansible/awx-ee repo These have been moved to: https://github.com/ansible/awx-ee that will be the home for the processes needed to build this execution environment. --- execution-environment.yml | 11 ----------- requirements/bindep_requirements.txt | 2 -- 2 files changed, 13 deletions(-) delete mode 100644 execution-environment.yml delete mode 100644 requirements/bindep_requirements.txt diff --git a/execution-environment.yml b/execution-environment.yml deleted file mode 100644 index 4e4d10cb36..0000000000 --- a/execution-environment.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -version: 1 -dependencies: - galaxy: requirements/collections_requirements.yml - system: requirements/bindep_requirements.txt -additional_build_steps: - prepend: - - RUN pip3 install --upgrade pip setuptools - append: - - COPY --from=quay.io/project-receptor/receptor /usr/bin/receptor /usr/bin/receptor - - RUN mkdir -m 0770 /var/run/receptor diff --git a/requirements/bindep_requirements.txt b/requirements/bindep_requirements.txt deleted file mode 100644 index fe8e3c519b..0000000000 --- a/requirements/bindep_requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -subversion [platform:rpm] -subversion [platform:dpkg] From c1133b3f6d6d5427b7b99b2f615f9ad8489b4151 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Tue, 17 Nov 2020 14:46:42 -0500 Subject: [PATCH 054/178] Add in more model changes around execution environments - a new unique name field to EE - a new configure-Tower-in-Tower setting DEFAULT_EXECUTION_ENVIRONMENT - an Org-level execution_environment_admin_role - a default_environment field on Project - a new Container Registry credential type - order EEs by reverse of the created timestamp - a method to resolve which EE to use on jobs --- awx/api/serializers.py | 11 +++-- awx/conf/fields.py | 1 + awx/main/access.py | 2 + awx/main/conf.py | 13 ++++++ .../0125_more_ee_modeling_changes.py | 41 +++++++++++++++++++ awx/main/models/credential/__init__.py | 32 ++++++++++++++- awx/main/models/execution_environments.py | 7 ++-- awx/main/models/organization.py | 3 ++ awx/main/models/projects.py | 9 ++++ awx/main/models/rbac.py | 2 + awx/main/models/unified_jobs.py | 20 +++++++++ awx/main/tests/functional/test_credential.py | 1 + awx/settings/defaults.py | 1 + .../api/pages/execution_environments.py | 2 +- 14 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 awx/main/migrations/0125_more_ee_modeling_changes.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index dcf403c47e..68e507a6d2 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -107,8 +107,8 @@ SUMMARIZABLE_FK_FIELDS = { 'insights_credential_id',), 'host': DEFAULT_SUMMARY_FIELDS, 'group': DEFAULT_SUMMARY_FIELDS, - 'default_environment': ('id', 'organization_id', 'image', 'description'), - 'execution_environment': ('id', 'organization_id', 'image', 'description'), + 'default_environment': DEFAULT_SUMMARY_FIELDS + ('image',), + 'execution_environment': DEFAULT_SUMMARY_FIELDS + ('image',), 'project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'), 'source_project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'), 'project_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',), @@ -1365,7 +1365,7 @@ class ExecutionEnvironmentSerializer(BaseSerializer): class Meta: model = ExecutionEnvironment - fields = ('*', '-name', 'organization', 'image', 'managed_by_tower', 'credential') + fields = ('*', 'organization', 'image', 'managed_by_tower', 'credential') def get_related(self, obj): res = super(ExecutionEnvironmentSerializer, self).get_related(obj) @@ -1395,7 +1395,7 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer): class Meta: model = Project fields = ('*', 'organization', 'scm_update_on_launch', - 'scm_update_cache_timeout', 'allow_override', 'custom_virtualenv',) + \ + 'scm_update_cache_timeout', 'allow_override', 'custom_virtualenv', 'default_environment') + \ ('last_update_failed', 'last_updated') # Backwards compatibility def get_related(self, obj): @@ -1420,6 +1420,9 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer): if obj.organization: res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) + if obj.default_environment: + res['default_environment'] = self.reverse('api:execution_environment_detail', + kwargs={'pk': obj.default_environment_id}) # Backwards compatibility. if obj.current_update: res['current_update'] = self.reverse('api:project_update_detail', diff --git a/awx/conf/fields.py b/awx/conf/fields.py index 7c9a94969d..90f495e293 100644 --- a/awx/conf/fields.py +++ b/awx/conf/fields.py @@ -14,6 +14,7 @@ from rest_framework.fields import ( # noqa BooleanField, CharField, ChoiceField, DictField, DateTimeField, EmailField, IntegerField, ListField, NullBooleanField ) +from rest_framework.serializers import PrimaryKeyRelatedField logger = logging.getLogger('awx.conf.fields') diff --git a/awx/main/access.py b/awx/main/access.py index 24e6bbc569..f1edc58006 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1320,6 +1320,8 @@ class ExecutionEnvironmentAccess(BaseAccess): """ model = ExecutionEnvironment + select_related = ('organization',) + prefetch_related = ('organization__admin_role',) def filtered_queryset(self): return ExecutionEnvironment.objects.filter( diff --git a/awx/main/conf.py b/awx/main/conf.py index 6bf86db214..f46371e22b 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -10,6 +10,7 @@ from rest_framework.fields import FloatField # Tower from awx.conf import fields, register, register_validate +from awx.main.models import ExecutionEnvironment logger = logging.getLogger('awx.main.conf') @@ -176,6 +177,18 @@ register( read_only=True, ) +register( + 'DEFAULT_EXECUTION_ENVIRONMENT', + field_class=fields.PrimaryKeyRelatedField, + allow_null=True, + default=None, + queryset=ExecutionEnvironment.objects.all(), + label=_('Global default execution environment'), + help_text=_('.'), + category=_('System'), + category_slug='system', +) + register( 'CUSTOM_VENV_PATHS', field_class=fields.StringListPathField, diff --git a/awx/main/migrations/0125_more_ee_modeling_changes.py b/awx/main/migrations/0125_more_ee_modeling_changes.py new file mode 100644 index 0000000000..3d5a076d8d --- /dev/null +++ b/awx/main/migrations/0125_more_ee_modeling_changes.py @@ -0,0 +1,41 @@ +# Generated by Django 2.2.16 on 2020-11-19 16:20 +import uuid + +import awx.main.fields +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0124_execution_environments'), + ] + + operations = [ + migrations.AlterModelOptions( + name='executionenvironment', + options={'ordering': ('-created',)}, + ), + migrations.AddField( + model_name='executionenvironment', + name='name', + field=models.CharField(default=uuid.uuid4, max_length=512, unique=True), + preserve_default=False, + ), + migrations.AddField( + model_name='organization', + name='execution_environment_admin_role', + field=awx.main.fields.ImplicitRoleField(editable=False, null='True', on_delete=django.db.models.deletion.CASCADE, parent_role='admin_role', related_name='+', to='main.Role'), + preserve_default='True', + ), + migrations.AddField( + model_name='project', + name='default_environment', + field=models.ForeignKey(blank=True, default=None, help_text='The default execution environment for jobs run using this project.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='main.ExecutionEnvironment'), + ), + migrations.AlterUniqueTogether( + name='executionenvironment', + unique_together=set(), + ), + ] diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index ebab3bc22f..00af665969 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -1133,7 +1133,6 @@ ManagedCredentialType( }, ) - ManagedCredentialType( namespace='kubernetes_bearer_token', kind='kubernetes', @@ -1165,6 +1164,37 @@ ManagedCredentialType( } ) +ManagedCredentialType( + namespace='registry', + kind='registry', + name=ugettext_noop('Container Registry'), + inputs={ + 'fields': [{ + 'id': 'host', + 'label': ugettext_noop('Authentication URL'), + 'type': 'string', + 'help_text': ugettext_noop('Authentication endpoint for the container registry.'), + }, { + 'id': 'username', + 'label': ugettext_noop('Username'), + 'type': 'string', + }, { + 'id': 'password', + 'label': ugettext_noop('Password'), + 'type': 'string', + 'secret': True, + }, { + 'id': 'token', + 'label': ugettext_noop('Access Token'), + 'type': 'string', + 'secret': True, + 'help_text': ugettext_noop('A token to use to authenticate with. ' + 'This should not be set if username/password are being used.'), + }], + 'required': ['host'], + } +) + ManagedCredentialType( namespace='galaxy_api_token', diff --git a/awx/main/models/execution_environments.py b/awx/main/models/execution_environments.py index bdbe75eb49..51c7c251ea 100644 --- a/awx/main/models/execution_environments.py +++ b/awx/main/models/execution_environments.py @@ -2,16 +2,15 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from awx.api.versioning import reverse -from awx.main.models.base import PrimordialModel +from awx.main.models.base import CommonModel __all__ = ['ExecutionEnvironment'] -class ExecutionEnvironment(PrimordialModel): +class ExecutionEnvironment(CommonModel): class Meta: - unique_together = ('organization', 'image') - ordering = (models.F('organization_id').asc(nulls_first=True), 'image') + ordering = ('-created',) organization = models.ForeignKey( 'Organization', diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 3730fe9af1..bdf1e38d7d 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -95,6 +95,9 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi job_template_admin_role = ImplicitRoleField( parent_role='admin_role', ) + execution_environment_admin_role = ImplicitRoleField( + parent_role='admin_role', + ) auditor_role = ImplicitRoleField( parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, ) diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 65fb8304ce..ec14a2ef76 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -259,6 +259,15 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn app_label = 'main' ordering = ('id',) + default_environment = models.ForeignKey( + 'ExecutionEnvironment', + null=True, + blank=True, + default=None, + on_delete=models.SET_NULL, + related_name='+', + help_text=_('The default execution environment for jobs run using this project.'), + ) scm_update_on_launch = models.BooleanField( default=False, help_text=_('Update the project when a job is launched that uses the project.'), diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 67d21e873d..fe8d622ac6 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -40,6 +40,7 @@ role_names = { 'inventory_admin_role': _('Inventory Admin'), 'credential_admin_role': _('Credential Admin'), 'job_template_admin_role': _('Job Template Admin'), + 'execution_environment_admin_role': _('Execution Environment Admin'), 'workflow_admin_role': _('Workflow Admin'), 'notification_admin_role': _('Notification Admin'), 'auditor_role': _('Auditor'), @@ -60,6 +61,7 @@ role_descriptions = { 'inventory_admin_role': _('Can manage all inventories of the %s'), 'credential_admin_role': _('Can manage all credentials of the %s'), 'job_template_admin_role': _('Can manage all job templates of the %s'), + 'execution_environment_admin_role': _('Can manage all execution environments of the %s'), 'workflow_admin_role': _('Can manage all workflows of the %s'), 'notification_admin_role': _('Can manage all notifications of the %s'), 'auditor_role': _('Can view all aspects of the %s'), diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index f4a9e1ba45..53406be172 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -40,6 +40,7 @@ from awx.main.dispatch import get_local_queuename from awx.main.dispatch.control import Control as ControlDispatcher from awx.main.registrar import activity_stream_registrar from awx.main.models.mixins import ResourceMixin, TaskManagerUnifiedJobMixin, ExecutionEnvironmentMixin +from awx.main.models.execution_environments import ExecutionEnvironment from awx.main.utils import ( camelcase_to_underscore, get_model_for_type, encrypt_dict, decrypt_field, _inventory_updates, @@ -338,6 +339,23 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, ExecutionEn from awx.main.models.notifications import NotificationTemplate return NotificationTemplate.objects.none() + def resolve_execution_environment(self): + """ + Return the execution environment that should be used when creating a new job. + """ + if self.execution_environment is not None: + return self.execution_environment + if getattr(self, 'project_id', None) and self.project.default_environment is not None: + return self.project.default_environment + if getattr(self, 'organization', None) and self.organization.default_environment is not None: + return self.organization.default_environment + if getattr(self, 'inventory', None) and self.inventory.organization is not None: + if self.inventory.organization.default_environment is not None: + return self.inventory.organization.default_environment + if settings.DEFAULT_EXECUTION_ENVIRONMENT is not None: + return settings.DEFAULT_EXECUTION_ENVIRONMENT + return ExecutionEnvironment.objects.filter(organization=None, managed_by_tower=True).first() + def create_unified_job(self, **kwargs): ''' Create a new unified job based on this unified job template. @@ -376,6 +394,8 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, ExecutionEn for fd, val in eager_fields.items(): setattr(unified_job, fd, val) + unified_job.execution_environment = self.resolve_execution_environment() + # NOTE: slice workflow jobs _get_parent_field_name method # is not correct until this is set if not parent_field_name: diff --git a/awx/main/tests/functional/test_credential.py b/awx/main/tests/functional/test_credential.py index 27f67b96f4..4f87c249be 100644 --- a/awx/main/tests/functional/test_credential.py +++ b/awx/main/tests/functional/test_credential.py @@ -90,6 +90,7 @@ def test_default_cred_types(): 'kubernetes_bearer_token', 'net', 'openstack', + 'registry', 'rhv', 'satellite6', 'scm', diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 058d6aeaee..52a044afa0 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -175,6 +175,7 @@ REMOTE_HOST_HEADERS = ['REMOTE_ADDR', 'REMOTE_HOST'] PROXY_IP_ALLOWED_LIST = [] CUSTOM_VENV_PATHS = [] +DEFAULT_EXECUTION_ENVIRONMENT = None # Note: This setting may be overridden by database settings. STDOUT_MAX_BYTES_DISPLAY = 1048576 diff --git a/awxkit/awxkit/api/pages/execution_environments.py b/awxkit/awxkit/api/pages/execution_environments.py index 87225d1052..c3bcecb4bf 100644 --- a/awxkit/awxkit/api/pages/execution_environments.py +++ b/awxkit/awxkit/api/pages/execution_environments.py @@ -18,7 +18,7 @@ log = logging.getLogger(__name__) class ExecutionEnvironment(HasCreate, base.Base): dependencies = [Organization, Credential] - NATURAL_KEY = ('organization', 'image') + NATURAL_KEY = ('name',) # fields are image, organization, managed_by_tower, credential def create(self, image='quay.io/ansible/ansible-runner:devel', credential=None, **kwargs): From 6d935f740c5a69d74d99b8fcf35ec1f70231ad3a Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Thu, 19 Nov 2020 14:06:32 -0500 Subject: [PATCH 055/178] Fill in the new execution environment collection module as well as changes to other ones that need to be able to attach EEs. --- awx/conf/fields.py | 2 +- awx/main/models/unified_jobs.py | 2 +- .../modules/tower_execution_environment.py | 66 ++++++++++++++----- .../plugins/modules/tower_inventory_source.py | 4 ++ .../plugins/modules/tower_job_template.py | 15 +++-- .../plugins/modules/tower_organization.py | 6 +- .../plugins/modules/tower_project.py | 20 ++++-- .../modules/tower_workflow_job_template.py | 5 ++ 8 files changed, 91 insertions(+), 29 deletions(-) diff --git a/awx/conf/fields.py b/awx/conf/fields.py index 90f495e293..e28a44aa32 100644 --- a/awx/conf/fields.py +++ b/awx/conf/fields.py @@ -14,7 +14,7 @@ from rest_framework.fields import ( # noqa BooleanField, CharField, ChoiceField, DictField, DateTimeField, EmailField, IntegerField, ListField, NullBooleanField ) -from rest_framework.serializers import PrimaryKeyRelatedField +from rest_framework.serializers import PrimaryKeyRelatedField # noqa logger = logging.getLogger('awx.conf.fields') diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 53406be172..aae9d59a8f 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -353,7 +353,7 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, ExecutionEn if self.inventory.organization.default_environment is not None: return self.inventory.organization.default_environment if settings.DEFAULT_EXECUTION_ENVIRONMENT is not None: - return settings.DEFAULT_EXECUTION_ENVIRONMENT + return settings.DEFAULT_EXECUTION_ENVIRONMENT return ExecutionEnvironment.objects.filter(organization=None, managed_by_tower=True).first() def create_unified_job(self, **kwargs): diff --git a/awx_collection/plugins/modules/tower_execution_environment.py b/awx_collection/plugins/modules/tower_execution_environment.py index 978d23298c..486a24d949 100644 --- a/awx_collection/plugins/modules/tower_execution_environment.py +++ b/awx_collection/plugins/modules/tower_execution_environment.py @@ -22,30 +22,34 @@ description: - Create, update, or destroy Execution Environments in Ansible Tower. See U(https://www.ansible.com/tower) for an overview. options: + name: + description: + - Name to use for the execution environment. + required: True + type: str image: description: - - The fully qualified name of the container image + - The fully qualified url of the container image. required: True type: str + description: + description: + - Description to use for the execution environment. + type: str + organization: + description: + - The organization the execution environment belongs to. + type: str + credential: + description: + - Name of the credential to use for the execution environment. + type: str state: description: - Desired state of the resource. choices: ["present", "absent"] default: "present" type: str - credential: - description: - - Name of the credential to use for the job template. - - Deprecated, use 'credentials'. - type: str - description: - description: - - Description to use for the job template. - type: str - organization: - description: - - TODO - type: str extends_documentation_fragment: awx.awx.auth ''' @@ -53,6 +57,7 @@ extends_documentation_fragment: awx.awx.auth EXAMPLES = ''' - name: Add EE to Tower tower_execution_environment: + name: "My EE" image: quay.io/awx/ee ''' @@ -64,22 +69,49 @@ import json def main(): # Any additional arguments that are not fields of the item can be added here argument_spec = dict( + name=dict(required=True), image=dict(required=True), + description=dict(default=''), + organization=dict(), + credential=dict(default=''), + state=dict(choices=['present', 'absent'], default='present'), ) # Create a module for ourselves module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters + name = module.params.get('name') image = module.params.get('image') + description = module.params.get('description') state = module.params.get('state') - existing_item = module.get_one('execution_environments', name_or_id=image) + existing_item = module.get_one('execution_environments', name_or_id=name) if state == 'absent': - module.delete_if_needed(image) + module.delete_if_needed(existing_item) - module.create_or_update_if_needed(existing_item, image, endpoint='execution_environments', item_type='execution_environment') + new_fields = { + 'name': name, + 'image': image, + } + if description: + new_fields['description'] = description + + # Attempt to look up the related items the user specified (these will fail the module if not found) + organization = module.params.get('organization') + if organization: + new_fields['organization'] = module.resolve_name_to_id('organizations', organization) + + credential = module.params.get('credential') + if credential: + new_fields['credential'] = module.resolve_name_to_id('credentials', credential) + + module.create_or_update_if_needed( + existing_item, new_fields, + endpoint='execution_environments', + item_type='execution_environment' + ) if __name__ == '__main__': diff --git a/awx_collection/plugins/modules/tower_inventory_source.py b/awx_collection/plugins/modules/tower_inventory_source.py index ceb0e8b5a6..9edf467617 100644 --- a/awx_collection/plugins/modules/tower_inventory_source.py +++ b/awx_collection/plugins/modules/tower_inventory_source.py @@ -177,6 +177,7 @@ def main(): enabled_value=dict(), host_filter=dict(), credential=dict(), + execution_environment=dict(), organization=dict(), overwrite=dict(type='bool'), overwrite_vars=dict(type='bool'), @@ -203,6 +204,7 @@ def main(): organization = module.params.get('organization') source_script = module.params.get('source_script') credential = module.params.get('credential') + ee = module.params.get('execution_environment') source_project = module.params.get('source_project') state = module.params.get('state') @@ -254,6 +256,8 @@ def main(): # Attempt to look up the related items the user specified (these will fail the module if not found) if credential is not None: inventory_source_fields['credential'] = module.resolve_name_to_id('credentials', credential) + if ee is not None: + inventory_source_fields['execution_environment'] = module.resolve_name_to_id('execution_environments', ee) if source_project is not None: inventory_source_fields['source_project'] = module.resolve_name_to_id('projects', source_project) if source_script is not None: diff --git a/awx_collection/plugins/modules/tower_job_template.py b/awx_collection/plugins/modules/tower_job_template.py index b2b1530d6f..131d05f924 100644 --- a/awx_collection/plugins/modules/tower_job_template.py +++ b/awx_collection/plugins/modules/tower_job_template.py @@ -60,10 +60,6 @@ options: description: - Path to the playbook to use for the job template within the project provided. type: str - execution_environment: - description: - - Execution Environment to use for the JT. - type: str credential: description: - Name of the credential to use for the job template. @@ -79,6 +75,10 @@ options: - Name of the vault credential to use for the job template. - Deprecated, use 'credentials'. type: str + execution_environment: + description: + - Execution Environment to use for the JT. + type: str forks: description: - The number of parallel or simultaneous processes to use while executing the playbook. @@ -354,6 +354,7 @@ def main(): vault_credential=dict(), custom_virtualenv=dict(), credentials=dict(type='list', elements='str'), + execution_environment=dict(), forks=dict(type='int'), limit=dict(), verbosity=dict(type='int', choices=[0, 1, 2, 3, 4], default=0), @@ -420,7 +421,11 @@ def main(): organization = module.params.get('organization') if organization: organization_id = module.resolve_name_to_id('organizations', organization) - search_fields['organization'] = new_fields['organization'] = organization_id + search_fields['organization'] = new_fields['organization'] = organization_id + + ee = module.params.get('execution_environment') + if ee: + new_fields['execution_environment'] = module.resolve_name_to_id('execution_environments', ee) # Attempt to look up an existing item based on the provided data existing_item = module.get_one('job_templates', name_or_id=name, **{'data': search_fields}) diff --git a/awx_collection/plugins/modules/tower_organization.py b/awx_collection/plugins/modules/tower_organization.py index 7d88d2a421..bcf6060ea6 100644 --- a/awx_collection/plugins/modules/tower_organization.py +++ b/awx_collection/plugins/modules/tower_organization.py @@ -38,7 +38,7 @@ options: default: '' default_environment: description: - - Default Execution Environment to use for the Organization. + - Default Execution Environment to use for jobs owned by the Organization. type: str max_hosts: description: @@ -114,6 +114,7 @@ def main(): name=dict(required=True), description=dict(), custom_virtualenv=dict(), + default_environment=dict(), max_hosts=dict(type='int', default="0"), notification_templates_started=dict(type="list", elements='str'), notification_templates_success=dict(type="list", elements='str'), @@ -130,6 +131,7 @@ def main(): name = module.params.get('name') description = module.params.get('description') custom_virtualenv = module.params.get('custom_virtualenv') + default_ee = module.params.get('default_environment') max_hosts = module.params.get('max_hosts') # instance_group_names = module.params.get('instance_groups') state = module.params.get('state') @@ -179,6 +181,8 @@ def main(): org_fields['description'] = description if custom_virtualenv is not None: org_fields['custom_virtualenv'] = custom_virtualenv + if default_ee is not None: + org_fields['default_environment'] = module.resolve_name_to_id('execution_environments', default_ee) if max_hosts is not None: org_fields['max_hosts'] = max_hosts diff --git a/awx_collection/plugins/modules/tower_project.py b/awx_collection/plugins/modules/tower_project.py index f6ab7d144c..1a8248c1fa 100644 --- a/awx_collection/plugins/modules/tower_project.py +++ b/awx_collection/plugins/modules/tower_project.py @@ -31,10 +31,6 @@ options: description: - Description to use for the project. type: str - execution_environment: - description: - - Execution Environment to use for the project. - type: str scm_type: description: - Type of SCM resource. @@ -106,6 +102,14 @@ options: - Local absolute file path containing a custom Python virtualenv to use type: str default: '' + default_environment: + description: + - Default Execution Environment to use for jobs relating to the project. + type: str + execution_environment: + description: + - Execution Environment to use for project updates. + type: str organization: description: - Name of organization for project. @@ -243,6 +247,8 @@ def main(): allow_override=dict(type='bool', aliases=['scm_allow_override']), timeout=dict(type='int', default=0, aliases=['job_timeout']), custom_virtualenv=dict(), + default_environment=dict(), + execution_environment=dict(), organization=dict(), notification_templates_started=dict(type="list", elements='str'), notification_templates_success=dict(type="list", elements='str'), @@ -274,6 +280,8 @@ def main(): allow_override = module.params.get('allow_override') timeout = module.params.get('timeout') custom_virtualenv = module.params.get('custom_virtualenv') + default_ee = module.params.get('default_environment') + ee = module.params.get('execution_environment') organization = module.params.get('organization') state = module.params.get('state') wait = module.params.get('wait') @@ -337,6 +345,10 @@ def main(): project_fields['description'] = description if credential is not None: project_fields['credential'] = credential + if default_ee is not None: + project_fields['default_environment'] = module.resolve_name_to_id('execution_environments', default_ee) + if ee is not None: + project_fields['execution_environment'] = module.resolve_name_to_id('execution_environments', ee) if allow_override is not None: project_fields['allow_override'] = allow_override if scm_type == '': diff --git a/awx_collection/plugins/modules/tower_workflow_job_template.py b/awx_collection/plugins/modules/tower_workflow_job_template.py index 54b6695b03..48759e1cc4 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template.py @@ -175,6 +175,7 @@ def main(): description=dict(), extra_vars=dict(type='dict'), organization=dict(), + execution_environment=dict(), survey_spec=dict(type='dict', aliases=['survey']), survey_enabled=dict(type='bool'), allow_simultaneous=dict(type='bool'), @@ -212,6 +213,10 @@ def main(): organization_id = module.resolve_name_to_id('organizations', organization) search_fields['organization'] = new_fields['organization'] = organization_id + ee = module.params.get('execution_environment') + if ee: + new_fields['execution_environment'] = module.resolve_name_to_id('execution_environments', ee) + # Attempt to look up an existing item based on the provided data existing_item = module.get_one('workflow_job_templates', name_or_id=name, **{'data': search_fields}) From 8562c378c036eb1cd671d49fc8c23613bdba4da3 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Fri, 20 Nov 2020 10:43:53 -0500 Subject: [PATCH 056/178] Make use of the EE resolver code when launching jobs --- awx/main/tasks.py | 9 +-------- awx/settings/defaults.py | 3 +++ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index e7a6aa711a..ea3aa94b82 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -887,14 +887,7 @@ class BaseTask(object): return os.path.abspath(os.path.join(os.path.dirname(__file__), *args)) def build_execution_environment_params(self, instance): - if getattr(instance, 'execution_environment', None): - # TODO: process heirarchy, JT-project-org, maybe here - # or maybe in create_unified_job - logger.info('using custom image {}'.format(instance.execution_environment.image)) - image = instance.execution_environment.image - else: - logger.info('using default image') - image = settings.AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE + image = instance.execution_environment.image params = { "container_image": image, "process_isolation": True diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 52a044afa0..7812b3af31 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -59,12 +59,15 @@ DATABASES = { } } +# TODO: remove this setting in favor of a default execution environment AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE = 'quay.io/shanemcd/ee' AWX_CONTAINER_GROUP_K8S_API_TIMEOUT = 10 AWX_CONTAINER_GROUP_POD_LAUNCH_RETRIES = 100 AWX_CONTAINER_GROUP_POD_LAUNCH_RETRY_DELAY = 5 AWX_CONTAINER_GROUP_DEFAULT_NAMESPACE = 'default' + +# TODO: remove this setting in favor of a default execution environment AWX_CONTAINER_GROUP_DEFAULT_IMAGE = AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE # Internationalization From 4993a9e6ec755f0e71537b86badcc5143e48d1ea Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Fri, 20 Nov 2020 14:08:11 -0500 Subject: [PATCH 057/178] Move the resolve_execution_environment method to the mixin class so that it can be used with AdHocCommands as well. --- awx/main/models/ad_hoc_commands.py | 4 ++-- awx/main/models/mixins.py | 19 +++++++++++++++++++ awx/main/models/unified_jobs.py | 18 ------------------ awx/main/tasks.py | 3 +++ 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/awx/main/models/ad_hoc_commands.py b/awx/main/models/ad_hoc_commands.py index 9787f01423..536ac8a912 100644 --- a/awx/main/models/ad_hoc_commands.py +++ b/awx/main/models/ad_hoc_commands.py @@ -198,8 +198,8 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin): def copy(self): data = {} for field in ('job_type', 'inventory_id', 'limit', 'credential_id', - 'module_name', 'module_args', 'forks', 'verbosity', - 'extra_vars', 'become_enabled', 'diff_mode'): + 'execution_environment_id', 'module_name', 'module_args', + 'forks', 'verbosity', 'extra_vars', 'become_enabled', 'diff_mode'): data[field] = getattr(self, field) return AdHocCommand.objects.create(**data) diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index 1cd1366a92..459eadabcf 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -455,6 +455,25 @@ class ExecutionEnvironmentMixin(models.Model): help_text=_('The container image to be used for execution.'), ) + def resolve_execution_environment(self): + """ + Return the execution environment that should be used when creating a new job. + """ + from awx.main.models.execution_environments import ExecutionEnvironment + + if self.execution_environment is not None: + return self.execution_environment + if getattr(self, 'project_id', None) and self.project.default_environment is not None: + return self.project.default_environment + if getattr(self, 'organization', None) and self.organization.default_environment is not None: + return self.organization.default_environment + if getattr(self, 'inventory', None) and self.inventory.organization is not None: + if self.inventory.organization.default_environment is not None: + return self.inventory.organization.default_environment + if settings.DEFAULT_EXECUTION_ENVIRONMENT is not None: + return settings.DEFAULT_EXECUTION_ENVIRONMENT + return ExecutionEnvironment.objects.filter(organization=None, managed_by_tower=True).first() + class CustomVirtualEnvMixin(models.Model): class Meta: diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index aae9d59a8f..7970de1b22 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -40,7 +40,6 @@ from awx.main.dispatch import get_local_queuename from awx.main.dispatch.control import Control as ControlDispatcher from awx.main.registrar import activity_stream_registrar from awx.main.models.mixins import ResourceMixin, TaskManagerUnifiedJobMixin, ExecutionEnvironmentMixin -from awx.main.models.execution_environments import ExecutionEnvironment from awx.main.utils import ( camelcase_to_underscore, get_model_for_type, encrypt_dict, decrypt_field, _inventory_updates, @@ -339,23 +338,6 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, ExecutionEn from awx.main.models.notifications import NotificationTemplate return NotificationTemplate.objects.none() - def resolve_execution_environment(self): - """ - Return the execution environment that should be used when creating a new job. - """ - if self.execution_environment is not None: - return self.execution_environment - if getattr(self, 'project_id', None) and self.project.default_environment is not None: - return self.project.default_environment - if getattr(self, 'organization', None) and self.organization.default_environment is not None: - return self.organization.default_environment - if getattr(self, 'inventory', None) and self.inventory.organization is not None: - if self.inventory.organization.default_environment is not None: - return self.inventory.organization.default_environment - if settings.DEFAULT_EXECUTION_ENVIRONMENT is not None: - return settings.DEFAULT_EXECUTION_ENVIRONMENT - return ExecutionEnvironment.objects.filter(organization=None, managed_by_tower=True).first() - def create_unified_job(self, **kwargs): ''' Create a new unified job based on this unified job template. diff --git a/awx/main/tasks.py b/awx/main/tasks.py index ea3aa94b82..8f09f67f56 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -887,6 +887,9 @@ class BaseTask(object): return os.path.abspath(os.path.join(os.path.dirname(__file__), *args)) def build_execution_environment_params(self, instance): + if instance.execution_environment_id is None: + self.update_model(instance.pk, execution_environment=instance.resolve_execution_environment()) + image = instance.execution_environment.image params = { "container_image": image, From e7bf81883be4d1eb16d4becdb172a5aa546a6c8c Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Tue, 8 Dec 2020 09:42:02 -0500 Subject: [PATCH 058/178] Populate the EE name field in awxkit --- awxkit/awxkit/api/pages/execution_environments.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/awxkit/awxkit/api/pages/execution_environments.py b/awxkit/awxkit/api/pages/execution_environments.py index c3bcecb4bf..a01aa91011 100644 --- a/awxkit/awxkit/api/pages/execution_environments.py +++ b/awxkit/awxkit/api/pages/execution_environments.py @@ -20,21 +20,22 @@ class ExecutionEnvironment(HasCreate, base.Base): dependencies = [Organization, Credential] NATURAL_KEY = ('name',) - # fields are image, organization, managed_by_tower, credential - def create(self, image='quay.io/ansible/ansible-runner:devel', credential=None, **kwargs): + # fields are name, image, organization, managed_by_tower, credential + def create(self, name='', image='quay.io/ansible/ansible-runner:devel', credential=None, **kwargs): # we do not want to make a credential by default - payload = self.create_payload(image=image, credential=credential, **kwargs) + payload = self.create_payload(name=name, image=image, credential=credential, **kwargs) ret = self.update_identity(ExecutionEnvironments(self.connection).post(payload)) return ret - def create_payload(self, organization=Organization, **kwargs): + def create_payload(self, name='', organization=Organization, **kwargs): self.create_and_update_dependencies(organization) - payload = self.payload(organization=self.ds.organization, **kwargs) + payload = self.payload(name=name, organization=self.ds.organization, **kwargs) payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store) return payload - def payload(self, image=None, organization=None, credential=None, **kwargs): + def payload(self, name='', image=None, organization=None, credential=None, **kwargs): payload = PseudoNamespace( + name=name or "EE - {}".format(random_title()), image=image or random_title(10), organization=organization.id if organization else None, credential=credential.id if credential else None, From 5f1da2b9233edcc589c243f961d42bb3a7ecf3fe Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Tue, 8 Dec 2020 09:59:45 -0500 Subject: [PATCH 059/178] Adjust ExecutionEnvironmentAccess to account for the new EE admin role --- awx/main/access.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index f1edc58006..a8a110e9c3 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1312,7 +1312,7 @@ class ExecutionEnvironmentAccess(BaseAccess): """ I can see an execution environment when: - I'm a superuser - - I'm a member of the organization + - I'm a member of the same organization - it is a global ExecutionEnvironment I can create/change an execution environment when: - I'm a superuser @@ -1321,32 +1321,32 @@ class ExecutionEnvironmentAccess(BaseAccess): model = ExecutionEnvironment select_related = ('organization',) - prefetch_related = ('organization__admin_role',) + prefetch_related = ('organization__admin_role', 'organization__execution_environment_admin_role') def filtered_queryset(self): return ExecutionEnvironment.objects.filter( - Q(organization__in=Organization.accessible_pk_qs(self.user, 'admin_role')) | + Q(organization__in=Organization.accessible_pk_qs(self.user, 'execution_environment_admin_role')) | Q(organization__isnull=True) ).distinct() @check_superuser def can_add(self, data): if not data: # So the browseable API will work - return Organization.accessible_objects(self.user, 'admin_role').exists() + return Organization.accessible_objects(self.user, 'execution_environment_admin_role').exists() return self.check_related('organization', Organization, data) @check_superuser def can_change(self, obj, data): if obj and obj.organization_id is None: raise PermissionDenied - if self.user not in obj.organization.admin_role: + if self.user not in obj.organization.execution_environment_admin_role: raise PermissionDenied org_pk = get_pk_from_dict(data, 'organization') if obj and obj.organization_id != org_pk: # Prevent moving an EE to a different organization, unless a superuser or admin on both orgs. if obj.organization_id is None or org_pk is None: raise PermissionDenied - if self.user not in Organization.objects.get(id=org_pk).admin_role: + if self.user not in Organization.objects.get(id=org_pk).execution_environment_admin_role: raise PermissionDenied return True From 4a0fc3e1af11576b6e1eeed35a7b62737eb46c9f Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Wed, 9 Dec 2020 10:28:12 -0500 Subject: [PATCH 060/178] Ensure that a fallback EE is available to be found for the failing tests. --- awx/main/tests/functional/test_inventory_source_injectors.py | 4 +++- awx/main/tests/unit/test_tasks.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/awx/main/tests/functional/test_inventory_source_injectors.py b/awx/main/tests/functional/test_inventory_source_injectors.py index bb26b7c029..bf1a4002c5 100644 --- a/awx/main/tests/functional/test_inventory_source_injectors.py +++ b/awx/main/tests/functional/test_inventory_source_injectors.py @@ -6,7 +6,7 @@ import re from collections import namedtuple from awx.main.tasks import RunInventoryUpdate -from awx.main.models import InventorySource, Credential, CredentialType, UnifiedJob +from awx.main.models import InventorySource, Credential, CredentialType, UnifiedJob, ExecutionEnvironment from awx.main.constants import CLOUD_PROVIDERS, STANDARD_INVENTORY_UPDATE_ENV from awx.main.tests import data @@ -183,6 +183,8 @@ def create_reference_data(source_dir, env, content): @pytest.mark.django_db @pytest.mark.parametrize('this_kind', CLOUD_PROVIDERS) def test_inventory_update_injected_content(this_kind, inventory, fake_credential_factory): + ExecutionEnvironment.objects.create(name='test EE', managed_by_tower=True) + injector = InventorySource.injectors[this_kind] if injector.plugin_name is None: pytest.skip('Use of inventory plugin is not enabled for this source') diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index d6e70fe3fd..94d622deac 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -18,6 +18,7 @@ from awx.main.models import ( AdHocCommand, Credential, CredentialType, + ExecutionEnvironment, Inventory, InventorySource, InventoryUpdate, @@ -657,9 +658,12 @@ class TestGenericRun(): assert env['FOO'] == 'BAR' +@pytest.mark.django_db class TestAdhocRun(TestJobExecution): def test_options_jinja_usage(self, adhoc_job, adhoc_update_model_wrapper): + ExecutionEnvironment.objects.create(name='test EE', managed_by_tower=True) + adhoc_job.module_args = '{{ ansible_ssh_pass }}' adhoc_job.websocket_emit_status = mock.Mock() adhoc_job.send_notification_templates = mock.Mock() From fde7a1e3e5680af91c3373e51659a765d73115e1 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Thu, 10 Dec 2020 09:27:45 -0500 Subject: [PATCH 061/178] Ensure that the updated job instance is used when attaching an EE. --- awx/main/tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 8f09f67f56..d0a78064af 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -888,7 +888,8 @@ class BaseTask(object): def build_execution_environment_params(self, instance): if instance.execution_environment_id is None: - self.update_model(instance.pk, execution_environment=instance.resolve_execution_environment()) + self.instance = instance = self.update_model( + instance.pk, execution_environment=instance.resolve_execution_environment()) image = instance.execution_environment.image params = { From 44ad6bfdce4dd7c2466969ce4654cf922477b698 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Thu, 10 Dec 2020 11:12:24 -0500 Subject: [PATCH 062/178] Insert a default EE into the development environment --- tools/docker-compose/bootstrap_development.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/docker-compose/bootstrap_development.sh b/tools/docker-compose/bootstrap_development.sh index 1d3e399bf7..46fb917b08 100755 --- a/tools/docker-compose/bootstrap_development.sh +++ b/tools/docker-compose/bootstrap_development.sh @@ -26,3 +26,8 @@ make init mkdir -p /awx_devel/awx/public/static mkdir -p /awx_devel/awx/ui/static mkdir -p /awx_devel/awx/ui_next/build/static + +echo "ee, created = ExecutionEnvironment.objects.get_or_create(name='Default EE', \ + defaults={'image': 'quay.io/awx/ee', \ + 'managed_by_tower': True}); \ + print('Already exists' if not created else 'Created')" | awx-manage shell_plus --quiet-load From c74d60f3f3b9488769e99bc22674eb8b6715ab4e Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Thu, 10 Dec 2020 11:42:52 -0500 Subject: [PATCH 063/178] Make sure that the new credential type is in the choices list --- awx/main/migrations/0125_more_ee_modeling_changes.py | 5 +++++ awx/main/models/credential/__init__.py | 1 + 2 files changed, 6 insertions(+) diff --git a/awx/main/migrations/0125_more_ee_modeling_changes.py b/awx/main/migrations/0125_more_ee_modeling_changes.py index 3d5a076d8d..be999cbb79 100644 --- a/awx/main/migrations/0125_more_ee_modeling_changes.py +++ b/awx/main/migrations/0125_more_ee_modeling_changes.py @@ -34,6 +34,11 @@ class Migration(migrations.Migration): name='default_environment', field=models.ForeignKey(blank=True, default=None, help_text='The default execution environment for jobs run using this project.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='main.ExecutionEnvironment'), ), + migrations.AlterField( + model_name='credentialtype', + name='kind', + field=models.CharField(choices=[('ssh', 'Machine'), ('vault', 'Vault'), ('net', 'Network'), ('scm', 'Source Control'), ('cloud', 'Cloud'), ('registry', 'Container Registry'), ('token', 'Personal Access Token'), ('insights', 'Insights'), ('external', 'External'), ('kubernetes', 'Kubernetes'), ('galaxy', 'Galaxy/Automation Hub')], max_length=32), + ), migrations.AlterUniqueTogether( name='executionenvironment', unique_together=set(), diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index 00af665969..7cdd9898d3 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -331,6 +331,7 @@ class CredentialType(CommonModelNameNotUnique): ('net', _('Network')), ('scm', _('Source Control')), ('cloud', _('Cloud')), + ('registry', _('Container Registry')), ('token', _('Personal Access Token')), ('insights', _('Insights')), ('external', _('External')), From 12b8349e88ea337da4da0abf359fbb413af1fbad Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Thu, 10 Dec 2020 13:24:58 -0500 Subject: [PATCH 064/178] Show EE images that are managed by tower in UI --- .../ExecutionEnvironmentList/ExecutionEnvironmentList.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx index 02ce49ee9f..749fe8893e 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx @@ -21,7 +21,6 @@ import ExecutionEnvironmentsListItem from './ExecutionEnvironmentListItem'; const QS_CONFIG = getQSConfig('execution_environments', { page: 1, page_size: 20, - managed_by_tower: false, order_by: 'image', }); From 9964ba7c9a10acbd2596c13846928944e37dfe06 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Fri, 11 Dec 2020 10:09:27 -0500 Subject: [PATCH 065/178] Improve the behavior of EE resolution for ad hoc commands - call resolve_execution_environment during AdHocCommand.save() - wrap the fallback call of the resolver in tasks.py in disable_activity_stream() --- awx/main/models/ad_hoc_commands.py | 3 +++ awx/main/tasks.py | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/awx/main/models/ad_hoc_commands.py b/awx/main/models/ad_hoc_commands.py index 536ac8a912..54269fff1e 100644 --- a/awx/main/models/ad_hoc_commands.py +++ b/awx/main/models/ad_hoc_commands.py @@ -209,6 +209,9 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin): self.name = Truncator(u': '.join(filter(None, (self.module_name, self.module_args)))).chars(512) if 'name' not in update_fields: update_fields.append('name') + if not self.execution_environment_id: + self.execution_environment = self.resolve_execution_environment() + update_fields.append('execution_environment') super(AdHocCommand, self).save(*args, **kwargs) @property diff --git a/awx/main/tasks.py b/awx/main/tasks.py index d0a78064af..cf081a9d17 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -888,8 +888,11 @@ class BaseTask(object): def build_execution_environment_params(self, instance): if instance.execution_environment_id is None: - self.instance = instance = self.update_model( - instance.pk, execution_environment=instance.resolve_execution_environment()) + from awx.main.signals import disable_activity_stream + + with disable_activity_stream(): + self.instance = instance = self.update_model( + instance.pk, execution_environment=instance.resolve_execution_environment()) image = instance.execution_environment.image params = { From b95347822599f68ce8e121ef3323611d60cc530a Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Fri, 11 Dec 2020 21:44:51 -0500 Subject: [PATCH 066/178] Change the default EE location --- awx_collection/plugins/modules/tower_execution_environment.py | 2 +- .../ansible/roles/sources/templates/docker-compose.yml.j2 | 2 +- tools/docker-compose/bootstrap_development.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/awx_collection/plugins/modules/tower_execution_environment.py b/awx_collection/plugins/modules/tower_execution_environment.py index 486a24d949..280408f72a 100644 --- a/awx_collection/plugins/modules/tower_execution_environment.py +++ b/awx_collection/plugins/modules/tower_execution_environment.py @@ -58,7 +58,7 @@ EXAMPLES = ''' - name: Add EE to Tower tower_execution_environment: name: "My EE" - image: quay.io/awx/ee + image: quay.io/ansible/awx-ee ''' diff --git a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 index ea330d66f8..eae187cea6 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 @@ -44,7 +44,7 @@ services: # context: ./docker-compose # dockerfile: Dockerfile-logstash ee: - image: quay.io/awx/ee + image: quay.io/ansible/awx-ee user: ${CURRENT_UID} volumes: - "./docker-compose/receptor.cfg:/receptor.cfg" diff --git a/tools/docker-compose/bootstrap_development.sh b/tools/docker-compose/bootstrap_development.sh index 46fb917b08..3cc937c9c6 100755 --- a/tools/docker-compose/bootstrap_development.sh +++ b/tools/docker-compose/bootstrap_development.sh @@ -28,6 +28,6 @@ mkdir -p /awx_devel/awx/ui/static mkdir -p /awx_devel/awx/ui_next/build/static echo "ee, created = ExecutionEnvironment.objects.get_or_create(name='Default EE', \ - defaults={'image': 'quay.io/awx/ee', \ + defaults={'image': 'quay.io/ansible/awx-ee', \ 'managed_by_tower': True}); \ print('Already exists' if not created else 'Created')" | awx-manage shell_plus --quiet-load From ba146343184a1f20638d05ae3f1e803159f04ce5 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Fri, 11 Dec 2020 21:50:10 -0500 Subject: [PATCH 067/178] Fix collection pep8 failure --- awx_collection/plugins/modules/tower_job_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/plugins/modules/tower_job_template.py b/awx_collection/plugins/modules/tower_job_template.py index 131d05f924..1ed750b86e 100644 --- a/awx_collection/plugins/modules/tower_job_template.py +++ b/awx_collection/plugins/modules/tower_job_template.py @@ -421,7 +421,7 @@ def main(): organization = module.params.get('organization') if organization: organization_id = module.resolve_name_to_id('organizations', organization) - search_fields['organization'] = new_fields['organization'] = organization_id + search_fields['organization'] = new_fields['organization'] = organization_id ee = module.params.get('execution_environment') if ee: From 0dfb183cb6f72caa3a4558138d916c93818a901a Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Fri, 11 Dec 2020 21:58:35 -0500 Subject: [PATCH 068/178] Fix another credential path-in-container bug --- awx/main/tasks.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index cf081a9d17..8182bd64cf 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1728,7 +1728,10 @@ class RunJob(BaseTask): cred_files = private_data_files.get('credentials', {}) for cloud_cred in job.cloud_credentials: if cloud_cred and cloud_cred.credential_type.namespace == 'openstack': - env['OS_CLIENT_CONFIG_FILE'] = cred_files.get(cloud_cred, '') + env['OS_CLIENT_CONFIG_FILE'] = os.path.join( + '/runner', + os.path.basename(cred_files.get(cloud_cred, '')) + ) for network_cred in job.network_credentials: env['ANSIBLE_NET_USERNAME'] = network_cred.get_input('username', default='') From 015fc29c1c0179d047f0653c0da0c2d61f06db69 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Sat, 12 Dec 2020 22:48:21 -0500 Subject: [PATCH 069/178] Fix another svn issue due to pre-existing folder --- awx/playbooks/project_update.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx/playbooks/project_update.yml b/awx/playbooks/project_update.yml index 664f189a28..e00bed4249 100644 --- a/awx/playbooks/project_update.yml +++ b/awx/playbooks/project_update.yml @@ -55,6 +55,8 @@ force: "{{scm_clean}}" username: "{{scm_username|default(omit)}}" password: "{{scm_password|default(omit)}}" + # must be in_place because folder pre-existing, because it is mounted + in_place: true environment: LC_ALL: 'en_US.UTF-8' register: svn_result From 49bdadcdbff662ff8e0a568cf903f40e695b0be2 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Sun, 13 Dec 2020 09:20:48 -0500 Subject: [PATCH 070/178] Fix yet another host vs container path bug --- awx/main/models/credential/injectors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/models/credential/injectors.py b/awx/main/models/credential/injectors.py index 4d7ef26054..75a08482cc 100644 --- a/awx/main/models/credential/injectors.py +++ b/awx/main/models/credential/injectors.py @@ -105,7 +105,8 @@ def openstack(cred, env, private_data_dir): yaml.safe_dump(openstack_data, f, default_flow_style=False, allow_unicode=True) f.close() os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) - env['OS_CLIENT_CONFIG_FILE'] = path + # TODO: constant for container base path + env['OS_CLIENT_CONFIG_FILE'] = os.path.join('/runner', os.path.basename(path)) def kubernetes_bearer_token(cred, env, private_data_dir): From 10e68c6fb332873cd19fe6a78694250ce1c2525b Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Mon, 14 Dec 2020 10:08:07 -0500 Subject: [PATCH 071/178] Fix unit test fallout --- awx/main/tests/unit/test_tasks.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 94d622deac..8586ad11c7 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -1288,7 +1288,11 @@ class TestJobCredentials(TestJobExecution): credential, env, {}, [], private_data_dir ) - shade_config = open(env['OS_CLIENT_CONFIG_FILE'], 'r').read() + # convert container path to host machine path + config_loc = os.path.join( + private_data_dir, os.path.basename(env['OS_CLIENT_CONFIG_FILE']) + ) + shade_config = open(config_loc, 'r').read() assert shade_config == '\n'.join([ 'clouds:', ' devstack:', From eb5bf599e3b9d6debeea8d1b1485595ba60f6a64 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Tue, 15 Dec 2020 13:29:03 -0500 Subject: [PATCH 072/178] Fix raw archive project updates Several squashed commits Fix git bug introduced by setting remote tmp in project path change shebang back to py3 again Revert shebang change --- awx/main/tasks.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 8182bd64cf..9ffcad4393 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -2231,6 +2231,14 @@ class RunProjectUpdate(BaseTask): elif project_update.project.allow_override: # If branch is override-able, do extra fetch for all branches extra_vars['scm_refspec'] = 'refs/heads/*:refs/remotes/origin/*' + + if project_update.scm_type == 'archive': + # for raw archive, prevent error moving files between volumes + extra_vars['ansible_remote_tmp'] = os.path.join( + project_update.get_project_path(check_if_exists=False), + '.ansible_awx', 'tmp' + ) + self._write_extra_vars_file(private_data_dir, extra_vars) def build_cwd(self, project_update, private_data_dir): From 90b9c7861c9797690a0cd0f14d7b7868f36cd2f4 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Fri, 18 Dec 2020 10:01:00 -0500 Subject: [PATCH 073/178] Allow jobs to run in the base ansible-runner image (#8949) --- awx/main/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 9ffcad4393..3dd9255cfb 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1137,7 +1137,7 @@ class BaseTask(object): fn = os.path.join(path, 'hosts') with open(fn, 'w') as f: os.chmod(fn, stat.S_IRUSR | stat.S_IXUSR | stat.S_IWUSR) - f.write('#! /usr/bin/env python\n# -*- coding: utf-8 -*-\nprint(%r)\n' % json_data) + f.write('#! /usr/bin/env python3\n# -*- coding: utf-8 -*-\nprint(%r)\n' % json_data) return fn def build_args(self, instance, private_data_dir, passwords): From 0c497fa6827f92884dabf5fb551fb5fe9d8a2cf6 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 4 Jan 2021 11:47:55 -0500 Subject: [PATCH 074/178] Get podman-in-docker working under cgroups v2 --- tools/ansible/roles/dockerfile/templates/Dockerfile.j2 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index ecdcb32733..838b0ef7c6 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -107,7 +107,6 @@ RUN dnf -y update && \ krb5-workstation \ libcgroup-tools \ nginx \ - podman \ @postgresql:12 \ python3-devel \ python3-libselinux \ @@ -157,6 +156,7 @@ RUN cd /usr/local/bin && \ {% if (build_dev|bool) or (kube_dev|bool) %} # Install development/test requirements RUN dnf -y install \ + crun \ gdb \ gtk3 \ gettext \ @@ -170,6 +170,7 @@ RUN dnf -y install \ nss \ make \ patch \ + podman \ socat \ tmux \ wget \ From acee22435b333ab7fd4ef2bbc9a115f25c500dd2 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 25 Jan 2021 17:39:23 -0500 Subject: [PATCH 075/178] Update ExecutionEnvironments.jsx with breadcrumb replacement --- .../screens/ExecutionEnvironment/ExecutionEnvironments.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.jsx index 7db0baaedc..d95c15959f 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.jsx @@ -6,7 +6,7 @@ import { Route, Switch } from 'react-router-dom'; import ExecutionEnvironment from './ExecutionEnvironment'; import ExecutionEnvironmentAdd from './ExecutionEnvironmentAdd'; import ExecutionEnvironmentList from './ExecutionEnvironmentList'; -import Breadcrumbs from '../../components/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader/ScreenHeader'; function ExecutionEnvironments({ i18n }) { const [breadcrumbConfig, setBreadcrumbConfig] = useState({ @@ -35,7 +35,10 @@ function ExecutionEnvironments({ i18n }) { ); return ( <> - + From 521d3d5edbc52d54cce8c22e9443574a5a727197 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Fri, 31 Jul 2020 09:43:46 -0400 Subject: [PATCH 076/178] Initial EE integration --- awx/main/tasks.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 3dd9255cfb..d51f3cb1a7 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1430,10 +1430,6 @@ class BaseTask(object): cwd = self.build_cwd(self.instance, private_data_dir) resource_profiling_params = self.build_params_resource_profiling(self.instance, private_data_dir) - # TODO: Remove if fully replaced with containerized runs - # process_isolation_params = self.build_params_process_isolation(self.instance, - # private_data_dir, - # cwd) execution_environment_params = self.build_execution_environment_params(self.instance) env = self.build_env(self.instance, private_data_dir, isolated, private_data_files=private_data_files) @@ -1469,7 +1465,6 @@ class BaseTask(object): 'settings': { 'job_timeout': self.get_instance_timeout(self.instance), 'suppress_ansible_output': True, - #**process_isolation_params, **resource_profiling_params, }, } From f1df4c54f835a115a3c1f9d1b9e6a3573a3be83a Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Thu, 12 Nov 2020 16:34:18 -0500 Subject: [PATCH 077/178] Begin integrating receptor --- awx/main/tasks.py | 122 ++++++++++++------ .../test_inventory_source_injectors.py | 4 + requirements/requirements_dev.txt | 1 + .../roles/dockerfile/templates/Dockerfile.j2 | 6 +- .../sources/templates/docker-compose.yml.j2 | 12 +- .../{receptor.cfg => receptor.conf} | 10 +- tools/docker-compose/supervisor.conf | 2 +- 7 files changed, 99 insertions(+), 58 deletions(-) rename tools/docker-compose/{receptor.cfg => receptor.conf} (59%) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index d51f3cb1a7..7a408265d9 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -23,6 +23,9 @@ import fcntl from pathlib import Path from uuid import uuid4 import urllib.parse as urlparse +import socket +import threading +import concurrent.futures # Django from django.conf import settings @@ -49,6 +52,9 @@ from gitdb.exc import BadName as BadGitName # Runner import ansible_runner +# Receptor +from receptorctl.socket_interface import ReceptorControl + # AWX from awx import __version__ as awx_application_version from awx.main.constants import PRIVILEGE_ESCALATION_METHODS, STANDARD_INVENTORY_UPDATE_ENV @@ -1453,15 +1459,10 @@ class BaseTask(object): params = { 'ident': self.instance.id, 'private_data_dir': private_data_dir, - 'project_dir': cwd, 'playbook': self.build_playbook_path_relative_to_cwd(self.instance, private_data_dir), 'inventory': self.build_inventory(self.instance, private_data_dir), 'passwords': expect_passwords, 'envvars': env, - 'event_handler': self.event_handler, - 'cancel_callback': self.cancel_callback, - 'finished_callback': self.finished_callback, - 'status_handler': self.status_handler, 'settings': { 'job_timeout': self.get_instance_timeout(self.instance), 'suppress_ansible_output': True, @@ -1473,10 +1474,7 @@ class BaseTask(object): # We don't want HOME passed through to container groups. # TODO: remove this conditional after everything is containerized params['envvars'].pop('HOME', None) - else: - # TODO: container group jobs will not work with container isolation settings - # but both will run with same settings when worker_in and worker_out are added - params['settings'].update(execution_environment_params) + if isinstance(self.instance, AdHocCommand): params['module'] = self.build_module_name(self.instance) @@ -1497,39 +1495,85 @@ class BaseTask(object): del params[v] self.dispatcher = CallbackQueueDispatcher() - if self.instance.is_isolated() or containerized: - module_args = None - if 'module_args' in params: - # if it's adhoc, copy the module args - module_args = ansible_runner.utils.args2cmdline( - params.get('module_args'), - ) - # TODO on merge: delete if https://github.com/ansible/awx/pull/8185 is merged - if not os.path.exists(os.path.join(private_data_dir, 'inventory')): - shutil.move( - params.pop('inventory'), - os.path.join(private_data_dir, 'inventory') - ) - ansible_runner.utils.dump_artifacts(params) - isolated_manager_instance = isolated_manager.IsolatedManager( - self.event_handler, - canceled_callback=lambda: self.update_model(self.instance.pk).cancel_flag, - check_callback=self.check_handler, - pod_manager=pod_manager - ) - status, rc = isolated_manager_instance.run(self.instance, - private_data_dir, - params.get('playbook'), - params.get('module'), - module_args, - ident=str(self.instance.pk)) - self.finished_callback(None) + if not isinstance(self.instance, ProjectUpdate): + worktype='worker' + # TODO: container group jobs will not work with container isolation settings + # but both will run with same settings when worker_in and worker_out are added + params['settings'].update(execution_environment_params) else: - res = ansible_runner.interface.run(**params) - status = res.status - rc = res.rc + worktype='worker' + params['settings'].update(execution_environment_params) + + # Create a socketpair. Where the left side will be used for writing our payload + # (private data dir, kwargs). The right side will be passed to Receptor for + # reading. + sockin, sockout = socket.socketpair() + + # Spawned in a thread so Receptor can start reading before we finish writing, we + # write our payload to the left side of our socketpair. + def transmit(_socket): + ansible_runner.interface.run(streamer='transmit', + _output=_socket.makefile('wb'), + **params) + + # Socket must be shutdown here, or the reader will hang forever. + _socket.shutdown(socket.SHUT_WR) + + threading.Thread(target=transmit, args=[sockin]).start() + + self.instance.log_lifecycle("running_playbook") + # We establish a connection to the Receptor socket and submit our work, passing + # in the right side of our socketpair for reading. + receptor_ctl = ReceptorControl('/var/run/receptor/receptor.sock') + result = receptor_ctl.submit_work(worktype=worktype, + payload=sockout.makefile('rb')) + sockin.close() + sockout.close() + + resultsock, resultfile = receptor_ctl.get_work_results(result['unitid'], + return_socket=True, + return_sockfile=True) + + def processor(): + return ansible_runner.interface.run(streamer='process', + quiet=True, + _input=resultfile, + event_handler=self.event_handler, + finished_callback=self.finished_callback, + status_handler=self.status_handler) + + def cancel_watcher(processor_future): + while True: + if processor_future.done(): + return + + if self.cancel_callback(): + result = namedtuple('result', ['status', 'rc']) + return result('canceled', 1) + time.sleep(1) + + # Both "processor" and "cancel_watcher" are spawned in separate threads. + # We wait for the first one to return. If cancel_watcher returns first, + # we yank the socket out from underneath the processor, which will cause it + # to exit. A reference to the processor_future is passed into the cancel_watcher_future, + # Which exits if the job has finished normally. The context manager ensures we do not + # leave any threads laying around. + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: + processor_future = executor.submit(processor) + cancel_watcher_future = executor.submit(cancel_watcher, processor_future) + futures = [processor_future, cancel_watcher_future] + first_future = concurrent.futures.wait(futures, + return_when=concurrent.futures.FIRST_COMPLETED) + + res = list(first_future.done)[0].result() + if res.status == 'canceled': + resultsock.shutdown(socket.SHUT_RDWR) + resultfile.close() + + status = res.status + rc = res.rc if status == 'timeout': self.instance.job_explanation = "Job terminated due to timeout" diff --git a/awx/main/tests/functional/test_inventory_source_injectors.py b/awx/main/tests/functional/test_inventory_source_injectors.py index bf1a4002c5..c4f7e6a17d 100644 --- a/awx/main/tests/functional/test_inventory_source_injectors.py +++ b/awx/main/tests/functional/test_inventory_source_injectors.py @@ -206,6 +206,10 @@ def test_inventory_update_injected_content(this_kind, inventory, fake_credential It will make assertions that the contents are correct If MAKE_INVENTORY_REFERENCE_FILES is set, it will produce reference files """ + if _kw.get('streamer') != 'transmit': + Res = namedtuple('Result', ['status', 'rc']) + return Res('successful', 0) + private_data_dir = envvars.pop('AWX_PRIVATE_DATA_DIR') assert envvars.pop('ANSIBLE_INVENTORY_ENABLED') == 'auto' set_files = bool(os.getenv("MAKE_INVENTORY_REFERENCE_FILES", 'false').lower()[0] not in ['f', '0']) diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt index fe51fff164..4788e153a2 100644 --- a/requirements/requirements_dev.txt +++ b/requirements/requirements_dev.txt @@ -20,5 +20,6 @@ matplotlib backports.tempfile # support in unit tests for py32+ tempfile.TemporaryDirectory mockldap sdb +remote-pdb gprof2dot atomicwrites==1.4.0 diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index 838b0ef7c6..0d48f2ea56 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -143,11 +143,6 @@ RUN ansible-galaxy collection install --collections-path /usr/share/ansible/coll RUN rm -rf /root/.cache && rm -rf /tmp/* -# Install Receptor -RUN cd /usr/local/bin && \ - curl -L http://nightlies.testing.ansible.com/receptor/receptor --output receptor && \ - chmod a+x receptor - # Install OpenShift CLI RUN cd /usr/local/bin && \ curl -L https://github.com/openshift/origin/releases/download/v3.11.0/openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz | \ @@ -190,6 +185,7 @@ COPY --from=builder /var/lib/awx /var/lib/awx RUN ln -s /var/lib/awx/venv/awx/bin/awx-manage /usr/bin/awx-manage {%if build_dev|bool %} +COPY --from=quay.io/shanemcd/receptor /usr/bin/receptor /usr/bin/receptor RUN openssl req -nodes -newkey rsa:2048 -keyout /etc/nginx/nginx.key -out /etc/nginx/nginx.csr \ -subj "/C=US/ST=North Carolina/L=Durham/O=Ansible/OU=AWX Development/CN=awx.localhost" && \ openssl x509 -req -days 365 -in /etc/nginx/nginx.csr -signkey /etc/nginx/nginx.key -out /etc/nginx/nginx.crt && \ diff --git a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 index eae187cea6..db81b4ba39 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 @@ -35,6 +35,8 @@ services: - "redis_socket:/var/run/redis/:rw" - "receptor:/var/run/receptor/" - "/sys/fs/cgroup:/sys/fs/cgroup" + - "./docker-compose/receptor.conf:/etc/receptor/receptor.conf" + - "~/.kube/config:/var/lib/awx/.kube/config" privileged: true tty: true # A useful container that simply passes through log messages to the console @@ -43,16 +45,6 @@ services: # build: # context: ./docker-compose # dockerfile: Dockerfile-logstash - ee: - image: quay.io/ansible/awx-ee - user: ${CURRENT_UID} - volumes: - - "./docker-compose/receptor.cfg:/receptor.cfg" - - "receptor:/var/run/receptor/" - command: - - receptor - - --config - - /receptor.cfg postgres: image: postgres:12 container_name: tools_postgres_1 diff --git a/tools/docker-compose/receptor.cfg b/tools/docker-compose/receptor.conf similarity index 59% rename from tools/docker-compose/receptor.cfg rename to tools/docker-compose/receptor.conf index 137d15cdf6..7df861f6ca 100644 --- a/tools/docker-compose/receptor.cfg +++ b/tools/docker-compose/receptor.conf @@ -5,11 +5,15 @@ service: control filename: /var/run/receptor/receptor.sock -- tcp-listener: - port: 2222 +- local-only: - work-command: worktype: worker command: ansible-runner params: worker - allowruntimeparams: true + +- work-kubernetes: + worktype: ocp + namespace: receptor + image: quay.io/shanemcd/ee + authmethod: kubeconfig diff --git a/tools/docker-compose/supervisor.conf b/tools/docker-compose/supervisor.conf index 04ddb66838..1a71b8018e 100644 --- a/tools/docker-compose/supervisor.conf +++ b/tools/docker-compose/supervisor.conf @@ -84,7 +84,7 @@ stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 [program:awx-receptor] -command = receptor --node id=%(ENV_HOSTNAME)s --control-service filename=/var/run/receptor/receptor.sock --tcp-listener port=2222 +command = receptor --config /etc/receptor/receptor.conf autostart = true autorestart = true stopsignal = KILL From 81f6d36a3a36615a84544de8dba11cd10358eaed Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Tue, 17 Nov 2020 13:40:45 -0500 Subject: [PATCH 078/178] Set SDB_NOTIFY_HOST for all processes --- awx/settings/development.py | 9 --------- tools/docker-compose/entrypoint.sh | 6 ++++++ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/awx/settings/development.py b/awx/settings/development.py index 6181d16ec6..d181ca10fc 100644 --- a/awx/settings/development.py +++ b/awx/settings/development.py @@ -177,15 +177,6 @@ CELERYBEAT_SCHEDULE.update({ # noqa CLUSTER_HOST_ID = socket.gethostname() - -if 'Docker Desktop' in os.getenv('OS', ''): - os.environ['SDB_NOTIFY_HOST'] = 'docker.for.mac.host.internal' -else: - try: - os.environ['SDB_NOTIFY_HOST'] = os.popen('ip route').read().split(' ')[2] - except Exception: - pass - AWX_CALLBACK_PROFILE = True if 'sqlite3' not in DATABASES['default']['ENGINE']: # noqa diff --git a/tools/docker-compose/entrypoint.sh b/tools/docker-compose/entrypoint.sh index 13c858b441..c155f022e4 100755 --- a/tools/docker-compose/entrypoint.sh +++ b/tools/docker-compose/entrypoint.sh @@ -21,4 +21,10 @@ fi # writing out the sub*id files above podman system migrate +if [[ "$OS" == *"Docker Desktop"* ]]; then + export SDB_NOTIFY_HOST='docker.for.mac.host.internal' +else + export SDB_NOTIFY_HOST=$(ip route | head -n1 | awk '{print $3}') +fi + exec $@ From 0184a7c267d9de3acb320f2ddb72e7772ea2dafd Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Wed, 18 Nov 2020 16:33:35 -0500 Subject: [PATCH 079/178] Create receptor mesh in cluster development environment --- tools/docker-compose-cluster.yml | 16 +++++++++++++ .../awx-1-receptor.conf | 23 +++++++++++++++++++ .../awx-2-receptor.conf | 23 +++++++++++++++++++ .../awx-3-receptor.conf | 23 +++++++++++++++++++ tools/docker-compose/supervisor.conf | 3 +++ 5 files changed, 88 insertions(+) create mode 100644 tools/docker-compose-cluster/awx-1-receptor.conf create mode 100644 tools/docker-compose-cluster/awx-2-receptor.conf create mode 100644 tools/docker-compose-cluster/awx-3-receptor.conf diff --git a/tools/docker-compose-cluster.yml b/tools/docker-compose-cluster.yml index 7a90aa88c9..6065069125 100644 --- a/tools/docker-compose-cluster.yml +++ b/tools/docker-compose-cluster.yml @@ -14,6 +14,7 @@ services: - "8013:8013" - "8043:8043" - "1936:1936" + awx-1: user: ${CURRENT_UID} container_name: tools_awx_1_1 @@ -31,9 +32,11 @@ services: - "../:/awx_devel" - "./redis/redis_socket_ha_1:/var/run/redis/" - "./docker-compose/supervisor.conf:/etc/supervisord.conf" + - "./docker-compose-cluster/awx-1-receptor.conf:/etc/receptor/receptor.conf" ports: - "2222:2222" - "5899-5999:5899-5999" + awx-2: user: ${CURRENT_UID} container_name: tools_awx_2_1 @@ -51,9 +54,11 @@ services: - "../:/awx_devel" - "./redis/redis_socket_ha_2:/var/run/redis/" - "./docker-compose/supervisor.conf:/etc/supervisord.conf" + - "./docker-compose-cluster/awx-2-receptor.conf:/etc/receptor/receptor.conf" ports: - "2223:2222" - "7899-7999:7899-7999" + awx-3: user: ${CURRENT_UID} container_name: tools_awx_3_1 @@ -71,9 +76,11 @@ services: - "../:/awx_devel" - "./redis/redis_socket_ha_3:/var/run/redis/" - "./docker-compose/supervisor.conf:/etc/supervisord.conf" + - "./docker-compose-cluster/awx-3-receptor.conf:/etc/receptor/receptor.conf" ports: - "2224:2222" - "8899-8999:8899-8999" + redis_1: user: ${CURRENT_UID} image: redis:latest @@ -82,6 +89,7 @@ services: volumes: - "./redis/redis.conf:/usr/local/etc/redis/redis.conf" - "./redis/redis_socket_ha_1:/var/run/redis/" + redis_2: user: ${CURRENT_UID} image: redis:latest @@ -98,6 +106,14 @@ services: volumes: - "./redis/redis.conf:/usr/local/etc/redis/redis.conf" - "./redis/redis_socket_ha_3:/var/run/redis/" + postgres: image: postgres:12 container_name: tools_postgres_1 + environment: + POSTGRES_HOST_AUTH_METHOD: trust + volumes: + - "awx_db:/var/lib/postgresql/data" + +volumes: + awx_db: diff --git a/tools/docker-compose-cluster/awx-1-receptor.conf b/tools/docker-compose-cluster/awx-1-receptor.conf new file mode 100644 index 0000000000..dcaca8263f --- /dev/null +++ b/tools/docker-compose-cluster/awx-1-receptor.conf @@ -0,0 +1,23 @@ +--- +- log-level: debug + +- control-service: + service: control + filename: /var/run/receptor/receptor.sock + +- tcp-listener: + port: 2200 + +- tcp-peer: + address: awx-2:2200 + +- work-command: + worktype: worker + command: ansible-runner + params: worker + +- work-kubernetes: + worktype: ocp + namespace: receptor + image: quay.io/shanemcd/ee + authmethod: kubeconfig diff --git a/tools/docker-compose-cluster/awx-2-receptor.conf b/tools/docker-compose-cluster/awx-2-receptor.conf new file mode 100644 index 0000000000..bf9d4889a0 --- /dev/null +++ b/tools/docker-compose-cluster/awx-2-receptor.conf @@ -0,0 +1,23 @@ +--- +- log-level: debug + +- control-service: + service: control + filename: /var/run/receptor/receptor.sock + +- tcp-listener: + port: 2200 + +- tcp-peer: + address: awx-3:2200 + +- work-command: + worktype: worker + command: ansible-runner + params: worker + +- work-kubernetes: + worktype: ocp + namespace: receptor + image: quay.io/shanemcd/ee + authmethod: kubeconfig diff --git a/tools/docker-compose-cluster/awx-3-receptor.conf b/tools/docker-compose-cluster/awx-3-receptor.conf new file mode 100644 index 0000000000..ac5db0d284 --- /dev/null +++ b/tools/docker-compose-cluster/awx-3-receptor.conf @@ -0,0 +1,23 @@ +--- +- log-level: debug + +- control-service: + service: control + filename: /var/run/receptor/receptor.sock + +- tcp-listener: + port: 2200 + +- tcp-peer: + address: awx-1:2200 + +- work-command: + worktype: worker + command: ansible-runner + params: worker + +- work-kubernetes: + worktype: ocp + namespace: receptor + image: quay.io/shanemcd/ee + authmethod: kubeconfig diff --git a/tools/docker-compose/supervisor.conf b/tools/docker-compose/supervisor.conf index 1a71b8018e..fc2eb2d028 100644 --- a/tools/docker-compose/supervisor.conf +++ b/tools/docker-compose/supervisor.conf @@ -90,6 +90,9 @@ autorestart = true stopsignal = KILL stopasgroup = true killasgroup = true +redirect_stderr=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 [group:tower-processes] programs=awx-dispatcher,awx-receiver,awx-uwsgi,awx-daphne,awx-nginx,awx-wsbroadcast,awx-rsyslogd From fd92ba0c0b32271bfc3e6e63a73524511d9efe3b Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Thu, 19 Nov 2020 12:44:28 -0500 Subject: [PATCH 080/178] Actually cancel things --- awx/main/tasks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 7a408265d9..615eb90a4f 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1569,9 +1569,11 @@ class BaseTask(object): res = list(first_future.done)[0].result() if res.status == 'canceled': + receptor_ctl.simple_command(f"work cancel {result['unitid']}") resultsock.shutdown(socket.SHUT_RDWR) resultfile.close() + receptor_ctl.simple_command(f"work release {result['unitid']}") status = res.status rc = res.rc From be8168b555cbe7d978fb54d247e29931eb63104e Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Sat, 21 Nov 2020 11:01:09 -0500 Subject: [PATCH 081/178] Surface errors when launching jobs through Receptor This will raise errors such as: exec: "ansible-runner": executable file not found in $PATH --- awx/main/tasks.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 615eb90a4f..afa6982ada 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1529,10 +1529,12 @@ class BaseTask(object): receptor_ctl = ReceptorControl('/var/run/receptor/receptor.sock') result = receptor_ctl.submit_work(worktype=worktype, payload=sockout.makefile('rb')) + unit_id = result['unitid'] + sockin.close() sockout.close() - resultsock, resultfile = receptor_ctl.get_work_results(result['unitid'], + resultsock, resultfile = receptor_ctl.get_work_results(unit_id, return_socket=True, return_sockfile=True) @@ -1547,7 +1549,7 @@ class BaseTask(object): def cancel_watcher(processor_future): while True: if processor_future.done(): - return + return processor_future.result() if self.cancel_callback(): result = namedtuple('result', ['status', 'rc']) @@ -1569,11 +1571,15 @@ class BaseTask(object): res = list(first_future.done)[0].result() if res.status == 'canceled': - receptor_ctl.simple_command(f"work cancel {result['unitid']}") + receptor_ctl.simple_command(f"work cancel {unit_id}") resultsock.shutdown(socket.SHUT_RDWR) resultfile.close() + elif res.status == 'error': + # TODO: There should be a more efficient way of getting this information + receptor_work_list = receptor_ctl.simple_command("work list") + raise RuntimeError(receptor_work_list[unit_id]['Detail']) - receptor_ctl.simple_command(f"work release {result['unitid']}") + receptor_ctl.simple_command(f"work release {unit_id}") status = res.status rc = res.rc From cf96275f1b52f089892a1d648212d500841cca90 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 23 Nov 2020 14:56:50 -0500 Subject: [PATCH 082/178] Pull awx -> receptor job code into its own class --- awx/main/tasks.py | 163 ++++++++++-------- .../test_inventory_source_injectors.py | 8 +- 2 files changed, 90 insertions(+), 81 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index afa6982ada..74a414026c 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1433,7 +1433,6 @@ class BaseTask(object): passwords = self.build_passwords(self.instance, kwargs) self.build_extra_vars_file(self.instance, private_data_dir) args = self.build_args(self.instance, private_data_dir, passwords) - cwd = self.build_cwd(self.instance, private_data_dir) resource_profiling_params = self.build_params_resource_profiling(self.instance, private_data_dir) execution_environment_params = self.build_execution_environment_params(self.instance) @@ -1497,89 +1496,18 @@ class BaseTask(object): self.dispatcher = CallbackQueueDispatcher() if not isinstance(self.instance, ProjectUpdate): - worktype='worker' + work_type='worker' # TODO: container group jobs will not work with container isolation settings # but both will run with same settings when worker_in and worker_out are added params['settings'].update(execution_environment_params) else: - worktype='worker' + work_type='worker' params['settings'].update(execution_environment_params) - # Create a socketpair. Where the left side will be used for writing our payload - # (private data dir, kwargs). The right side will be passed to Receptor for - # reading. - sockin, sockout = socket.socketpair() - - # Spawned in a thread so Receptor can start reading before we finish writing, we - # write our payload to the left side of our socketpair. - def transmit(_socket): - ansible_runner.interface.run(streamer='transmit', - _output=_socket.makefile('wb'), - **params) - - # Socket must be shutdown here, or the reader will hang forever. - _socket.shutdown(socket.SHUT_WR) - - threading.Thread(target=transmit, args=[sockin]).start() - - self.instance.log_lifecycle("running_playbook") - # We establish a connection to the Receptor socket and submit our work, passing - # in the right side of our socketpair for reading. - receptor_ctl = ReceptorControl('/var/run/receptor/receptor.sock') - result = receptor_ctl.submit_work(worktype=worktype, - payload=sockout.makefile('rb')) - unit_id = result['unitid'] + receptor_job = AWXReceptorJob(self, work_type, params) + res = receptor_job.run() - sockin.close() - sockout.close() - - resultsock, resultfile = receptor_ctl.get_work_results(unit_id, - return_socket=True, - return_sockfile=True) - - def processor(): - return ansible_runner.interface.run(streamer='process', - quiet=True, - _input=resultfile, - event_handler=self.event_handler, - finished_callback=self.finished_callback, - status_handler=self.status_handler) - - def cancel_watcher(processor_future): - while True: - if processor_future.done(): - return processor_future.result() - - if self.cancel_callback(): - result = namedtuple('result', ['status', 'rc']) - return result('canceled', 1) - time.sleep(1) - - # Both "processor" and "cancel_watcher" are spawned in separate threads. - # We wait for the first one to return. If cancel_watcher returns first, - # we yank the socket out from underneath the processor, which will cause it - # to exit. A reference to the processor_future is passed into the cancel_watcher_future, - # Which exits if the job has finished normally. The context manager ensures we do not - # leave any threads laying around. - with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: - processor_future = executor.submit(processor) - cancel_watcher_future = executor.submit(cancel_watcher, processor_future) - futures = [processor_future, cancel_watcher_future] - first_future = concurrent.futures.wait(futures, - return_when=concurrent.futures.FIRST_COMPLETED) - - res = list(first_future.done)[0].result() - if res.status == 'canceled': - receptor_ctl.simple_command(f"work cancel {unit_id}") - resultsock.shutdown(socket.SHUT_RDWR) - resultfile.close() - elif res.status == 'error': - # TODO: There should be a more efficient way of getting this information - receptor_work_list = receptor_ctl.simple_command("work list") - raise RuntimeError(receptor_work_list[unit_id]['Detail']) - - receptor_ctl.simple_command(f"work release {unit_id}") status = res.status rc = res.rc @@ -3201,3 +3129,86 @@ def deep_copy_model_obj( permission_check_func(creater, copy_mapping.values()) if isinstance(new_obj, Inventory): update_inventory_computed_fields.delay(new_obj.id) + + +class AWXReceptorJob: + def __init__(self, task, work_type, runner_params): + self.task = task + self.work_type = work_type + self.runner_params = runner_params + + def run(self): + # Create a socketpair. Where the left side will be used for writing our payload + # (private data dir, kwargs). The right side will be passed to Receptor for + # reading. + sockin, sockout = socket.socketpair() + + threading.Thread(target=self.transmit, args=[sockin]).start() + + # We establish a connection to the Receptor socket and submit our work, passing + # in the right side of our socketpair for reading. + receptor_ctl = ReceptorControl('/var/run/receptor/receptor.sock') + result = receptor_ctl.submit_work(worktype=self.work_type, + payload=sockout.makefile('rb')) + unit_id = result['unitid'] + + sockin.close() + sockout.close() + + resultsock, resultfile = receptor_ctl.get_work_results(unit_id, + return_socket=True, + return_sockfile=True) + # Both "processor" and "cancel_watcher" are spawned in separate threads. + # We wait for the first one to return. If cancel_watcher returns first, + # we yank the socket out from underneath the processor, which will cause it + # to exit. A reference to the processor_future is passed into the cancel_watcher_future, + # Which exits if the job has finished normally. The context manager ensures we do not + # leave any threads laying around. + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: + processor_future = executor.submit(self.processor, resultfile) + cancel_watcher_future = executor.submit(self.cancel_watcher, processor_future) + futures = [processor_future, cancel_watcher_future] + first_future = concurrent.futures.wait(futures, + return_when=concurrent.futures.FIRST_COMPLETED) + + res = list(first_future.done)[0].result() + if res.status == 'canceled': + receptor_ctl.simple_command(f"work cancel {unit_id}") + resultsock.shutdown(socket.SHUT_RDWR) + resultfile.close() + elif res.status == 'error': + # TODO: There should be a more efficient way of getting this information + receptor_work_list = receptor_ctl.simple_command("work list") + raise RuntimeError(receptor_work_list[unit_id]['Detail']) + + + receptor_ctl.simple_command(f"work release {unit_id}") + return res + + # Spawned in a thread so Receptor can start reading before we finish writing, we + # write our payload to the left side of our socketpair. + def transmit(self, _socket): + ansible_runner.interface.run(streamer='transmit', + _output=_socket.makefile('wb'), + **self.runner_params) + + # Socket must be shutdown here, or the reader will hang forever. + _socket.shutdown(socket.SHUT_WR) + + def processor(self, resultfile): + return ansible_runner.interface.run(streamer='process', + quiet=True, + _input=resultfile, + event_handler=self.task.event_handler, + finished_callback=self.task.finished_callback, + status_handler=self.task.status_handler) + + def cancel_watcher(self, processor_future): + while True: + if processor_future.done(): + return processor_future.result() + + if self.task.cancel_callback(): + result = namedtuple('result', ['status', 'rc']) + return result('canceled', 1) + time.sleep(1) diff --git a/awx/main/tests/functional/test_inventory_source_injectors.py b/awx/main/tests/functional/test_inventory_source_injectors.py index c4f7e6a17d..f9edfdcd22 100644 --- a/awx/main/tests/functional/test_inventory_source_injectors.py +++ b/awx/main/tests/functional/test_inventory_source_injectors.py @@ -200,15 +200,13 @@ def test_inventory_update_injected_content(this_kind, inventory, fake_credential inventory_update = inventory_source.create_unified_job() task = RunInventoryUpdate() - def substitute_run(envvars=None, **_kw): + def substitute_run(awx_receptor_job): """This method will replace run_pexpect instead of running, it will read the private data directory contents It will make assertions that the contents are correct If MAKE_INVENTORY_REFERENCE_FILES is set, it will produce reference files """ - if _kw.get('streamer') != 'transmit': - Res = namedtuple('Result', ['status', 'rc']) - return Res('successful', 0) + envvars = awx_receptor_job.runner_params['envvars'] private_data_dir = envvars.pop('AWX_PRIVATE_DATA_DIR') assert envvars.pop('ANSIBLE_INVENTORY_ENABLED') == 'auto' @@ -260,6 +258,6 @@ def test_inventory_update_injected_content(this_kind, inventory, fake_credential # Also do not send websocket status updates with mock.patch.object(UnifiedJob, 'websocket_emit_status', mock.Mock()): # The point of this test is that we replace run with assertions - with mock.patch('awx.main.tasks.ansible_runner.interface.run', substitute_run): + with mock.patch('awx.main.tasks.AWXReceptorJob.run', substitute_run): # so this sets up everything for a run and then yields control over to substitute_run task.run(inventory_update.pk) From e453afa0643cadbcabfe5ad060f4455ec4efbf19 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 23 Nov 2020 18:38:06 -0500 Subject: [PATCH 083/178] FOLLOW UP ON THIS: Fix fact_cache directory location The part where we pass in the runner params to the processor phase is legit. Need to investigate why the fact_cache directory is no longer nested under job.id. --- awx/main/tasks.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 74a414026c..f96050616b 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1994,7 +1994,7 @@ class RunJob(BaseTask): return if job.use_fact_cache: job.finish_job_fact_cache( - os.path.join(private_data_dir, 'artifacts', str(job.id), 'fact_cache'), + os.path.join(private_data_dir, 'artifacts', 'fact_cache'), fact_modification_times, ) if isolated_manager_instance and not job.is_containerized: @@ -3201,7 +3201,8 @@ class AWXReceptorJob: _input=resultfile, event_handler=self.task.event_handler, finished_callback=self.task.finished_callback, - status_handler=self.task.status_handler) + status_handler=self.task.status_handler, + **self.runner_params) def cancel_watcher(self, processor_future): while True: From 7b7465f168bcd202aa423e920a014b68eadd9322 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 11 Jan 2021 19:55:03 -0500 Subject: [PATCH 084/178] Update receptor config to allow for runtime options --- tools/docker-compose/receptor.conf | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tools/docker-compose/receptor.conf b/tools/docker-compose/receptor.conf index 7df861f6ca..ba975b8820 100644 --- a/tools/docker-compose/receptor.conf +++ b/tools/docker-compose/receptor.conf @@ -14,6 +14,7 @@ - work-kubernetes: worktype: ocp - namespace: receptor - image: quay.io/shanemcd/ee - authmethod: kubeconfig + authmethod: runtime + allowruntimeauth: true + allowruntimepod: true + allowruntimeparams: true From 286b1d4e25a82efe6ecd74b39ba713b6a5b878b7 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Wed, 13 Jan 2021 19:38:03 -0500 Subject: [PATCH 085/178] InstanceGroup#is_containerized -> InstanceGroup#is_container_group --- awx/api/serializers.py | 12 ++++++------ awx/api/views/__init__.py | 2 +- awx/main/models/ad_hoc_commands.py | 4 ++-- awx/main/models/ha.py | 8 ++++---- awx/main/models/jobs.py | 6 +++--- awx/main/scheduler/task_manager.py | 12 ++++++------ awx/main/tasks.py | 4 ++-- awx/main/tests/functional/api/test_instance_group.py | 5 ++--- .../task_management/test_container_groups.py | 2 +- awx/ui_next/SEARCH.md | 8 ++++---- .../ContainerGroupDetails.test.jsx | 2 +- .../ContainerGroupEdit/ContainerGroupEdit.test.jsx | 2 +- .../InstanceGroupAdd/InstanceGroupAdd.test.jsx | 2 +- .../InstanceGroupDetails/InstanceGroupDetails.jsx | 2 +- .../InstanceGroupDetails.test.jsx | 4 ++-- .../InstanceGroupEdit/InstanceGroupEdit.test.jsx | 2 +- .../InstanceGroupList/InstanceGroupList.jsx | 2 +- .../InstanceGroupList/InstanceGroupListItem.jsx | 2 +- .../InstanceGroupList/InstanceGroupListItem.test.jsx | 4 ++-- .../InstanceGroup/shared/ContainerGroupForm.test.jsx | 2 +- .../InstanceGroup/shared/InstanceGroupForm.test.jsx | 2 +- 21 files changed, 44 insertions(+), 45 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 68e507a6d2..3a28239b0d 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -131,7 +131,7 @@ SUMMARIZABLE_FK_FIELDS = { 'source_script': DEFAULT_SUMMARY_FIELDS, 'role': ('id', 'role_field'), 'notification_template': DEFAULT_SUMMARY_FIELDS, - 'instance_group': ('id', 'name', 'controller_id', 'is_containerized'), + 'instance_group': ('id', 'name', 'controller_id', 'is_container_group'), 'insights_credential': DEFAULT_SUMMARY_FIELDS, 'source_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'), 'target_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'), @@ -4768,7 +4768,7 @@ class InstanceGroupSerializer(BaseSerializer): 'Isolated groups have a designated controller group.'), read_only=True ) - is_containerized = serializers.BooleanField( + is_container_group = serializers.BooleanField( help_text=_('Indicates whether instances in this group are containerized.' 'Containerized groups have a designated Openshift or Kubernetes cluster.'), read_only=True @@ -4798,7 +4798,7 @@ class InstanceGroupSerializer(BaseSerializer): fields = ("id", "type", "url", "related", "name", "created", "modified", "capacity", "committed_capacity", "consumed_capacity", "percent_capacity_remaining", "jobs_running", "jobs_total", - "instances", "controller", "is_controller", "is_isolated", "is_containerized", "credential", + "instances", "controller", "is_controller", "is_isolated", "is_container_group", "credential", "policy_instance_percentage", "policy_instance_minimum", "policy_instance_list", "pod_spec_override", "summary_fields") @@ -4823,17 +4823,17 @@ class InstanceGroupSerializer(BaseSerializer): raise serializers.ValidationError(_('Isolated instances may not be added or removed from instances groups via the API.')) if self.instance and self.instance.controller_id is not None: raise serializers.ValidationError(_('Isolated instance group membership may not be managed via the API.')) - if value and self.instance and self.instance.is_containerized: + if value and self.instance and self.instance.is_container_group: raise serializers.ValidationError(_('Containerized instances may not be managed via the API')) return value def validate_policy_instance_percentage(self, value): - if value and self.instance and self.instance.is_containerized: + if value and self.instance and self.instance.is_container_group: raise serializers.ValidationError(_('Containerized instances may not be managed via the API')) return value def validate_policy_instance_minimum(self, value): - if value and self.instance and self.instance.is_containerized: + if value and self.instance and self.instance.is_container_group: raise serializers.ValidationError(_('Containerized instances may not be managed via the API')) return value diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 7ec932c9ce..01ad6d98ed 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -397,7 +397,7 @@ class InstanceGroupDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAP permission_classes = (InstanceGroupTowerPermission,) def update_raw_data(self, data): - if self.get_object().is_containerized: + if self.get_object().is_container_group: data.pop('policy_instance_percentage', None) data.pop('policy_instance_minimum', None) data.pop('policy_instance_list', None) diff --git a/awx/main/models/ad_hoc_commands.py b/awx/main/models/ad_hoc_commands.py index 54269fff1e..f327e2a7e6 100644 --- a/awx/main/models/ad_hoc_commands.py +++ b/awx/main/models/ad_hoc_commands.py @@ -151,8 +151,8 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin): return True @property - def is_containerized(self): - return bool(self.instance_group and self.instance_group.is_containerized) + def is_container_group_task(self): + return bool(self.instance_group and self.instance_group.is_container_group) @property def can_run_containerized(self): diff --git a/awx/main/models/ha.py b/awx/main/models/ha.py index 5071786653..4f96bdc5b1 100644 --- a/awx/main/models/ha.py +++ b/awx/main/models/ha.py @@ -247,7 +247,7 @@ class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin): return bool(self.controller) @property - def is_containerized(self): + def is_container_group(self): return bool(self.credential and self.credential.kubernetes) ''' @@ -306,9 +306,9 @@ def schedule_policy_task(): @receiver(post_save, sender=InstanceGroup) def on_instance_group_saved(sender, instance, created=False, raw=False, **kwargs): if created or instance.has_policy_changes(): - if not instance.is_containerized: + if not instance.is_container_group: schedule_policy_task() - elif created or instance.is_containerized: + elif created or instance.is_container_group: instance.set_default_policy_fields() @@ -320,7 +320,7 @@ def on_instance_saved(sender, instance, created=False, raw=False, **kwargs): @receiver(post_delete, sender=InstanceGroup) def on_instance_group_deleted(sender, instance, using, **kwargs): - if not instance.is_containerized: + if not instance.is_container_group: schedule_policy_task() diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 31f4784962..81e17cdebf 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -768,11 +768,11 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana @property def can_run_containerized(self): - return any([ig for ig in self.preferred_instance_groups if ig.is_containerized]) + return any([ig for ig in self.preferred_instance_groups if ig.is_container_group]) @property - def is_containerized(self): - return bool(self.instance_group and self.instance_group.is_containerized) + def is_container_group_task(self): + return bool(self.instance_group and self.instance_group.is_container_group) @property def preferred_instance_groups(self): diff --git a/awx/main/scheduler/task_manager.py b/awx/main/scheduler/task_manager.py index f06f93834a..8731ca3109 100644 --- a/awx/main/scheduler/task_manager.py +++ b/awx/main/scheduler/task_manager.py @@ -283,12 +283,12 @@ class TaskManager(): task.controller_node = controller_node logger.debug('Submitting isolated {} to queue {} controlled by {}.'.format( task.log_format, task.execution_node, controller_node)) - elif rampart_group.is_containerized: + elif rampart_group.is_container_group: # find one real, non-containerized instance with capacity to # act as the controller for k8s API interaction match = None for group in InstanceGroup.objects.all(): - if group.is_containerized or group.controller_id: + if group.is_container_group or group.controller_id: continue match = group.fit_task_to_most_remaining_capacity_instance(task, group.instances.all()) if match: @@ -521,14 +521,14 @@ class TaskManager(): self.start_task(task, None, task.get_jobs_fail_chain(), None) continue for rampart_group in preferred_instance_groups: - if task.can_run_containerized and rampart_group.is_containerized: + if task.can_run_containerized and rampart_group.is_container_group: self.graph[rampart_group.name]['graph'].add_job(task) self.start_task(task, rampart_group, task.get_jobs_fail_chain(), None) found_acceptable_queue = True break remaining_capacity = self.get_remaining_capacity(rampart_group.name) - if not rampart_group.is_containerized and self.get_remaining_capacity(rampart_group.name) <= 0: + if not rampart_group.is_container_group and self.get_remaining_capacity(rampart_group.name) <= 0: logger.debug("Skipping group {}, remaining_capacity {} <= 0".format( rampart_group.name, remaining_capacity)) continue @@ -536,8 +536,8 @@ class TaskManager(): execution_instance = InstanceGroup.fit_task_to_most_remaining_capacity_instance(task, self.graph[rampart_group.name]['instances']) or \ InstanceGroup.find_largest_idle_instance(self.graph[rampart_group.name]['instances']) - if execution_instance or rampart_group.is_containerized: - if not rampart_group.is_containerized: + if execution_instance or rampart_group.is_container_group: + if not rampart_group.is_container_group: execution_instance.remaining_capacity = max(0, execution_instance.remaining_capacity - task.task_impact) execution_instance.jobs_running += 1 logger.debug("Starting {} in group {} instance {} (remaining_capacity={})".format( diff --git a/awx/main/tasks.py b/awx/main/tasks.py index f96050616b..efc4f4594f 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -262,7 +262,7 @@ def apply_cluster_membership_policies(): # On a differential basis, apply instances to non-isolated groups with transaction.atomic(): for g in actual_groups: - if g.obj.is_containerized: + if g.obj.is_container_group: logger.debug('Skipping containerized group {} for policy calculation'.format(g.obj.name)) continue instances_to_add = set(g.instances) - set(g.prior_instances) @@ -507,7 +507,7 @@ def cluster_node_heartbeat(): def awx_k8s_reaper(): from awx.main.scheduler.kubernetes import PodManager # prevent circular import for group in InstanceGroup.objects.filter(credential__isnull=False).iterator(): - if group.is_containerized: + if group.is_container_group: logger.debug("Checking for orphaned k8s pods for {}.".format(group)) for job in UnifiedJob.objects.filter( pk__in=list(PodManager.list_active_jobs(group)) diff --git a/awx/main/tests/functional/api/test_instance_group.py b/awx/main/tests/functional/api/test_instance_group.py index 43c7d51960..61c1054912 100644 --- a/awx/main/tests/functional/api/test_instance_group.py +++ b/awx/main/tests/functional/api/test_instance_group.py @@ -255,7 +255,7 @@ def test_instance_group_update_fields(patch, instance, instance_group, admin, co # policy_instance_ variables can only be updated in instance groups that are NOT containerized # instance group (not containerized) ig_url = reverse("api:instance_group_detail", kwargs={'pk': instance_group.pk}) - assert not instance_group.is_containerized + assert not instance_group.is_container_group assert not containerized_instance_group.is_isolated resp = patch(ig_url, {'policy_instance_percentage':15}, admin, expect=200) assert 15 == resp.data['policy_instance_percentage'] @@ -266,7 +266,7 @@ def test_instance_group_update_fields(patch, instance, instance_group, admin, co # containerized instance group cg_url = reverse("api:instance_group_detail", kwargs={'pk': containerized_instance_group.pk}) - assert containerized_instance_group.is_containerized + assert containerized_instance_group.is_container_group assert not containerized_instance_group.is_isolated resp = patch(cg_url, {'policy_instance_percentage':15}, admin, expect=400) assert ["Containerized instances may not be managed via the API"] == resp.data['policy_instance_percentage'] @@ -291,4 +291,3 @@ def test_containerized_group_default_fields(instance_group, kube_credential): assert ig.policy_instance_list == [] assert ig.policy_instance_minimum == 0 assert ig.policy_instance_percentage == 0 - \ No newline at end of file diff --git a/awx/main/tests/functional/task_management/test_container_groups.py b/awx/main/tests/functional/task_management/test_container_groups.py index 47d982a725..c9028efacb 100644 --- a/awx/main/tests/functional/task_management/test_container_groups.py +++ b/awx/main/tests/functional/task_management/test_container_groups.py @@ -30,7 +30,7 @@ def containerized_job(default_instance_group, kube_credential, job_template_fact @pytest.mark.django_db def test_containerized_job(containerized_job): assert containerized_job.is_containerized - assert containerized_job.instance_group.is_containerized + assert containerized_job.instance_group.is_container_group assert containerized_job.instance_group.credential.kubernetes diff --git a/awx/ui_next/SEARCH.md b/awx/ui_next/SEARCH.md index 111dfb2f56..131e4fe277 100644 --- a/awx/ui_next/SEARCH.md +++ b/awx/ui_next/SEARCH.md @@ -86,7 +86,7 @@ Instances of orgs list include: **Instance Groups list** - Name - search is ?name=ig - - ? is_containerized boolean choice (doesn't work right now in API but will soon) - search is ?is_containerized=true + - ? is_container_group boolean choice (doesn't work right now in API but will soon) - search is ?is_container_group=true - ? credential name - search is ?credentials__name=kubey Instance of instance groups list include: @@ -136,7 +136,7 @@ Instance of team lists include: **Credentials list** - Name - - ? Type (dropdown on right with different types) + - ? Type (dropdown on right with different types) - ? Created by (username) - ? Modified by (username) @@ -273,7 +273,7 @@ For the UI url params, we want to only encode those params that aren't defaults, #### mergeParams vs. replaceParams -**mergeParams** is used to suppport putting values with the same key +**mergeParams** is used to suppport putting values with the same key From a UX perspective, we wanted to be able to support searching on the same key multiple times (i.e. searching for things like `?foo=bar&foo=baz`). We do this by creating an array of all values. i.e.: @@ -361,7 +361,7 @@ Smart search will be able to craft the tag through various states. Note that th "instance_groups__search" ], ``` - + PHASE 3: keys, give by object key names for data.actions.GET - type is given for each key which we could use to help craft the value diff --git a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupDetails/ContainerGroupDetails.test.jsx b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupDetails/ContainerGroupDetails.test.jsx index aa979ef730..68ca23f80a 100644 --- a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupDetails/ContainerGroupDetails.test.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupDetails/ContainerGroupDetails.test.jsx @@ -32,7 +32,7 @@ const instanceGroup = { controller: null, is_controller: false, is_isolated: false, - is_containerized: true, + is_container_group: true, credential: 71, policy_instance_percentage: 0, policy_instance_minimum: 0, diff --git a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.test.jsx b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.test.jsx index 937aa15adb..860c6363c5 100644 --- a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.test.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.test.jsx @@ -31,7 +31,7 @@ const instanceGroup = { controller: null, is_controller: false, is_isolated: false, - is_containerized: true, + is_container_group: true, credential: 71, policy_instance_percentage: 0, policy_instance_minimum: 0, diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupAdd/InstanceGroupAdd.test.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupAdd/InstanceGroupAdd.test.jsx index 4b2d879398..a4ae1e74fc 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupAdd/InstanceGroupAdd.test.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupAdd/InstanceGroupAdd.test.jsx @@ -29,7 +29,7 @@ const instanceGroupData = { controller: null, is_controller: false, is_isolated: false, - is_containerized: false, + is_container_group: false, credential: null, policy_instance_percentage: 46, policy_instance_minimum: 12, diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx index 748e92702d..05ad3277dc 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx @@ -78,7 +78,7 @@ function InstanceGroupDetails({ instanceGroup, i18n }) { { - return item.is_containerized + return item.is_container_group ? `${match.url}/container_group/${item.id}/details` : `${match.url}/${item.id}/details`; }; diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx index 8bfcf05325..4c47269074 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx @@ -32,7 +32,7 @@ function InstanceGroupListItem({ const labelId = `check-action-${instanceGroup.id}`; const isContainerGroup = item => { - return item.is_containerized; + return item.is_container_group; }; function usedCapacity(item) { diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.test.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.test.jsx index 0f22a4b6d7..579aa36dcc 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.test.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.test.jsx @@ -17,7 +17,7 @@ describe('', () => { policy_instance_minimum: 10, policy_instance_percentage: 50, percent_capacity_remaining: 60, - is_containerized: false, + is_container_group: false, summary_fields: { user_capabilities: { edit: true, @@ -34,7 +34,7 @@ describe('', () => { policy_instance_minimum: 0, policy_instance_percentage: 0, percent_capacity_remaining: 0, - is_containerized: true, + is_container_group: true, summary_fields: { user_capabilities: { edit: false, diff --git a/awx/ui_next/src/screens/InstanceGroup/shared/ContainerGroupForm.test.jsx b/awx/ui_next/src/screens/InstanceGroup/shared/ContainerGroupForm.test.jsx index 62709df53e..3e48389195 100644 --- a/awx/ui_next/src/screens/InstanceGroup/shared/ContainerGroupForm.test.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/shared/ContainerGroupForm.test.jsx @@ -27,7 +27,7 @@ const instanceGroup = { controller: null, is_controller: false, is_isolated: false, - is_containerized: false, + is_container_group: false, credential: 3, policy_instance_percentage: 46, policy_instance_minimum: 12, diff --git a/awx/ui_next/src/screens/InstanceGroup/shared/InstanceGroupForm.test.jsx b/awx/ui_next/src/screens/InstanceGroup/shared/InstanceGroupForm.test.jsx index 233ce7f849..0dad4fe6d1 100644 --- a/awx/ui_next/src/screens/InstanceGroup/shared/InstanceGroupForm.test.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/shared/InstanceGroupForm.test.jsx @@ -27,7 +27,7 @@ const instanceGroup = { controller: null, is_controller: false, is_isolated: false, - is_containerized: false, + is_container_group: false, credential: null, policy_instance_percentage: 46, policy_instance_minimum: 12, From 373bb443aacb7a2c8b0abe44ea51417afcc1206b Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Wed, 13 Jan 2021 19:39:56 -0500 Subject: [PATCH 086/178] UnifiedJob#is_containerized -> UnifiedJob#is_container_group_task --- awx/main/isolated/manager.py | 2 +- awx/main/managers.py | 2 +- awx/main/models/unified_jobs.py | 2 +- awx/main/scheduler/task_manager.py | 2 +- awx/main/tasks.py | 6 +++--- .../functional/task_management/test_container_groups.py | 2 +- awx/main/tests/unit/test_capacity.py | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/awx/main/isolated/manager.py b/awx/main/isolated/manager.py index de4783e277..ffeb6af908 100644 --- a/awx/main/isolated/manager.py +++ b/awx/main/isolated/manager.py @@ -48,7 +48,7 @@ class IsolatedManager(object): self.pod_manager = pod_manager def build_inventory(self, hosts): - if self.instance and self.instance.is_containerized: + if self.instance and self.instance.is_container_group_task: inventory = {'all': {'hosts': {}}} fd, path = tempfile.mkstemp( prefix='.kubeconfig', dir=self.private_data_dir diff --git a/awx/main/managers.py b/awx/main/managers.py index ae93a552a0..1af57a9423 100644 --- a/awx/main/managers.py +++ b/awx/main/managers.py @@ -237,7 +237,7 @@ class InstanceGroupManager(models.Manager): elif t.status == 'running': # Subtract capacity from all groups that contain the instance if t.execution_node not in instance_ig_mapping: - if not t.is_containerized: + if not t.is_container_group_task: logger.warning('Detected %s running inside lost instance, ' 'may still be waiting for reaper.', t.log_format) if t.instance_group: diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 7970de1b22..cf22430f7b 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -1490,7 +1490,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique return bool(self.controller_node) @property - def is_containerized(self): + def is_container_group_task(self): return False def log_lifecycle(self, state, blocked_by=None): diff --git a/awx/main/scheduler/task_manager.py b/awx/main/scheduler/task_manager.py index 8731ca3109..50345e5bb7 100644 --- a/awx/main/scheduler/task_manager.py +++ b/awx/main/scheduler/task_manager.py @@ -594,7 +594,7 @@ class TaskManager(): ).exclude( execution_node__in=Instance.objects.values_list('hostname', flat=True) ): - if j.execution_node and not j.is_containerized: + if j.execution_node and not j.is_container_group_task: logger.error(f'{j.execution_node} is not a registered instance; reaping {j.log_format}') reap_job(j, 'failed') diff --git a/awx/main/tasks.py b/awx/main/tasks.py index efc4f4594f..5dd6b0fd5c 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1867,7 +1867,7 @@ class RunJob(BaseTask): ''' Return whether this task should use proot. ''' - if job.is_containerized: + if job.is_container_group_task: return False return getattr(settings, 'AWX_PROOT_ENABLED', False) @@ -1997,7 +1997,7 @@ class RunJob(BaseTask): os.path.join(private_data_dir, 'artifacts', 'fact_cache'), fact_modification_times, ) - if isolated_manager_instance and not job.is_containerized: + if isolated_manager_instance and not job.is_container_group_task: isolated_manager_instance.cleanup() try: @@ -2994,7 +2994,7 @@ class RunAdHocCommand(BaseTask): ''' Return whether this task should use proot. ''' - if ad_hoc_command.is_containerized: + if ad_hoc_command.is_container_group_task: return False return getattr(settings, 'AWX_PROOT_ENABLED', False) diff --git a/awx/main/tests/functional/task_management/test_container_groups.py b/awx/main/tests/functional/task_management/test_container_groups.py index c9028efacb..84dcaf12d7 100644 --- a/awx/main/tests/functional/task_management/test_container_groups.py +++ b/awx/main/tests/functional/task_management/test_container_groups.py @@ -29,7 +29,7 @@ def containerized_job(default_instance_group, kube_credential, job_template_fact @pytest.mark.django_db def test_containerized_job(containerized_job): - assert containerized_job.is_containerized + assert containerized_job.is_container_group_task assert containerized_job.instance_group.is_container_group assert containerized_job.instance_group.credential.kubernetes diff --git a/awx/main/tests/unit/test_capacity.py b/awx/main/tests/unit/test_capacity.py index 16fe81053c..1da05ec1f3 100644 --- a/awx/main/tests/unit/test_capacity.py +++ b/awx/main/tests/unit/test_capacity.py @@ -11,7 +11,7 @@ class FakeObject(object): class Job(FakeObject): task_impact = 43 - is_containerized = False + is_container_group_task = False def log_format(self): return 'job 382 (fake)' From 1d9f01a201818b065398f4ed228676de9dbfde3c Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Wed, 13 Jan 2021 19:40:35 -0500 Subject: [PATCH 087/178] Deleted unused build_params_process_isolation method --- awx/main/tasks.py | 40 -------------------- awx/main/tests/unit/test_tasks.py | 61 ------------------------------- 2 files changed, 101 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 5dd6b0fd5c..256d477323 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1005,46 +1005,6 @@ class BaseTask(object): Build ansible yaml file filled with extra vars to be passed via -e@file.yml ''' - def build_params_process_isolation(self, instance, private_data_dir, cwd): - ''' - Build ansible runner .run() parameters for process isolation. - ''' - process_isolation_params = dict() - if self.should_use_proot(instance): - local_paths = [private_data_dir] - if cwd != private_data_dir and Path(private_data_dir) not in Path(cwd).parents: - local_paths.append(cwd) - show_paths = self.proot_show_paths + local_paths + \ - settings.AWX_PROOT_SHOW_PATHS - - pi_path = settings.AWX_PROOT_BASE_PATH - if not self.instance.is_isolated() and not self.instance.is_containerized: - pi_path = tempfile.mkdtemp( - prefix='ansible_runner_pi_', - dir=settings.AWX_PROOT_BASE_PATH - ) - os.chmod(pi_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) - self.cleanup_paths.append(pi_path) - - process_isolation_params = { - 'process_isolation': True, - 'process_isolation_path': pi_path, - 'process_isolation_show_paths': show_paths, - 'process_isolation_hide_paths': [ - settings.AWX_PROOT_BASE_PATH, - '/etc/tower', - '/etc/ssh', - '/var/lib/awx', - '/var/log', - settings.PROJECTS_ROOT, - settings.JOBOUTPUT_ROOT, - ] + getattr(settings, 'AWX_PROOT_HIDE_PATHS', None) or [], - 'process_isolation_ro_paths': [settings.ANSIBLE_VENV_PATH, settings.AWX_VENV_PATH], - } - if getattr(instance, 'ansible_virtualenv_path', settings.ANSIBLE_VENV_PATH) != settings.ANSIBLE_VENV_PATH: - process_isolation_params['process_isolation_ro_paths'].append(instance.ansible_virtualenv_path) - return process_isolation_params - def build_params_resource_profiling(self, instance, private_data_dir): resource_profiling_params = {} if self.should_use_resource_profiling(instance): diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 8586ad11c7..3acdd7ead9 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -547,44 +547,6 @@ class TestGenericRun(): job_cwd='/foobar', job_env={'switch': 'blade', 'foot': 'ball', 'secret_key': 'redacted_value'}) - def test_uses_process_isolation(self, settings): - job = Job(project=Project(), inventory=Inventory()) - task = tasks.RunJob() - task.should_use_proot = lambda instance: True - task.instance = job - - private_data_dir = '/foo' - cwd = '/bar' - - settings.AWX_PROOT_HIDE_PATHS = ['/AWX_PROOT_HIDE_PATHS1', '/AWX_PROOT_HIDE_PATHS2'] - settings.ANSIBLE_VENV_PATH = '/ANSIBLE_VENV_PATH' - settings.AWX_VENV_PATH = '/AWX_VENV_PATH' - - process_isolation_params = task.build_params_process_isolation(job, private_data_dir, cwd) - assert True is process_isolation_params['process_isolation'] - assert process_isolation_params['process_isolation_path'].startswith(settings.AWX_PROOT_BASE_PATH), \ - "Directory where a temp directory will be created for the remapping to take place" - assert private_data_dir in process_isolation_params['process_isolation_show_paths'], \ - "The per-job private data dir should be in the list of directories the user can see." - assert cwd in process_isolation_params['process_isolation_show_paths'], \ - "The current working directory should be in the list of directories the user can see." - - for p in [settings.AWX_PROOT_BASE_PATH, - '/etc/tower', - '/etc/ssh', - '/var/lib/awx', - '/var/log', - settings.PROJECTS_ROOT, - settings.JOBOUTPUT_ROOT, - '/AWX_PROOT_HIDE_PATHS1', - '/AWX_PROOT_HIDE_PATHS2']: - assert p in process_isolation_params['process_isolation_hide_paths'] - assert 9 == len(process_isolation_params['process_isolation_hide_paths']) - assert '/ANSIBLE_VENV_PATH' in process_isolation_params['process_isolation_ro_paths'] - assert '/AWX_VENV_PATH' in process_isolation_params['process_isolation_ro_paths'] - assert 2 == len(process_isolation_params['process_isolation_ro_paths']) - - @mock.patch('os.makedirs') def test_build_params_resource_profiling(self, os_makedirs): job = Job(project=Project(), inventory=Inventory()) @@ -1962,29 +1924,6 @@ class TestProjectUpdateCredentials(TestJobExecution): ] } - def test_process_isolation_exposes_projects_root(self, private_data_dir, project_update): - task = tasks.RunProjectUpdate() - task.revision_path = 'foobar' - task.instance = project_update - ssh = CredentialType.defaults['ssh']() - project_update.scm_type = 'git' - project_update.credential = Credential( - pk=1, - credential_type=ssh, - ) - process_isolation = task.build_params_process_isolation(job, private_data_dir, 'cwd') - - assert process_isolation['process_isolation'] is True - assert settings.PROJECTS_ROOT in process_isolation['process_isolation_show_paths'] - - task._write_extra_vars_file = mock.Mock() - - with mock.patch.object(Licenser, 'validate', lambda *args, **kw: {}): - task.build_extra_vars_file(project_update, private_data_dir) - - call_args, _ = task._write_extra_vars_file.call_args_list[0] - _, extra_vars = call_args - def test_username_and_password_auth(self, project_update, scm_type): task = tasks.RunProjectUpdate() ssh = CredentialType.defaults['ssh']() From d37cb64aaf0197ede2b336de23e5ff679a5a35c6 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Wed, 13 Jan 2021 19:46:48 -0500 Subject: [PATCH 088/178] Delete some old container group v1 code --- awx/main/isolated/manager.py | 26 ++++---------------- awx/main/tasks.py | 47 ------------------------------------ 2 files changed, 5 insertions(+), 68 deletions(-) diff --git a/awx/main/isolated/manager.py b/awx/main/isolated/manager.py index ffeb6af908..61a55ff7a9 100644 --- a/awx/main/isolated/manager.py +++ b/awx/main/isolated/manager.py @@ -32,7 +32,7 @@ def set_pythonpath(venv_libdir, env): class IsolatedManager(object): - def __init__(self, event_handler, canceled_callback=None, check_callback=None, pod_manager=None): + def __init__(self, event_handler, canceled_callback=None, check_callback=None): """ :param event_handler: a callable used to persist event data from isolated nodes :param canceled_callback: a callable - which returns `True` or `False` @@ -45,28 +45,12 @@ class IsolatedManager(object): self.started_at = None self.captured_command_artifact = False self.instance = None - self.pod_manager = pod_manager def build_inventory(self, hosts): - if self.instance and self.instance.is_container_group_task: - inventory = {'all': {'hosts': {}}} - fd, path = tempfile.mkstemp( - prefix='.kubeconfig', dir=self.private_data_dir - ) - with open(path, 'wb') as temp: - temp.write(yaml.dump(self.pod_manager.kube_config).encode()) - temp.flush() - os.chmod(temp.name, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) - for host in hosts: - inventory['all']['hosts'][host] = { - "ansible_connection": "kubectl", - "ansible_kubectl_config": path, - } - else: - inventory = '\n'.join([ - '{} ansible_ssh_user={}'.format(host, settings.AWX_ISOLATED_USERNAME) - for host in hosts - ]) + inventory = '\n'.join([ + '{} ansible_ssh_user={}'.format(host, settings.AWX_ISOLATED_USERNAME) + for host in hosts + ]) return inventory diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 256d477323..2da516aacd 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1180,12 +1180,6 @@ class BaseTask(object): if os.path.isdir(job_profiling_dir): shutil.copytree(job_profiling_dir, os.path.join(awx_profiling_dir, str(instance.pk))) - if instance.is_containerized: - from awx.main.scheduler.kubernetes import PodManager # prevent circular import - pm = PodManager(instance) - logger.debug(f"Deleting pod {pm.pod_name}") - pm.delete() - def event_handler(self, event_data): # @@ -1325,16 +1319,6 @@ class BaseTask(object): Run the job/task and capture its output. ''' self.instance = self.model.objects.get(pk=pk) - containerized = self.instance.is_containerized - pod_manager = None - if containerized: - # Here we are trying to launch a pod before transitioning the job into a running - # state. For some scenarios (like waiting for resources to become available) we do this - # rather than marking the job as error or failed. This is not always desirable. Cases - # such as invalid authentication should surface as an error. - pod_manager = self.deploy_container_group_pod(self.instance) - if not pod_manager: - return # self.instance because of the update_model pattern and when it's used in callback handlers self.instance = self.update_model(pk, status='running', @@ -1517,37 +1501,6 @@ class BaseTask(object): raise AwxTaskError.TaskError(self.instance, rc) - def deploy_container_group_pod(self, task): - from awx.main.scheduler.kubernetes import PodManager # Avoid circular import - pod_manager = PodManager(self.instance) - try: - log_name = task.log_format - logger.debug(f"Launching pod for {log_name}.") - pod_manager.deploy() - except (ApiException, Exception) as exc: - if isinstance(exc, ApiException) and exc.status == 403: - try: - if 'exceeded quota' in json.loads(exc.body)['message']: - # If the k8s cluster does not have capacity, we move the - # job back into pending and wait until the next run of - # the task manager. This does not exactly play well with - # our current instance group precendence logic, since it - # will just sit here forever if kubernetes returns this - # error. - logger.warn(exc.body) - logger.warn(f"Could not launch pod for {log_name}. Exceeded quota.") - self.update_model(task.pk, status='pending') - return - except Exception: - logger.exception(f"Unable to handle response from Kubernetes API for {log_name}.") - - logger.exception(f"Error when launching pod for {log_name}") - self.update_model(task.pk, status='error', result_traceback=traceback.format_exc()) - return - - self.update_model(task.pk, execution_node=pod_manager.pod_name) - return pod_manager - From 9df29e8fc41824dd78839909d50bc25ba24dfa0e Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Wed, 13 Jan 2021 20:02:09 -0500 Subject: [PATCH 089/178] Use official awx-ee by default --- awx/settings/defaults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 7812b3af31..abf9d9014f 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -60,7 +60,7 @@ DATABASES = { } # TODO: remove this setting in favor of a default execution environment -AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE = 'quay.io/shanemcd/ee' +AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE = 'quay.io/ansible/awx-ee' AWX_CONTAINER_GROUP_K8S_API_TIMEOUT = 10 AWX_CONTAINER_GROUP_POD_LAUNCH_RETRIES = 100 From 70f7a082bba06ab0d2229eeb04741284264e66a5 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Wed, 13 Jan 2021 20:02:45 -0500 Subject: [PATCH 090/178] Minimally functional container group v2 w/ receptor --- awx/main/tasks.py | 147 ++++++++++++++++++++++++----- awx/main/utils/common.py | 21 ++++- tools/docker-compose/receptor.conf | 2 +- 3 files changed, 146 insertions(+), 24 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 2da516aacd..ee630dcf2e 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -78,7 +78,8 @@ from awx.main.dispatch import get_local_queuename, reaper from awx.main.utils import (update_scm_url, ignore_inventory_computed_fields, ignore_inventory_group_removal, extract_ansible_vars, schedule_task_manager, - get_awx_version) + get_awx_version, + deepmerge) from awx.main.utils.ansible import read_ansible_config from awx.main.utils.external_logging import reconfigure_rsyslog from awx.main.utils.safe_yaml import safe_dump, sanitize_jinja @@ -1379,7 +1380,6 @@ class BaseTask(object): args = self.build_args(self.instance, private_data_dir, passwords) resource_profiling_params = self.build_params_resource_profiling(self.instance, private_data_dir) - execution_environment_params = self.build_execution_environment_params(self.instance) env = self.build_env(self.instance, private_data_dir, isolated, private_data_files=private_data_files) self.safe_env = build_safe_env(env) @@ -1413,12 +1413,6 @@ class BaseTask(object): }, } - if containerized: - # We don't want HOME passed through to container groups. - # TODO: remove this conditional after everything is containerized - params['envvars'].pop('HOME', None) - - if isinstance(self.instance, AdHocCommand): params['module'] = self.build_module_name(self.instance) params['module_args'] = self.build_module_args(self.instance) @@ -1439,17 +1433,8 @@ class BaseTask(object): self.dispatcher = CallbackQueueDispatcher() - if not isinstance(self.instance, ProjectUpdate): - work_type='worker' - # TODO: container group jobs will not work with container isolation settings - # but both will run with same settings when worker_in and worker_out are added - params['settings'].update(execution_environment_params) - else: - work_type='worker' - params['settings'].update(execution_environment_params) - self.instance.log_lifecycle("running_playbook") - receptor_job = AWXReceptorJob(self, work_type, params) + receptor_job = AWXReceptorJob(self, params) res = receptor_job.run() status = res.status @@ -3045,11 +3030,14 @@ def deep_copy_model_obj( class AWXReceptorJob: - def __init__(self, task, work_type, runner_params): + def __init__(self, task, runner_params): self.task = task - self.work_type = work_type self.runner_params = runner_params + if not self.task.instance.is_container_group_task: + execution_environment_params = self.task.build_execution_environment_params(self.task.instance) + self.runner_params['settings'].update(execution_environment_params) + def run(self): # Create a socketpair. Where the left side will be used for writing our payload # (private data dir, kwargs). The right side will be passed to Receptor for @@ -3062,7 +3050,8 @@ class AWXReceptorJob: # in the right side of our socketpair for reading. receptor_ctl = ReceptorControl('/var/run/receptor/receptor.sock') result = receptor_ctl.submit_work(worktype=self.work_type, - payload=sockout.makefile('rb')) + payload=sockout.makefile('rb'), + params=self.receptor_params) unit_id = result['unitid'] sockin.close() @@ -3094,7 +3083,6 @@ class AWXReceptorJob: receptor_work_list = receptor_ctl.simple_command("work list") raise RuntimeError(receptor_work_list[unit_id]['Detail']) - receptor_ctl.simple_command(f"work release {unit_id}") return res @@ -3117,6 +3105,31 @@ class AWXReceptorJob: status_handler=self.task.status_handler, **self.runner_params) + @property + def receptor_params(self): + receptor_params = {} + if self.task.instance.is_container_group_task: + spec_yaml = yaml.dump(self.pod_definition, explicit_start=True) + kubeconfig_yaml = yaml.dump(self.kube_config, explicit_start=True) + + receptor_params = { + "secret_kube_pod": spec_yaml, + "secret_kube_config": kubeconfig_yaml + } + + return receptor_params + + + + @property + def work_type(self): + if self.task.instance.is_container_group_task: + work_type = 'ocp' + else: + work_type = 'local' + + return work_type + def cancel_watcher(self, processor_future): while True: if processor_future.done(): @@ -3126,3 +3139,93 @@ class AWXReceptorJob: result = namedtuple('result', ['status', 'rc']) return result('canceled', 1) time.sleep(1) + + @property + def pod_definition(self): + default_pod_spec = { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "namespace": settings.AWX_CONTAINER_GROUP_DEFAULT_NAMESPACE + }, + "spec": { + "containers": [{ + "image": settings.AWX_CONTAINER_GROUP_DEFAULT_IMAGE, + "name": 'worker', + "args": ['ansible-runner', 'worker'] + }] + } + } + + pod_spec_override = {} + if self.task and self.task.instance.instance_group.pod_spec_override: + pod_spec_override = parse_yaml_or_json( + self.task.instance_group.pod_spec_override) + pod_spec = {**default_pod_spec, **pod_spec_override} + + if self.task: + pod_spec['metadata'] = deepmerge( + pod_spec.get('metadata', {}), + dict(name=self.pod_name, + labels={ + 'ansible-awx': settings.INSTALL_UUID, + 'ansible-awx-job-id': str(self.task.instance.id) + })) + + return pod_spec + + @property + def pod_name(self): + return f"awx-job-{self.task.instance.id}" + + @property + def credential(self): + return self.task.instance.instance_group.credential + + @property + def namespace(self): + return self.pod_definition['metadata']['namespace'] + + @property + def kube_config(self): + host_input = self.credential.get_input('host') + config = { + "apiVersion": "v1", + "kind": "Config", + "preferences": {}, + "clusters": [ + { + "name": host_input, + "cluster": { + "server": host_input + } + } + ], + "users": [ + { + "name": host_input, + "user": { + "token": self.credential.get_input('bearer_token') + } + } + ], + "contexts": [ + { + "name": host_input, + "context": { + "cluster": host_input, + "user": host_input, + "namespace": self.namespace + } + } + ], + "current-context": host_input + } + + if self.credential.get_input('verify_ssl') and 'ssl_ca_cert' in self.credential.inputs: + config["clusters"][0]["cluster"]["certificate-authority-data"] = b64encode( + self.credential.get_input('ssl_ca_cert').encode() # encode to bytes + ).decode() # decode the base64 data into a str + else: + config["clusters"][0]["cluster"]["insecure-skip-tls-verify"] = True + return config diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index 283a028f3f..ad90d5e4ec 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -55,7 +55,8 @@ __all__ = [ 'model_instance_diff', 'parse_yaml_or_json', 'RequireDebugTrueOrTest', 'has_model_field_prefetched', 'set_environ', 'IllegalArgumentError', 'get_custom_venv_choices', 'get_external_account', 'task_manager_bulk_reschedule', - 'schedule_task_manager', 'classproperty', 'create_temporary_fifo', 'truncate_stdout' + 'schedule_task_manager', 'classproperty', 'create_temporary_fifo', 'truncate_stdout', + 'deepmerge' ] @@ -1079,3 +1080,21 @@ def truncate_stdout(stdout, size): set_count += 1 return stdout + u'\u001b[0m' * (set_count - reset_count) + + +def deepmerge(a, b): + """ + Merge dict structures and return the result. + + >>> a = {'first': {'all_rows': {'pass': 'dog', 'number': '1'}}} + >>> b = {'first': {'all_rows': {'fail': 'cat', 'number': '5'}}} + >>> import pprint; pprint.pprint(deepmerge(a, b)) + {'first': {'all_rows': {'fail': 'cat', 'number': '5', 'pass': 'dog'}}} + """ + if isinstance(a, dict) and isinstance(b, dict): + return dict([(k, deepmerge(a.get(k), b.get(k))) + for k in set(a.keys()).union(b.keys())]) + elif b is None: + return a + else: + return b diff --git a/tools/docker-compose/receptor.conf b/tools/docker-compose/receptor.conf index ba975b8820..4c498243e2 100644 --- a/tools/docker-compose/receptor.conf +++ b/tools/docker-compose/receptor.conf @@ -8,7 +8,7 @@ - local-only: - work-command: - worktype: worker + worktype: local command: ansible-runner params: worker From fe9b24cde2aa06fc0328411264fa6c8ddd586ccf Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Wed, 13 Jan 2021 20:20:16 -0500 Subject: [PATCH 091/178] flake8 --- awx/main/isolated/manager.py | 1 - awx/main/tasks.py | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/awx/main/isolated/manager.py b/awx/main/isolated/manager.py index 61a55ff7a9..abcd41c5c1 100644 --- a/awx/main/isolated/manager.py +++ b/awx/main/isolated/manager.py @@ -6,7 +6,6 @@ import stat import tempfile import time import logging -import yaml import datetime from django.conf import settings diff --git a/awx/main/tasks.py b/awx/main/tasks.py index ee630dcf2e..7f5dede047 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -26,6 +26,7 @@ import urllib.parse as urlparse import socket import threading import concurrent.futures +from base64 import b64encode # Django from django.conf import settings @@ -39,9 +40,6 @@ from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist from django_guid.middleware import GuidMiddleware -# Kubernetes -from kubernetes.client.rest import ApiException - # Django-CRUM from crum import impersonate @@ -79,7 +77,8 @@ from awx.main.utils import (update_scm_url, ignore_inventory_computed_fields, ignore_inventory_group_removal, extract_ansible_vars, schedule_task_manager, get_awx_version, - deepmerge) + deepmerge, + parse_yaml_or_json) from awx.main.utils.ansible import read_ansible_config from awx.main.utils.external_logging import reconfigure_rsyslog from awx.main.utils.safe_yaml import safe_dump, sanitize_jinja From 70755a395b17e13d7f102df69713a35b33b83d9a Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Thu, 14 Jan 2021 19:34:36 -0500 Subject: [PATCH 092/178] Make receptorctl easier to use in dev env --- tools/ansible/roles/dockerfile/templates/Dockerfile.j2 | 1 + .../ansible/roles/sources/templates/docker-compose.yml.j2 | 1 + 2 files changed, 2 insertions(+) diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index 0d48f2ea56..bf04f8fd00 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -270,6 +270,7 @@ ENV HOME="/var/lib/awx" ENV PATH="/usr/pgsql-10/bin:${PATH}" {% if build_dev|bool %} +ENV PATH="/var/lib/awx/venv/awx/bin/:${PATH}" EXPOSE 8043 8013 8080 22 diff --git a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 index db81b4ba39..00f136ef49 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 @@ -13,6 +13,7 @@ services: SDB_HOST: 0.0.0.0 SDB_PORT: 7899 AWX_GROUP_QUEUES: tower + RECEPTORCTL_SOCKET: /var/run/receptor/receptor.sock ports: - "2222:2222" - "8888:8888" From 341e1e34e336f85042aebe04a0ff45e830922d04 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Thu, 14 Jan 2021 19:35:28 -0500 Subject: [PATCH 093/178] Dont zip/unzip private data dir for local jobs --- awx/main/tasks.py | 9 ++++++++- tools/docker-compose/receptor.conf | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 7f5dede047..f660cf1813 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -3088,6 +3088,9 @@ class AWXReceptorJob: # Spawned in a thread so Receptor can start reading before we finish writing, we # write our payload to the left side of our socketpair. def transmit(self, _socket): + if self.work_type == 'local': + self.runner_params['only_transmit_kwargs'] = True + ansible_runner.interface.run(streamer='transmit', _output=_socket.makefile('wb'), **self.runner_params) @@ -3106,7 +3109,6 @@ class AWXReceptorJob: @property def receptor_params(self): - receptor_params = {} if self.task.instance.is_container_group_task: spec_yaml = yaml.dump(self.pod_definition, explicit_start=True) kubeconfig_yaml = yaml.dump(self.kube_config, explicit_start=True) @@ -3115,6 +3117,11 @@ class AWXReceptorJob: "secret_kube_pod": spec_yaml, "secret_kube_config": kubeconfig_yaml } + else: + private_data_dir = self.runner_params['private_data_dir'] + receptor_params = { + "params": f"--private-data-dir={private_data_dir}" + } return receptor_params diff --git a/tools/docker-compose/receptor.conf b/tools/docker-compose/receptor.conf index 4c498243e2..f86d50f523 100644 --- a/tools/docker-compose/receptor.conf +++ b/tools/docker-compose/receptor.conf @@ -11,6 +11,7 @@ worktype: local command: ansible-runner params: worker + allowruntimeparams: true - work-kubernetes: worktype: ocp From c6be92cdf63e1b8e5cb2986770811394e38730a5 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Sun, 17 Jan 2021 13:20:39 -0500 Subject: [PATCH 094/178] Create awx group in container --- tools/ansible/roles/dockerfile/templates/Dockerfile.j2 | 1 + tools/docker-compose/entrypoint.sh | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index bf04f8fd00..8c7e685736 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -234,6 +234,7 @@ RUN for dir in \ for file in \ /etc/subuid \ /etc/subgid \ + /etc/group \ /etc/passwd \ /var/lib/awx/rsyslog/rsyslog.conf ; \ do touch $file ; chmod g+rw $file ; chgrp root $file ; done diff --git a/tools/docker-compose/entrypoint.sh b/tools/docker-compose/entrypoint.sh index c155f022e4..006435000a 100755 --- a/tools/docker-compose/entrypoint.sh +++ b/tools/docker-compose/entrypoint.sh @@ -7,6 +7,10 @@ root:x:0:0:root:/root:/bin/bash awx:x:`id -u`:`id -g`:,,,:/var/lib/awx:/bin/bash EOF +cat <> /etc/group +awx:x:`id -u`:awx +EOF + cat < /etc/subuid awx:100000:50001 EOF From e63383bde6f4f34307a256ea4222d61100ee6a53 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Sun, 17 Jan 2021 13:31:39 -0500 Subject: [PATCH 095/178] Add PATH to blocked inventory source vars This used to be skiped because PATH was already present in the env we constructed for runner. --- awx/settings/defaults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index abf9d9014f..9d21ac68bb 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -685,7 +685,7 @@ AD_HOC_COMMANDS = [ 'win_user', ] -INV_ENV_VARIABLE_BLOCKED = ("HOME", "USER", "_", "TERM") +INV_ENV_VARIABLE_BLOCKED = ("HOME", "USER", "_", "TERM", "PATH") # ---------------- # -- Amazon EC2 -- From 3f76499c565b10bbf698213a496fe915c2efe2a9 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Mon, 18 Jan 2021 10:44:26 -0500 Subject: [PATCH 096/178] Use the fully qualified inventory plugin name only for foreman --- awx/main/models/inventory.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 28f4b09948..4e671af7df 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1373,6 +1373,7 @@ class PluginFileInjector(object): collection = None collection_migration = '2.9' # Starting with this version, we use collections + # TODO: delete this method and update unit tests @classmethod def get_proper_name(cls): if cls.plugin_name is None: @@ -1397,13 +1398,12 @@ class PluginFileInjector(object): def inventory_as_dict(self, inventory_update, private_data_dir): source_vars = dict(inventory_update.source_vars_dict) # make a copy - proper_name = self.get_proper_name() ''' None conveys that we should use the user-provided plugin. Note that a plugin value of '' should still be overridden. ''' - if proper_name is not None: - source_vars['plugin'] = proper_name + if self.plugin_name is not None: + source_vars['plugin'] = self.plugin_name return source_vars def build_env(self, inventory_update, env, private_data_dir, private_data_files): @@ -1573,6 +1573,12 @@ class satellite6(PluginFileInjector): ret['FOREMAN_PASSWORD'] = credential.get_input('password', default='') return ret + def inventory_as_dict(self, inventory_update, private_data_dir): + ret = super(satellite6, self).inventory_as_dict(inventory_update, private_data_dir) + # this inventory plugin requires the fully qualified inventory plugin name + ret['plugin'] = f'{cls.namespace}.{cls.collection}.{cls.plugin_name}' + return ret + class tower(PluginFileInjector): plugin_name = 'tower' From b05b6b2e0368d2ae87d7e53c897dc32b1e66cf3a Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Tue, 19 Jan 2021 14:48:55 -0500 Subject: [PATCH 097/178] Fix minor syntax error failing AdHocCommands --- awx/main/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index f660cf1813..0fb1f92732 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -3166,7 +3166,7 @@ class AWXReceptorJob: pod_spec_override = {} if self.task and self.task.instance.instance_group.pod_spec_override: pod_spec_override = parse_yaml_or_json( - self.task.instance_group.pod_spec_override) + self.task.instance.instance_group.pod_spec_override) pod_spec = {**default_pod_spec, **pod_spec_override} if self.task: From c29d476919417bdc2d4ba632eebbbb2ba66f36c5 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Thu, 21 Jan 2021 10:55:41 -0500 Subject: [PATCH 098/178] Fix obvious code error with foreman inventory --- awx/main/models/inventory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 4e671af7df..9000fe41c4 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1576,7 +1576,7 @@ class satellite6(PluginFileInjector): def inventory_as_dict(self, inventory_update, private_data_dir): ret = super(satellite6, self).inventory_as_dict(inventory_update, private_data_dir) # this inventory plugin requires the fully qualified inventory plugin name - ret['plugin'] = f'{cls.namespace}.{cls.collection}.{cls.plugin_name}' + ret['plugin'] = f'{self.namespace}.{self.collection}.{self.plugin_name}' return ret From f850f8d3e0ebc97e3efed7d5001395bae85c997b Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Thu, 21 Jan 2021 12:25:34 -0500 Subject: [PATCH 099/178] introduce a new global flag for denoating K8S-based deployments - In K8S-based installs, only container groups are intended to be used for playbook execution (JTs, adhoc, inventory updates), so in this scenario, other job types have a task impact of zero. - In K8S-based installs, traditional instances have *zero* capacity (because they're only members of the control plane where services - http/s, local control plane execution - run) - This commit also includes some changes that allow for the task manager to launch tasks with task_impact=0 on instances that have capacity=0 (previously, an instance with zero capacity would never be selected as the "execution node" This means that when IS_K8S=True, any Job Template associated with an Instance Group will never actually go from pending -> running (because there's no capacity - all playbooks must run through Container Groups). For an improved ux, our intention is to introduce logic into the operator install process such that the *default* group that's created at install time is a *Container Group* that's configured to point at the K8S cluster where awx itself is deployed. --- awx/main/models/ha.py | 7 +++++++ awx/main/models/jobs.py | 2 ++ awx/main/models/projects.py | 2 ++ awx/main/scheduler/task_manager.py | 9 ++++++--- awx/settings/defaults.py | 7 +++++++ 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/awx/main/models/ha.py b/awx/main/models/ha.py index 4f96bdc5b1..6dd72861cc 100644 --- a/awx/main/models/ha.py +++ b/awx/main/models/ha.py @@ -147,6 +147,13 @@ class Instance(HasPolicyEditsMixin, BaseModel): return self.rampart_groups.filter(controller__isnull=False).exists() def refresh_capacity(self): + if settings.IS_K8S: + self.capacity = self.cpu = self.memory = self.cpu_capacity = self.mem_capacity = 0 # noqa + self.version = awx_application_version + self.save(update_fields=['capacity', 'version', 'modified', 'cpu', + 'memory', 'cpu_capacity', 'mem_capacity']) + return + cpu = get_cpu_capacity() mem = get_mem_capacity() if self.enabled: diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 81e17cdebf..70cdfa363a 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -1286,6 +1286,8 @@ class SystemJob(UnifiedJob, SystemJobOptions, JobNotificationMixin): @property def task_impact(self): + if settings.IS_K8S: + return 0 return 5 @property diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index ec14a2ef76..fb948916d0 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -563,6 +563,8 @@ class ProjectUpdate(UnifiedJob, ProjectOptions, JobNotificationMixin, TaskManage @property def task_impact(self): + if settings.IS_K8S: + return 0 return 0 if self.job_type == 'run' else 1 @property diff --git a/awx/main/scheduler/task_manager.py b/awx/main/scheduler/task_manager.py index 50345e5bb7..8d8df5eee2 100644 --- a/awx/main/scheduler/task_manager.py +++ b/awx/main/scheduler/task_manager.py @@ -70,7 +70,7 @@ class TaskManager(): ''' Init AFTER we know this instance of the task manager will run because the lock is acquired. ''' - instances = Instance.objects.filter(~Q(hostname=None), capacity__gt=0, enabled=True) + instances = Instance.objects.filter(~Q(hostname=None), enabled=True) self.real_instances = {i.hostname: i for i in instances} instances_partial = [SimpleNamespace(obj=instance, @@ -86,7 +86,7 @@ class TaskManager(): capacity_total=rampart_group.capacity, consumed_capacity=0, instances=[]) - for instance in rampart_group.instances.filter(capacity__gt=0, enabled=True).order_by('hostname'): + for instance in rampart_group.instances.filter(enabled=True).order_by('hostname'): if instance.hostname in instances_by_hostname: self.graph[rampart_group.name]['instances'].append(instances_by_hostname[instance.hostname]) @@ -528,7 +528,10 @@ class TaskManager(): break remaining_capacity = self.get_remaining_capacity(rampart_group.name) - if not rampart_group.is_container_group and self.get_remaining_capacity(rampart_group.name) <= 0: + if ( + task.task_impact > 0 and # project updates have a cost of zero + not rampart_group.is_container_group and + self.get_remaining_capacity(rampart_group.name) <= 0): logger.debug("Skipping group {}, remaining_capacity {} <= 0".format( rampart_group.name, remaining_capacity)) continue diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 9d21ac68bb..845bbe74d8 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -59,6 +59,13 @@ DATABASES = { } } +# Whether or not the deployment is a K8S-based deployment +# In K8S-based deployments, instances have zero capacity - all playbook +# automation is intended to flow through defined Container Groups that +# interface with some (or some set of) K8S api (which may or may not include +# the K8S cluster where awx itself is running) +IS_K8S = False + # TODO: remove this setting in favor of a default execution environment AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE = 'quay.io/ansible/awx-ee' From a435843f23260cefae59664603759c9791f21134 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Mon, 25 Jan 2021 15:15:14 -0500 Subject: [PATCH 100/178] Exception handling to always release work units --- awx/main/tasks.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 0fb1f92732..686bcf7526 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -3038,6 +3038,17 @@ class AWXReceptorJob: self.runner_params['settings'].update(execution_environment_params) def run(self): + # We establish a connection to the Receptor socket + receptor_ctl = ReceptorControl('/var/run/receptor/receptor.sock') + + try: + return self._run_internal(receptor_ctl) + finally: + # Make sure to always release the work unit if we established it + if self.unit_id is not None: + receptor_ctl.simple_command(f"work release {self.unit_id}") + + def _run_internal(self, receptor_ctl): # Create a socketpair. Where the left side will be used for writing our payload # (private data dir, kwargs). The right side will be passed to Receptor for # reading. @@ -3045,18 +3056,17 @@ class AWXReceptorJob: threading.Thread(target=self.transmit, args=[sockin]).start() - # We establish a connection to the Receptor socket and submit our work, passing + # submit our work, passing # in the right side of our socketpair for reading. - receptor_ctl = ReceptorControl('/var/run/receptor/receptor.sock') result = receptor_ctl.submit_work(worktype=self.work_type, payload=sockout.makefile('rb'), params=self.receptor_params) - unit_id = result['unitid'] + self.unit_id = result['unitid'] sockin.close() sockout.close() - resultsock, resultfile = receptor_ctl.get_work_results(unit_id, + resultsock, resultfile = receptor_ctl.get_work_results(self.unit_id, return_socket=True, return_sockfile=True) # Both "processor" and "cancel_watcher" are spawned in separate threads. @@ -3074,15 +3084,14 @@ class AWXReceptorJob: res = list(first_future.done)[0].result() if res.status == 'canceled': - receptor_ctl.simple_command(f"work cancel {unit_id}") + receptor_ctl.simple_command(f"work cancel {self.unit_id}") resultsock.shutdown(socket.SHUT_RDWR) resultfile.close() elif res.status == 'error': # TODO: There should be a more efficient way of getting this information receptor_work_list = receptor_ctl.simple_command("work list") - raise RuntimeError(receptor_work_list[unit_id]['Detail']) + raise RuntimeError(receptor_work_list[self.unit_id]['Detail']) - receptor_ctl.simple_command(f"work release {unit_id}") return res # Spawned in a thread so Receptor can start reading before we finish writing, we From fd0c4ec869425bffab02274b4aebd443788fbd12 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Tue, 26 Jan 2021 13:47:44 -0500 Subject: [PATCH 101/178] Pin to latest version of PyYAML Fixes https://github.com/yaml/pyyaml/issues/478 --- requirements/requirements.in | 2 +- requirements/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/requirements.in b/requirements/requirements.in index 93b5b4f72e..f54094ae72 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -40,7 +40,7 @@ pygerduty pyparsing python3-saml python-ldap>=3.3.1 # https://github.com/python-ldap/python-ldap/issues/270 -pyyaml>=5.3.1 # minimum version to pull in new pyyaml for CVE-2017-18342 +pyyaml>=5.4.1 # minimum to fix https://github.com/yaml/pyyaml/issues/478 schedule==0.6.0 social-auth-core==3.3.1 # see UPGRADE BLOCKERs social-auth-app-django==3.1.0 # see UPGRADE BLOCKERs diff --git a/requirements/requirements.txt b/requirements/requirements.txt index a6901fd3ef..6256afd061 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -100,7 +100,7 @@ python-string-utils==1.0.0 # via openshift python3-openid==3.1.0 # via social-auth-core python3-saml==1.9.0 # via -r /awx_devel/requirements/requirements.in pytz==2019.3 # via django, irc, tempora, twilio -pyyaml==5.3.1 # via -r /awx_devel/requirements/requirements.in, ansible-runner, djangorestframework-yaml, kubernetes +pyyaml==5.4.1 # via -r /awx_devel/requirements/requirements.in, ansible-runner, djangorestframework-yaml, kubernetes redis==3.4.1 # via -r /awx_devel/requirements/requirements.in, django-redis requests-oauthlib==1.3.0 # via kubernetes, msrest, social-auth-core requests==2.23.0 # via -r /awx_devel/requirements/requirements.in, adal, azure-keyvault, django-oauth-toolkit, kubernetes, msrest, requests-oauthlib, slackclient, social-auth-core, twilio From 8ab7745e3abea3476beda4a293f788b203ed96d7 Mon Sep 17 00:00:00 2001 From: Yago Marques Date: Tue, 26 Jan 2021 16:57:01 -0300 Subject: [PATCH 102/178] WIP Inclusion of the EE option in the payloads within the Organization and Projects. (#9145) * add ee option on factories for organizations * add new lines * remove inventory from the options * remove line * remove line from projects * fix the tuple * fix lint problems --- awxkit/awxkit/api/pages/organizations.py | 12 +++++++++++- awxkit/awxkit/api/pages/projects.py | 10 ++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/awxkit/awxkit/api/pages/organizations.py b/awxkit/awxkit/api/pages/organizations.py index e1d13a0013..fe34eb07e7 100644 --- a/awxkit/awxkit/api/pages/organizations.py +++ b/awxkit/awxkit/api/pages/organizations.py @@ -39,10 +39,20 @@ class Organization(HasCreate, HasInstanceGroups, HasNotifications, base.Base): "disassociate": True, }) - def payload(self, **kwargs): payload = PseudoNamespace(name=kwargs.get('name') or 'Organization - {}'.format(random_title()), description=kwargs.get('description') or random_title(10)) + + for fk_field in ('default_environment',): + rel_obj = kwargs.get(fk_field) + if rel_obj is None: + continue + elif isinstance(rel_obj, int): + payload.update(**{fk_field: int(rel_obj)}) + elif hasattr(rel_obj, 'id'): + payload.update(**{fk_field: rel_obj.id}) + else: + raise AttributeError(f'Related field {fk_field} must be either integer of pkid or object') return payload def create_payload(self, name='', description='', **kwargs): diff --git a/awxkit/awxkit/api/pages/projects.py b/awxkit/awxkit/api/pages/projects.py index e40191260c..bc46f5edb3 100644 --- a/awxkit/awxkit/api/pages/projects.py +++ b/awxkit/awxkit/api/pages/projects.py @@ -43,6 +43,16 @@ class Project(HasCopy, HasCreate, HasNotifications, UnifiedJobTemplate): 'allow_override') update_payload(payload, fields, kwargs) + for fk_field in ('execution_environment', 'default_environment'): + rel_obj = kwargs.get(fk_field) + if rel_obj is None: + continue + elif isinstance(rel_obj, int): + payload.update(**{fk_field: int(rel_obj)}) + elif hasattr(rel_obj, 'id'): + payload.update(**{fk_field: rel_obj.id}) + else: + raise AttributeError(f'Related field {fk_field} must be either integer of pkid or object') return payload def create_payload( From c7e0e30f932a4c3043f59e39b31db0f7cfa4c647 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Wed, 27 Jan 2021 11:55:32 -0500 Subject: [PATCH 103/178] Make sure project updates run in default EE (#9172) * Make sure project updates run in default EE * Remove project execution_environment field from collection --- awx/api/serializers.py | 2 +- awx/main/models/mixins.py | 14 +++++++++----- awx/main/models/projects.py | 8 ++++++++ awx_collection/plugins/modules/tower_project.py | 8 -------- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 3a28239b0d..eb13f150d3 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1394,7 +1394,7 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer): class Meta: model = Project - fields = ('*', 'organization', 'scm_update_on_launch', + fields = ('*', '-execution_environment', 'organization', 'scm_update_on_launch', 'scm_update_cache_timeout', 'allow_override', 'custom_virtualenv', 'default_environment') + \ ('last_update_failed', 'last_updated') # Backwards compatibility diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index 459eadabcf..549c93607d 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -455,12 +455,17 @@ class ExecutionEnvironmentMixin(models.Model): help_text=_('The container image to be used for execution.'), ) + def get_execution_environment_default(self): + from awx.main.models.execution_environments import ExecutionEnvironment + + if settings.DEFAULT_EXECUTION_ENVIRONMENT is not None: + return settings.DEFAULT_EXECUTION_ENVIRONMENT + return ExecutionEnvironment.objects.filter(organization=None, managed_by_tower=True).first() + def resolve_execution_environment(self): """ Return the execution environment that should be used when creating a new job. """ - from awx.main.models.execution_environments import ExecutionEnvironment - if self.execution_environment is not None: return self.execution_environment if getattr(self, 'project_id', None) and self.project.default_environment is not None: @@ -470,9 +475,8 @@ class ExecutionEnvironmentMixin(models.Model): if getattr(self, 'inventory', None) and self.inventory.organization is not None: if self.inventory.organization.default_environment is not None: return self.inventory.organization.default_environment - if settings.DEFAULT_EXECUTION_ENVIRONMENT is not None: - return settings.DEFAULT_EXECUTION_ENVIRONMENT - return ExecutionEnvironment.objects.filter(organization=None, managed_by_tower=True).first() + + return self.get_execution_environment_default() class CustomVirtualEnvMixin(models.Model): diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index fb948916d0..c9bf87f408 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -187,6 +187,14 @@ class ProjectOptions(models.Model): pass return cred + def resolve_execution_environment(self): + """ + Project updates, themselves, will use the default execution environment. + Jobs using the project can use the default_environment, but the project updates + are not flexible enough to allow customizing the image they use. + """ + return self.get_execution_environment_default() + def get_project_path(self, check_if_exists=True): local_path = os.path.basename(self.local_path) if local_path and not local_path.startswith('.'): diff --git a/awx_collection/plugins/modules/tower_project.py b/awx_collection/plugins/modules/tower_project.py index 1a8248c1fa..acbf3833b5 100644 --- a/awx_collection/plugins/modules/tower_project.py +++ b/awx_collection/plugins/modules/tower_project.py @@ -106,10 +106,6 @@ options: description: - Default Execution Environment to use for jobs relating to the project. type: str - execution_environment: - description: - - Execution Environment to use for project updates. - type: str organization: description: - Name of organization for project. @@ -248,7 +244,6 @@ def main(): timeout=dict(type='int', default=0, aliases=['job_timeout']), custom_virtualenv=dict(), default_environment=dict(), - execution_environment=dict(), organization=dict(), notification_templates_started=dict(type="list", elements='str'), notification_templates_success=dict(type="list", elements='str'), @@ -281,7 +276,6 @@ def main(): timeout = module.params.get('timeout') custom_virtualenv = module.params.get('custom_virtualenv') default_ee = module.params.get('default_environment') - ee = module.params.get('execution_environment') organization = module.params.get('organization') state = module.params.get('state') wait = module.params.get('wait') @@ -347,8 +341,6 @@ def main(): project_fields['credential'] = credential if default_ee is not None: project_fields['default_environment'] = module.resolve_name_to_id('execution_environments', default_ee) - if ee is not None: - project_fields['execution_environment'] = module.resolve_name_to_id('execution_environments', ee) if allow_override is not None: project_fields['allow_override'] = allow_override if scm_type == '': From 8f66793276f92159b92eada75e2406d676ac3d20 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Wed, 27 Jan 2021 16:10:56 -0500 Subject: [PATCH 104/178] Assure that unit_id is always defined (#9180) --- awx/main/tasks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 686bcf7526..db6b8644cd 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -3032,6 +3032,7 @@ class AWXReceptorJob: def __init__(self, task, runner_params): self.task = task self.runner_params = runner_params + self.unit_id = None if not self.task.instance.is_container_group_task: execution_environment_params = self.task.build_execution_environment_params(self.task.instance) From 86363e260e756313ce95af00d5f250b80f4cec8d Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Wed, 27 Jan 2021 18:49:06 -0500 Subject: [PATCH 105/178] Provide new default pod defintion in CG metadata (#9181) --- awx/api/metadata.py | 4 ++-- awx/main/tasks.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/awx/api/metadata.py b/awx/api/metadata.py index 0b60f9a1ef..dedeeba8fb 100644 --- a/awx/api/metadata.py +++ b/awx/api/metadata.py @@ -24,7 +24,7 @@ from rest_framework.request import clone_request from awx.api.fields import ChoiceNullField from awx.main.fields import JSONField, ImplicitRoleField from awx.main.models import NotificationTemplate -from awx.main.scheduler.kubernetes import PodManager +from awx.main.tasks import AWXReceptorJob class Metadata(metadata.SimpleMetadata): @@ -209,7 +209,7 @@ class Metadata(metadata.SimpleMetadata): continue if field == "pod_spec_override": - meta['default'] = PodManager().pod_definition + meta['default'] = AWXReceptorJob().pod_definition # Add type choices if available from the serializer. if field == 'type' and hasattr(serializer, 'get_type_choices'): diff --git a/awx/main/tasks.py b/awx/main/tasks.py index db6b8644cd..ef0bd7e6e5 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -3029,12 +3029,12 @@ def deep_copy_model_obj( class AWXReceptorJob: - def __init__(self, task, runner_params): + def __init__(self, task=None, runner_params=None): self.task = task self.runner_params = runner_params self.unit_id = None - if not self.task.instance.is_container_group_task: + if self.task and not self.task.instance.is_container_group_task: execution_environment_params = self.task.build_execution_environment_params(self.task.instance) self.runner_params['settings'].update(execution_environment_params) From 1a68df275c2878ef736e52201b4594e3dae70355 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Sun, 31 Jan 2021 12:03:03 -0500 Subject: [PATCH 106/178] Set correct SDB_NOTIFY_HOST in minikube env --- tools/ansible/roles/dockerfile/files/launch_awx.sh | 2 +- tools/ansible/roles/dockerfile/files/launch_awx_task.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/ansible/roles/dockerfile/files/launch_awx.sh b/tools/ansible/roles/dockerfile/files/launch_awx.sh index 839f7cf746..7b5e86f0bd 100644 --- a/tools/ansible/roles/dockerfile/files/launch_awx.sh +++ b/tools/ansible/roles/dockerfile/files/launch_awx.sh @@ -10,7 +10,7 @@ if [ -n "${AWX_KUBE_DEVEL}" ]; then make awx-link popd - export SDB_NOTIFY_HOST=$(ip route | head -n1 | awk '{print $3}') + export SDB_NOTIFY_HOST=$MY_POD_IP fi source /etc/tower/conf.d/environment.sh diff --git a/tools/ansible/roles/dockerfile/files/launch_awx_task.sh b/tools/ansible/roles/dockerfile/files/launch_awx_task.sh index 120cc9e3f8..8b9774a477 100644 --- a/tools/ansible/roles/dockerfile/files/launch_awx_task.sh +++ b/tools/ansible/roles/dockerfile/files/launch_awx_task.sh @@ -10,7 +10,7 @@ if [ -n "${AWX_KUBE_DEVEL}" ]; then make awx-link popd - export SDB_NOTIFY_HOST=$(ip route | head -n1 | awk '{print $3}') + export SDB_NOTIFY_HOST=$MY_POD_IP fi source /etc/tower/conf.d/environment.sh From 9f97efece8f99e4a497b160599f9d76311d56feb Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Sun, 31 Jan 2021 16:32:06 -0500 Subject: [PATCH 107/178] Stop and kill dispatcher and callback reciever as group --- .../roles/dockerfile/templates/supervisor_task.conf.j2 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/ansible/roles/dockerfile/templates/supervisor_task.conf.j2 b/tools/ansible/roles/dockerfile/templates/supervisor_task.conf.j2 index b9fe0be41a..994323e6a8 100644 --- a/tools/ansible/roles/dockerfile/templates/supervisor_task.conf.j2 +++ b/tools/ansible/roles/dockerfile/templates/supervisor_task.conf.j2 @@ -16,6 +16,8 @@ directory = /var/lib/awx autostart = true autorestart = true stopwaitsecs = 5 +stopasgroup=true +killasgroup=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr @@ -32,6 +34,8 @@ directory = /var/lib/awx autostart = true autorestart = true stopwaitsecs = 5 +stopasgroup=true +killasgroup=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr From 68f9c5137d10d1b58fb11eb642a889a8bdf7d118 Mon Sep 17 00:00:00 2001 From: nixocio Date: Tue, 2 Feb 2021 17:01:23 -0500 Subject: [PATCH 108/178] Mark string to translation Mark string to translation --- .../ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx index 7789facfbb..8e41c5a0f8 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx @@ -52,7 +52,7 @@ function ExecutionEnvironmentListItem({ ]} /> From 57b317d4404fffe1e4f2b3c0af1e937c78305cf8 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Wed, 3 Feb 2021 15:06:21 -0500 Subject: [PATCH 109/178] Get system jobs working under new deployment model (#9221) --- awx/main/models/ha.py | 3 +++ awx/main/tasks.py | 37 +++++++++++++++++++++++++++++++------ awx/settings/defaults.py | 2 +- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/awx/main/models/ha.py b/awx/main/models/ha.py index 6dd72861cc..94d4b8d462 100644 --- a/awx/main/models/ha.py +++ b/awx/main/models/ha.py @@ -255,6 +255,9 @@ class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin): @property def is_container_group(self): + if settings.IS_K8S: + return True + return bool(self.credential and self.credential.kubernetes) ''' diff --git a/awx/main/tasks.py b/awx/main/tasks.py index ef0bd7e6e5..0b915ab909 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -893,6 +893,9 @@ class BaseTask(object): return os.path.abspath(os.path.join(os.path.dirname(__file__), *args)) def build_execution_environment_params(self, instance): + if settings.IS_K8S: + return {} + if instance.execution_environment_id is None: from awx.main.signals import disable_activity_stream @@ -1423,6 +1426,9 @@ class BaseTask(object): # Disable Ansible fact cache. params['fact_cache_type'] = '' + if self.instance.is_container_group_task or settings.IS_K8S: + params['envvars'].pop('HOME', None) + ''' Delete parameters if the values are None or empty array ''' @@ -1433,8 +1439,16 @@ class BaseTask(object): self.dispatcher = CallbackQueueDispatcher() self.instance.log_lifecycle("running_playbook") - receptor_job = AWXReceptorJob(self, params) - res = receptor_job.run() + if isinstance(self.instance, SystemJob): + cwd = self.build_cwd(self.instance, private_data_dir) + res = ansible_runner.interface.run(project_dir=cwd, + event_handler=self.event_handler, + finished_callback=self.finished_callback, + status_handler=self.status_handler, + **params) + else: + receptor_job = AWXReceptorJob(self, params) + res = receptor_job.run() status = res.status rc = res.rc @@ -1769,6 +1783,9 @@ class RunJob(BaseTask): return getattr(settings, 'AWX_PROOT_ENABLED', False) def build_execution_environment_params(self, instance): + if settings.IS_K8S: + return {} + params = super(RunJob, self).build_execution_environment_params(instance) # If this has an insights agent and it is not already mounted then show it insights_dir = os.path.dirname(settings.INSIGHTS_SYSTEM_ID_FILE) @@ -2398,6 +2415,9 @@ class RunProjectUpdate(BaseTask): return getattr(settings, 'AWX_PROOT_ENABLED', False) def build_execution_environment_params(self, instance): + if settings.IS_K8S: + return {} + params = super(RunProjectUpdate, self).build_execution_environment_params(instance) project_path = instance.get_project_path(check_if_exists=False) cache_path = instance.get_cache_path() @@ -3098,7 +3118,7 @@ class AWXReceptorJob: # Spawned in a thread so Receptor can start reading before we finish writing, we # write our payload to the left side of our socketpair. def transmit(self, _socket): - if self.work_type == 'local': + if not settings.IS_K8S and self.work_type == 'local': self.runner_params['only_transmit_kwargs'] = True ansible_runner.interface.run(streamer='transmit', @@ -3121,12 +3141,14 @@ class AWXReceptorJob: def receptor_params(self): if self.task.instance.is_container_group_task: spec_yaml = yaml.dump(self.pod_definition, explicit_start=True) - kubeconfig_yaml = yaml.dump(self.kube_config, explicit_start=True) receptor_params = { "secret_kube_pod": spec_yaml, - "secret_kube_config": kubeconfig_yaml } + + if self.credential: + kubeconfig_yaml = yaml.dump(self.kube_config, explicit_start=True) + receptor_params["secret_kube_config"] = kubeconfig_yaml else: private_data_dir = self.runner_params['private_data_dir'] receptor_params = { @@ -3140,7 +3162,10 @@ class AWXReceptorJob: @property def work_type(self): if self.task.instance.is_container_group_task: - work_type = 'ocp' + if self.credential: + work_type = 'kubernetes-runtime-auth' + else: + work_type = 'kubernetes-incluster-auth' else: work_type = 'local' diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 845bbe74d8..6b2f78479a 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -67,7 +67,7 @@ DATABASES = { IS_K8S = False # TODO: remove this setting in favor of a default execution environment -AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE = 'quay.io/ansible/awx-ee' +AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE = 'quay.io/shanemcd/awx-ee' AWX_CONTAINER_GROUP_K8S_API_TIMEOUT = 10 AWX_CONTAINER_GROUP_POD_LAUNCH_RETRIES = 100 From 428f8addf8145a0ceec6bbe39938b0ecd45593d3 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Wed, 3 Feb 2021 17:06:14 -0500 Subject: [PATCH 110/178] Create default EE as a part of create_preload_data --- .../commands/create_preload_data.py | 114 ++++++++++-------- tools/docker-compose/bootstrap_development.sh | 5 - 2 files changed, 66 insertions(+), 53 deletions(-) diff --git a/awx/main/management/commands/create_preload_data.py b/awx/main/management/commands/create_preload_data.py index 9b1d131735..05ed18b96c 100644 --- a/awx/main/management/commands/create_preload_data.py +++ b/awx/main/management/commands/create_preload_data.py @@ -2,22 +2,22 @@ # All Rights Reserved from django.core.management.base import BaseCommand +from django.conf import settings from crum import impersonate -from awx.main.models import User, Organization, Project, Inventory, CredentialType, Credential, Host, JobTemplate +from awx.main.models import ( + User, Organization, Project, Inventory, CredentialType, + Credential, Host, JobTemplate, ExecutionEnvironment +) from awx.main.signals import disable_computed_fields class Command(BaseCommand): """Create preloaded data, intended for new installs """ - help = 'Creates a preload tower data iff there is none.' + help = 'Creates a preload tower data if there is none.' def handle(self, *args, **kwargs): - # Sanity check: Is there already an organization in the system? - if Organization.objects.count(): - print('An organization is already in the system, exiting.') - print('(changed: False)') - return + changed = False # Create a default organization as the first superuser found. try: @@ -26,44 +26,62 @@ class Command(BaseCommand): superuser = None with impersonate(superuser): with disable_computed_fields(): - o = Organization.objects.create(name='Default') - p = Project(name='Demo Project', - scm_type='git', - scm_url='https://github.com/ansible/ansible-tower-samples', - scm_update_on_launch=True, - scm_update_cache_timeout=0, - organization=o) - p.save(skip_update=True) - ssh_type = CredentialType.objects.filter(namespace='ssh').first() - c = Credential.objects.create(credential_type=ssh_type, - name='Demo Credential', - inputs={ - 'username': superuser.username - }, - created_by=superuser) - c.admin_role.members.add(superuser) - public_galaxy_credential = Credential( - name='Ansible Galaxy', - managed_by_tower=True, - credential_type=CredentialType.objects.get(kind='galaxy'), - inputs = { - 'url': 'https://galaxy.ansible.com/' - } - ) - public_galaxy_credential.save() - o.galaxy_credentials.add(public_galaxy_credential) - i = Inventory.objects.create(name='Demo Inventory', - organization=o, - created_by=superuser) - Host.objects.create(name='localhost', - inventory=i, - variables="ansible_connection: local\nansible_python_interpreter: '{{ ansible_playbook_python }}'", - created_by=superuser) - jt = JobTemplate.objects.create(name='Demo Job Template', - playbook='hello_world.yml', - project=p, - inventory=i) - jt.credentials.add(c) - print('Default organization added.') - print('Demo Credential, Inventory, and Job Template added.') - print('(changed: True)') + if not Organization.objects.exists(): + o = Organization.objects.create(name='Default') + + p = Project(name='Demo Project', + scm_type='git', + scm_url='https://github.com/ansible/ansible-tower-samples', + scm_update_on_launch=True, + scm_update_cache_timeout=0, + organization=o) + p.save(skip_update=True) + + ssh_type = CredentialType.objects.filter(namespace='ssh').first() + c = Credential.objects.create(credential_type=ssh_type, + name='Demo Credential', + inputs={ + 'username': superuser.username + }, + created_by=superuser) + + c.admin_role.members.add(superuser) + + public_galaxy_credential = Credential(name='Ansible Galaxy', + managed_by_tower=True, + credential_type=CredentialType.objects.get(kind='galaxy'), + inputs={'url': 'https://galaxy.ansible.com/'}) + public_galaxy_credential.save() + o.galaxy_credentials.add(public_galaxy_credential) + + i = Inventory.objects.create(name='Demo Inventory', + organization=o, + created_by=superuser) + + Host.objects.create(name='localhost', + inventory=i, + variables="ansible_connection: local\nansible_python_interpreter: '{{ ansible_playbook_python }}'", + created_by=superuser) + + jt = JobTemplate.objects.create(name='Demo Job Template', + playbook='hello_world.yml', + project=p, + inventory=i) + jt.credentials.add(c) + + print('Default organization added.') + print('Demo Credential, Inventory, and Job Template added.') + changed = True + + default_ee = settings.AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE + ee, created = ExecutionEnvironment.objects.get_or_create(name='Default EE', defaults={'image': default_ee, + 'managed_by_tower': True}) + + if created: + changed = True + print('Default Execution Environment registered.') + + if changed: + print('(changed: True)') + else: + print('(changed: False)') diff --git a/tools/docker-compose/bootstrap_development.sh b/tools/docker-compose/bootstrap_development.sh index 3cc937c9c6..1d3e399bf7 100755 --- a/tools/docker-compose/bootstrap_development.sh +++ b/tools/docker-compose/bootstrap_development.sh @@ -26,8 +26,3 @@ make init mkdir -p /awx_devel/awx/public/static mkdir -p /awx_devel/awx/ui/static mkdir -p /awx_devel/awx/ui_next/build/static - -echo "ee, created = ExecutionEnvironment.objects.get_or_create(name='Default EE', \ - defaults={'image': 'quay.io/ansible/awx-ee', \ - 'managed_by_tower': True}); \ - print('Already exists' if not created else 'Created')" | awx-manage shell_plus --quiet-load From 8eb4dafb17be1196b77994869efdadc5881b722f Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Thu, 4 Feb 2021 17:39:14 -0500 Subject: [PATCH 111/178] Fix missing postgresql module --- tools/ansible/roles/dockerfile/templates/Dockerfile.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index 8c7e685736..22e725a216 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -139,7 +139,7 @@ RUN python3 -m ensurepip && pip3 install "virtualenv < 20" supervisor {% if buil pip3 install --no-cache-dir https://github.com/ansible/ansible/archive/${ANSIBLE_BRANCH}.tar.gz # TODO: Remove this once launch script removes need for postres modules -RUN ansible-galaxy collection install --collections-path /usr/share/ansible/collections community.general +RUN ansible-galaxy collection install --collections-path /usr/share/ansible/collections community.general community.postgresql RUN rm -rf /root/.cache && rm -rf /tmp/* From 9f39bab2b877cf8be2a206091003cfdefbcb511b Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Sun, 7 Feb 2021 10:39:13 -0500 Subject: [PATCH 112/178] Quick fix for jobs failing in dev environment / VMs The other alternative here is to go all the way with https://github.com/ansible/ansible-runner/pull/617, which is proving to be difficult if not impossible. --- awx/main/tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 0b915ab909..9fa0456fff 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -906,7 +906,8 @@ class BaseTask(object): image = instance.execution_environment.image params = { "container_image": image, - "process_isolation": True + "process_isolation": True, + "container_options": ['--user=root'], } if settings.AWX_PROOT_SHOW_PATHS: params['container_volume_mounts'] = [] From 44d7d68322b8a58ee172f20886a50ad056e89237 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 8 Feb 2021 14:10:04 -0500 Subject: [PATCH 113/178] Update default ee image --- awx/settings/defaults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 6b2f78479a..845bbe74d8 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -67,7 +67,7 @@ DATABASES = { IS_K8S = False # TODO: remove this setting in favor of a default execution environment -AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE = 'quay.io/shanemcd/awx-ee' +AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE = 'quay.io/ansible/awx-ee' AWX_CONTAINER_GROUP_K8S_API_TIMEOUT = 10 AWX_CONTAINER_GROUP_POD_LAUNCH_RETRIES = 100 From ea39cbce73a25c90633149a4779a1a7ad95edbf9 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 8 Feb 2021 14:10:13 -0500 Subject: [PATCH 114/178] Update receptor.conf in dev env --- tools/docker-compose/receptor.conf | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/docker-compose/receptor.conf b/tools/docker-compose/receptor.conf index f86d50f523..d5ac25cf2d 100644 --- a/tools/docker-compose/receptor.conf +++ b/tools/docker-compose/receptor.conf @@ -14,8 +14,15 @@ allowruntimeparams: true - work-kubernetes: - worktype: ocp + worktype: kubernetes-runtime-auth authmethod: runtime allowruntimeauth: true allowruntimepod: true allowruntimeparams: true + +- work-kubernetes: + worktype: kubernetes-incluster-auth + authmethod: incluster + allowruntimeauth: true + allowruntimepod: true + allowruntimeparams: true From ddcbc408b908626d42f8d5393bf61abaf5ecb310 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 8 Feb 2021 14:29:32 -0500 Subject: [PATCH 115/178] Remove Ansible from control plane Execution Environments or bust! --- .../roles/dockerfile/templates/Dockerfile.j2 | 28 ++----------------- tools/docker-compose/bootstrap_development.sh | 8 ------ 2 files changed, 3 insertions(+), 33 deletions(-) diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index 22e725a216..78eebc2977 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -4,15 +4,9 @@ ### DO NOT EDIT ### -# Locations - set globally to be used across stages -ARG COLLECTION_BASE="/var/lib/awx/vendor/awx_ansible_collections" -ARG ANSIBLE_BRANCH=devel - # Build container FROM centos:8 as builder -ARG ANSIBLE_BRANCH - ENV LANG en_US.UTF-8 ENV LANGUAGE en_US:en ENV LC_ALL en_US.UTF-8 @@ -49,8 +43,7 @@ RUN dnf -y update && \ xmlsec1-devel \ xmlsec1-openssl-devel -RUN python3 -m ensurepip && pip3 install "virtualenv < 20" && \ - pip3 install --no-cache-dir https://github.com/ansible/ansible/archive/${ANSIBLE_BRANCH}.tar.gz +RUN python3 -m ensurepip && pip3 install "virtualenv < 20" # Install & build requirements ADD Makefile /tmp/Makefile @@ -61,11 +54,9 @@ ADD requirements/requirements_ansible.txt \ requirements/requirements.txt \ requirements/requirements_tower_uninstall.txt \ requirements/requirements_git.txt \ - requirements/collections_requirements.yml \ /tmp/requirements/ RUN cd /tmp && make requirements_awx requirements_ansible_py3 -RUN cd /tmp && make requirements_collections {% if (build_dev|bool) or (kube_dev|bool) %} ADD requirements/requirements_dev.txt /tmp/requirements @@ -84,9 +75,6 @@ RUN make sdist && \ # Final container(s) FROM centos:8 -ARG COLLECTION_BASE -ARG ANSIBLE_BRANCH - ENV LANG en_US.UTF-8 ENV LANGUAGE en_US:en ENV LC_ALL en_US.UTF-8 @@ -100,7 +88,6 @@ RUN dnf -y update && \ dnf module -y enable 'postgresql:12' && \ dnf config-manager --set-enabled powertools && \ dnf -y install acl \ - bubblewrap \ git-core \ git-lfs \ glibc-langpack-en \ @@ -135,11 +122,7 @@ RUN curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master RUN curl -L -o /usr/bin/tini https://github.com/krallin/tini/releases/download/v0.19.0/tini-{{ tini_architecture | default('amd64') }} && \ chmod +x /usr/bin/tini -RUN python3 -m ensurepip && pip3 install "virtualenv < 20" supervisor {% if build_dev|bool %}flake8{% endif %} && \ - pip3 install --no-cache-dir https://github.com/ansible/ansible/archive/${ANSIBLE_BRANCH}.tar.gz - -# TODO: Remove this once launch script removes need for postres modules -RUN ansible-galaxy collection install --collections-path /usr/share/ansible/collections community.general community.postgresql +RUN python3 -m ensurepip && pip3 install "virtualenv < 20" supervisor {% if build_dev|bool %}flake8{% endif %} RUN rm -rf /root/.cache && rm -rf /tmp/* @@ -185,7 +168,7 @@ COPY --from=builder /var/lib/awx /var/lib/awx RUN ln -s /var/lib/awx/venv/awx/bin/awx-manage /usr/bin/awx-manage {%if build_dev|bool %} -COPY --from=quay.io/shanemcd/receptor /usr/bin/receptor /usr/bin/receptor +COPY --from=quay.io/shanemcd/receptor:latest /usr/bin/receptor /usr/bin/receptor RUN openssl req -nodes -newkey rsa:2048 -keyout /etc/nginx/nginx.key -out /etc/nginx/nginx.csr \ -subj "/C=US/ST=North Carolina/L=Durham/O=Ansible/OU=AWX Development/CN=awx.localhost" && \ openssl x509 -req -days 365 -in /etc/nginx/nginx.csr -signkey /etc/nginx/nginx.key -out /etc/nginx/nginx.crt && \ @@ -239,11 +222,6 @@ RUN for dir in \ /var/lib/awx/rsyslog/rsyslog.conf ; \ do touch $file ; chmod g+rw $file ; chgrp root $file ; done -# Adjust any remaining permissions -RUN chmod u+s /usr/bin/bwrap ; \ - chgrp -R root ${COLLECTION_BASE} ; \ - chmod -R g+rw ${COLLECTION_BASE} - {% if (build_dev|bool) or (kube_dev|bool) %} RUN for dir in \ /var/lib/awx/venv \ diff --git a/tools/docker-compose/bootstrap_development.sh b/tools/docker-compose/bootstrap_development.sh index 1d3e399bf7..88a7e62941 100755 --- a/tools/docker-compose/bootstrap_development.sh +++ b/tools/docker-compose/bootstrap_development.sh @@ -1,14 +1,6 @@ #!/bin/bash set +x -# Wait for the databases to come up -ansible -i "127.0.0.1," -c local -v -m wait_for -a "host=postgres port=5432" all -ansible -i "127.0.0.1," -c local -v -m wait_for -a "path=/var/run/redis/redis.sock" all - -# In case AWX in the container wants to connect to itself, use "docker exec" to attach to the container otherwise -# TODO: FIX -#/etc/init.d/ssh start - # Move to the source directory so we can bootstrap if [ -f "/awx_devel/manage.py" ]; then cd /awx_devel From f2dfa132a77814a72340024655e4e830c0732ce2 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 8 Feb 2021 15:50:06 -0500 Subject: [PATCH 116/178] Install Ansible only for collection tests --- Makefile | 3 ++- tools/ansible/roles/dockerfile/templates/Dockerfile.j2 | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b3d14d4ba9..858a6f67e4 100644 --- a/Makefile +++ b/Makefile @@ -383,7 +383,8 @@ test_collection: rm -f $(shell ls -d $(VENV_BASE)/awx/lib/python* | head -n 1)/no-global-site-packages.txt if [ "$(VENV_BASE)" ]; then \ . $(VENV_BASE)/awx/bin/activate; \ - fi; \ + fi && \ + pip install ansible && \ py.test $(COLLECTION_TEST_DIRS) -v # The python path needs to be modified so that the tests can find Ansible within the container # First we will use anything expility set as PYTHONPATH diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index 78eebc2977..15c25c2d3a 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -225,7 +225,9 @@ RUN for dir in \ {% if (build_dev|bool) or (kube_dev|bool) %} RUN for dir in \ /var/lib/awx/venv \ + /var/lib/awx/venv/awx/bin \ /var/lib/awx/venv/awx/lib/python3.6 \ + /var/lib/awx/venv/awx/lib/python3.6/site-packages \ /var/lib/awx/projects \ /var/lib/awx/rsyslog \ /var/run/awx-rsyslog \ From 6a7520d10fae18d43ee0d0f65b5d364ea5dcc0b6 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Tue, 9 Feb 2021 11:12:28 -0500 Subject: [PATCH 117/178] Handle quota exceeded in Container Groups v2 --- awx/main/tasks.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 9fa0456fff..ab9484eba9 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1451,6 +1451,9 @@ class BaseTask(object): receptor_job = AWXReceptorJob(self, params) res = receptor_job.run() + if not res: + return + status = res.status rc = res.rc @@ -3112,7 +3115,15 @@ class AWXReceptorJob: elif res.status == 'error': # TODO: There should be a more efficient way of getting this information receptor_work_list = receptor_ctl.simple_command("work list") - raise RuntimeError(receptor_work_list[self.unit_id]['Detail']) + detail = receptor_work_list[self.unit_id]['Detail'] + if 'exceeded quota' in detail: + logger.warn(detail) + log_name = self.task.instance.log_format + logger.warn(f"Could not launch pod for {log_name}. Exceeded quota.") + self.task.update_model(self.task.instance.pk, status='pending') + return + + raise RuntimeError(detail) return res From 92f0af684c12ae2dd2ae0c6839f80d899c47e2c4 Mon Sep 17 00:00:00 2001 From: Rebeccah Date: Wed, 27 Jan 2021 16:24:00 -0500 Subject: [PATCH 118/178] execution model pull container options added --- awx/main/models/execution_environments.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/awx/main/models/execution_environments.py b/awx/main/models/execution_environments.py index 51c7c251ea..84b6cd8c0a 100644 --- a/awx/main/models/execution_environments.py +++ b/awx/main/models/execution_environments.py @@ -12,6 +12,12 @@ class ExecutionEnvironment(CommonModel): class Meta: ordering = ('-created',) + PULL_CHOICES = [ + ('always', _("Always pull container before running.")), + ('missing', _("No pull option has been selected")), + ('never', _("Never cull container before running")) + ] + organization = models.ForeignKey( 'Organization', null=True, @@ -35,6 +41,11 @@ class ExecutionEnvironment(CommonModel): default=None, on_delete=models.SET_NULL, ) + container_options = models.CharField( + max_length=1024, + choices=PULL_CHOICES, + help_text=_('Pull image before running?'), + ) def get_absolute_url(self, request=None): return reverse('api:execution_environment_detail', kwargs={'pk': self.pk}, request=request) From 31e7e10f303e885ed2406b2e74fcb4b241593f74 Mon Sep 17 00:00:00 2001 From: Rebeccah Date: Wed, 27 Jan 2021 17:32:01 -0500 Subject: [PATCH 119/178] migration for container options for EE model Co-authored-by: Shane McDonald --- ...6_executionenvironment_container_options.py | 18 ++++++++++++++++++ awx/main/models/execution_environments.py | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 awx/main/migrations/0126_executionenvironment_container_options.py diff --git a/awx/main/migrations/0126_executionenvironment_container_options.py b/awx/main/migrations/0126_executionenvironment_container_options.py new file mode 100644 index 0000000000..27c667f9f0 --- /dev/null +++ b/awx/main/migrations/0126_executionenvironment_container_options.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.16 on 2021-01-27 22:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0125_more_ee_modeling_changes'), + ] + + operations = [ + migrations.AddField( + model_name='executionenvironment', + name='container_options', + field=models.CharField(choices=[('always', 'Always pull container before running.'), ('missing', 'No pull option has been selected'), ('never', 'Never pull container before running')], default='missing', help_text='Pull image before running?', max_length=1024), + ), + ] diff --git a/awx/main/models/execution_environments.py b/awx/main/models/execution_environments.py index 84b6cd8c0a..353b6cdbc7 100644 --- a/awx/main/models/execution_environments.py +++ b/awx/main/models/execution_environments.py @@ -15,7 +15,7 @@ class ExecutionEnvironment(CommonModel): PULL_CHOICES = [ ('always', _("Always pull container before running.")), ('missing', _("No pull option has been selected")), - ('never', _("Never cull container before running")) + ('never', _("Never pull container before running")) ] organization = models.ForeignKey( @@ -44,6 +44,7 @@ class ExecutionEnvironment(CommonModel): container_options = models.CharField( max_length=1024, choices=PULL_CHOICES, + default='missing', help_text=_('Pull image before running?'), ) From 4ca33579a5d5daf887b2f426683ab592efea4a7d Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Fri, 5 Feb 2021 12:48:49 -0500 Subject: [PATCH 120/178] Add an interface for new ee options --- awx/api/serializers.py | 2 +- .../ExecutionEnvironmentAdd.test.jsx | 26 +++++- .../ExecutionEnvironmentDetails.jsx | 22 ++++- .../ExecutionEnvironmentDetails.test.jsx | 1 + .../ExecutionEnvironmentEdit.test.jsx | 20 +++++ .../shared/ExecutionEnvironmentForm.jsx | 90 ++++++++++++++++++- .../shared/ExecutionEnvironmentForm.test.jsx | 33 ++++++- 7 files changed, 186 insertions(+), 8 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index eb13f150d3..941b465157 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1365,7 +1365,7 @@ class ExecutionEnvironmentSerializer(BaseSerializer): class Meta: model = ExecutionEnvironment - fields = ('*', 'organization', 'image', 'managed_by_tower', 'credential') + fields = ('*', 'organization', 'image', 'managed_by_tower', 'credential', 'container_options') def get_related(self, obj): res = super(ExecutionEnvironmentSerializer, self).get_related(obj) diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx index 5396746223..ede58e5d58 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx @@ -2,7 +2,10 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; -import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { + mountWithContexts, + waitForElement, +} from '../../../../testUtils/enzymeHelpers'; import { ExecutionEnvironmentsAPI } from '../../../api'; import ExecutionEnvironmentAdd from './ExecutionEnvironmentAdd'; @@ -14,11 +17,30 @@ const mockMe = { }; const executionEnvironmentData = { + name: 'Test EE', credential: 4, description: 'A simple EE', image: 'https://registry.com/image/container', + container_options: 'one', }; +const mockOptions = { + data: { + actions: { + POST: { + container_options: { + choices: [ + ['one', 'One'], + ['two', 'Two'], + ['three', 'Three'], + ], + }, + }, + }, + }, +}; + +ExecutionEnvironmentsAPI.readOptions.mockResolvedValue(mockOptions); ExecutionEnvironmentsAPI.create.mockResolvedValue({ data: { id: 42, @@ -61,6 +83,8 @@ describe('', () => { }); test('handleCancel should return the user back to the execution environments list', async () => { + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + wrapper.find('Button[aria-label="Cancel"]').simulate('click'); expect(history.location.pathname).toEqual('/execution_environments'); }); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx index abb6cf5ddc..64925df1cb 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx @@ -13,11 +13,18 @@ import { UserDateDetail, } from '../../../components/DetailList'; import useRequest, { useDismissableError } from '../../../util/useRequest'; +import { toTitleCase } from '../../../util/strings'; import { ExecutionEnvironmentsAPI } from '../../../api'; function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) { const history = useHistory(); - const { id, image, description } = executionEnvironment; + const { + id, + name, + image, + description, + container_options, + } = executionEnvironment; const { request: deleteExecutionEnvironment, @@ -35,12 +42,25 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) { return ( + + {executionEnvironment.summary_fields.credential && ( ', () => { let wrapper; let history; diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx index 5d7a16d217..4dd1b695a4 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx @@ -1,18 +1,28 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { func, shape } from 'prop-types'; import { Formik, useField, useFormikContext } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { Form } from '@patternfly/react-core'; +import { Form, FormGroup } from '@patternfly/react-core'; +import { ExecutionEnvironmentsAPI } from '../../../api'; import CredentialLookup from '../../../components/Lookup/CredentialLookup'; import FormActionGroup from '../../../components/FormActionGroup'; import FormField, { FormSubmitError } from '../../../components/FormField'; +import AnsibleSelect from '../../../components/AnsibleSelect'; import { FormColumnLayout } from '../../../components/FormLayout'; import { OrganizationLookup } from '../../../components/Lookup'; +import ContentError from '../../../components/ContentError'; +import ContentLoading from '../../../components/ContentLoading'; import { required, url } from '../../../util/validators'; +import useRequest from '../../../util/useRequest'; -function ExecutionEnvironmentFormFields({ i18n, me, executionEnvironment }) { +function ExecutionEnvironmentFormFields({ + i18n, + me, + options, + executionEnvironment, +}) { const [credentialField] = useField('credential'); const [organizationField, organizationMeta, organizationHelpers] = useField({ name: 'organization', @@ -37,8 +47,28 @@ function ExecutionEnvironmentFormFields({ i18n, me, executionEnvironment }) { [setFieldValue] ); + const [ + containerOptionsField, + containerOptionsMeta, + containerOptionsHelpers, + ] = useField({ + name: 'container_options', + }); + + const containerPullChoices = options?.actions?.POST?.container_options?.choices.map( + ([value, label]) => ({ value, label, key: value }) + ); + return ( <> + + + { + containerOptionsHelpers.setValue(value); + }} + /> + { + const res = await ExecutionEnvironmentsAPI.readOptions(); + const { data } = res; + return data; + }, []), + null + ); + + useEffect(() => { + fetchOptions(); + }, [fetchOptions]); + + if (isLoading || !options) { + return ; + } + + if (error) { + return ; + } + const initialValues = { + name: executionEnvironment.name || '', image: executionEnvironment.image || '', + container_options: executionEnvironment?.container_options || '', description: executionEnvironment.description || '', credential: executionEnvironment.summary_fields?.credential || null, organization: executionEnvironment.summary_fields?.organization || null, @@ -101,7 +178,12 @@ function ExecutionEnvironmentForm({ {formik => ( - + {submitError && } ', () => { let wrapper; let onCancel; @@ -46,16 +68,19 @@ describe('', () => { beforeEach(async () => { onCancel = jest.fn(); onSubmit = jest.fn(); + ExecutionEnvironmentsAPI.readOptions.mockResolvedValue(mockOptions); await act(async () => { wrapper = mountWithContexts( ); }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); }); afterEach(() => { @@ -83,6 +108,12 @@ describe('', () => { test('should update form values', async () => { await act(async () => { + wrapper.find('input#execution-environment-image').simulate('change', { + target: { + value: 'Updated EE Name', + name: 'name', + }, + }); wrapper.find('input#execution-environment-image').simulate('change', { target: { value: 'https://registry.com/image/container2', From b0265b060ba38316eaac3154ebd9e643b03c90ba Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Fri, 5 Feb 2021 16:53:38 -0500 Subject: [PATCH 121/178] Remove client-side url validator --- .../ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx index 4dd1b695a4..364d86e58f 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx @@ -14,7 +14,7 @@ import { FormColumnLayout } from '../../../components/FormLayout'; import { OrganizationLookup } from '../../../components/Lookup'; import ContentError from '../../../components/ContentError'; import ContentLoading from '../../../components/ContentLoading'; -import { required, url } from '../../../util/validators'; +import { required } from '../../../util/validators'; import useRequest from '../../../util/useRequest'; function ExecutionEnvironmentFormFields({ @@ -74,7 +74,7 @@ function ExecutionEnvironmentFormFields({ label={i18n._(t`Image name`)} name="image" type="text" - validate={url(i18n)} + validate={required(null, i18n)} isRequired tooltip={i18n._( t`The registry location where the container is stored.` From 4b40cb3abbb788bee0e88db49122cbdfb2e0111a Mon Sep 17 00:00:00 2001 From: Rebeccah Date: Tue, 9 Feb 2021 11:40:44 -0500 Subject: [PATCH 122/178] changed the field name from 'container_options' to simply 'pull' --- awx/api/serializers.py | 2 +- ...126_executionenvironment_container_options.py | 4 ++-- awx/main/models/execution_environments.py | 6 +++--- .../ExecutionEnvironmentAdd.test.jsx | 4 ++-- .../ExecutionEnvironmentDetails.jsx | 16 +++------------- .../ExecutionEnvironmentEdit.test.jsx | 4 ++-- .../shared/ExecutionEnvironmentForm.jsx | 6 +++--- .../shared/ExecutionEnvironmentForm.test.jsx | 4 ++-- .../modules/tower_execution_environment.py | 11 +++++++++++ 9 files changed, 29 insertions(+), 28 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 941b465157..22b7ab3a7a 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1365,7 +1365,7 @@ class ExecutionEnvironmentSerializer(BaseSerializer): class Meta: model = ExecutionEnvironment - fields = ('*', 'organization', 'image', 'managed_by_tower', 'credential', 'container_options') + fields = ('*', 'organization', 'image', 'managed_by_tower', 'credential', 'pull') def get_related(self, obj): res = super(ExecutionEnvironmentSerializer, self).get_related(obj) diff --git a/awx/main/migrations/0126_executionenvironment_container_options.py b/awx/main/migrations/0126_executionenvironment_container_options.py index 27c667f9f0..c2fd2e77b5 100644 --- a/awx/main/migrations/0126_executionenvironment_container_options.py +++ b/awx/main/migrations/0126_executionenvironment_container_options.py @@ -12,7 +12,7 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name='executionenvironment', - name='container_options', - field=models.CharField(choices=[('always', 'Always pull container before running.'), ('missing', 'No pull option has been selected'), ('never', 'Never pull container before running')], default='missing', help_text='Pull image before running?', max_length=1024), + name='pull', + field=models.CharField(choices=[('always', 'Always pull container before running.'), ('missing', 'No pull option has been selected.'), ('never', 'Never pull container before running.')], default='missing', help_text='Pull image before running?', max_length=1024), ), ] diff --git a/awx/main/models/execution_environments.py b/awx/main/models/execution_environments.py index 353b6cdbc7..681cdf94db 100644 --- a/awx/main/models/execution_environments.py +++ b/awx/main/models/execution_environments.py @@ -14,8 +14,8 @@ class ExecutionEnvironment(CommonModel): PULL_CHOICES = [ ('always', _("Always pull container before running.")), - ('missing', _("No pull option has been selected")), - ('never', _("Never pull container before running")) + ('missing', _("No pull option has been selected.")), + ('never', _("Never pull container before running.")) ] organization = models.ForeignKey( @@ -41,7 +41,7 @@ class ExecutionEnvironment(CommonModel): default=None, on_delete=models.SET_NULL, ) - container_options = models.CharField( + pull = models.CharField( max_length=1024, choices=PULL_CHOICES, default='missing', diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx index ede58e5d58..92f18c7d33 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx @@ -21,14 +21,14 @@ const executionEnvironmentData = { credential: 4, description: 'A simple EE', image: 'https://registry.com/image/container', - container_options: 'one', + pull: 'one', }; const mockOptions = { data: { actions: { POST: { - container_options: { + pull: { choices: [ ['one', 'One'], ['two', 'Two'], diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx index 64925df1cb..866b74f5fb 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx @@ -18,13 +18,7 @@ import { ExecutionEnvironmentsAPI } from '../../../api'; function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) { const history = useHistory(); - const { - id, - name, - image, - description, - container_options, - } = executionEnvironment; + const { id, name, image, description, pull } = executionEnvironment; const { request: deleteExecutionEnvironment, @@ -54,12 +48,8 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) { /> {executionEnvironment.summary_fields.credential && ( ({ value, label, key: value }) ); @@ -168,7 +168,7 @@ function ExecutionEnvironmentForm({ const initialValues = { name: executionEnvironment.name || '', image: executionEnvironment.image || '', - container_options: executionEnvironment?.container_options || '', + pull: executionEnvironment?.pull || '', description: executionEnvironment.description || '', credential: executionEnvironment.summary_fields?.credential || null, organization: executionEnvironment.summary_fields?.organization || null, diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.test.jsx index 1253852640..cddef9ffce 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.test.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.test.jsx @@ -19,7 +19,7 @@ const executionEnvironment = { id: 16, name: 'Test EE', type: 'execution_environment', - container_options: 'one', + pull: 'one', url: '/api/v2/execution_environments/16/', related: { created_by: '/api/v2/users/1/', @@ -48,7 +48,7 @@ const mockOptions = { data: { actions: { POST: { - container_options: { + pull: { choices: [ ['one', 'One'], ['two', 'Two'], diff --git a/awx_collection/plugins/modules/tower_execution_environment.py b/awx_collection/plugins/modules/tower_execution_environment.py index 280408f72a..862f4e3ab6 100644 --- a/awx_collection/plugins/modules/tower_execution_environment.py +++ b/awx_collection/plugins/modules/tower_execution_environment.py @@ -50,6 +50,12 @@ options: choices: ["present", "absent"] default: "present" type: str + pull: + description: + - determine image pull behavior + choices: ["always", "missing", "never"] + default: "missing" + type: str extends_documentation_fragment: awx.awx.auth ''' @@ -75,6 +81,7 @@ def main(): organization=dict(), credential=dict(default=''), state=dict(choices=['present', 'absent'], default='present'), + pull=dict(choices=['always', 'missing', 'never'], default='missing') ) # Create a module for ourselves @@ -85,6 +92,7 @@ def main(): image = module.params.get('image') description = module.params.get('description') state = module.params.get('state') + pull = module.params.get('pull') existing_item = module.get_one('execution_environments', name_or_id=name) @@ -98,6 +106,9 @@ def main(): if description: new_fields['description'] = description + if pull: + new_fields['pull'] = pull + # Attempt to look up the related items the user specified (these will fail the module if not found) organization = module.params.get('organization') if organization: From 0381a3ac8c7bd091d53433b2c6259c443633da47 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Tue, 9 Feb 2021 20:20:33 -0500 Subject: [PATCH 123/178] Container Pull Option -> Pull Co-authored-by: Jake McDermott --- .../ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx index 548946afeb..a8e0e33a0f 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx @@ -88,7 +88,7 @@ function ExecutionEnvironmentFormFields({ ? 'default' : 'error' } - label={i18n._(t`Container Pull Option`)} + label={i18n._(t`Pull`)} > Date: Mon, 8 Feb 2021 17:07:12 -0500 Subject: [PATCH 124/178] Update Job Detail container group variable `is_containerized` was updated on the API side to be `is_container_group`. --- awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx index 35b48f17fb..ad0be4c9e6 100644 --- a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx +++ b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx @@ -252,13 +252,13 @@ function JobDetail({ job, i18n }) { - {instanceGroup && !instanceGroup?.is_containerized && ( + {instanceGroup && !instanceGroup?.is_container_group && ( )} - {instanceGroup && instanceGroup?.is_containerized && ( + {instanceGroup && instanceGroup?.is_container_group && ( Date: Mon, 8 Feb 2021 16:35:28 -0500 Subject: [PATCH 125/178] Migrate EE list to tables Migrate EE list to tables. See:https://github.com/ansible/awx/issues/7884 --- .../ExecutionEnviromentList.test.jsx | 60 ++++++----- .../ExecutionEnvironmentList.jsx | 20 +++- .../ExecutionEnvironmentListItem.jsx | 102 +++++++++--------- .../ExecutionEnvironmentListItem.test.jsx | 54 +++++++--- 4 files changed, 135 insertions(+), 101 deletions(-) diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx index 475dd1a8b5..1490ff49e3 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx @@ -15,6 +15,7 @@ const executionEnvironments = { data: { results: [ { + name: 'Foo', id: 1, image: 'https://registry.com/r/image/manifest', organization: null, @@ -23,6 +24,7 @@ const executionEnvironments = { summary_fields: { user_capabilities: { edit: true, delete: true } }, }, { + name: 'Bar', id: 2, image: 'https://registry.com/r/image2/manifest', organization: null, @@ -38,6 +40,14 @@ const executionEnvironments = { const options = { data: { actions: { POST: true } } }; describe('', () => { + beforeEach(() => { + ExecutionEnvironmentsAPI.read.mockResolvedValue(executionEnvironments); + ExecutionEnvironmentsAPI.readOptions.mockResolvedValue(options); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); let wrapper; test('should mount successfully', async () => { @@ -52,9 +62,6 @@ describe('', () => { }); test('should have data fetched and render 2 rows', async () => { - ExecutionEnvironmentsAPI.read.mockResolvedValue(executionEnvironments); - ExecutionEnvironmentsAPI.readOptions.mockResolvedValue(options); - await act(async () => { wrapper = mountWithContexts(); }); @@ -69,10 +76,7 @@ describe('', () => { expect(ExecutionEnvironmentsAPI.readOptions).toBeCalled(); }); - test('should delete item successfully', async () => { - ExecutionEnvironmentsAPI.read.mockResolvedValue(executionEnvironments); - ExecutionEnvironmentsAPI.readOptions.mockResolvedValue(options); - + test('should delete items successfully', async () => { await act(async () => { wrapper = mountWithContexts(); }); @@ -82,27 +86,25 @@ describe('', () => { el => el.length > 0 ); - wrapper - .find('input#select-execution-environment-1') - .simulate('change', executionEnvironments.data.results[0]); - wrapper.update(); - - expect( - wrapper.find('input#select-execution-environment-1').prop('checked') - ).toBe(true); - await act(async () => { - wrapper.find('Button[aria-label="Delete"]').prop('onClick')(); + wrapper + .find('ExecutionEnvironmentListItem') + .at(0) + .invoke('onSelect')(); }); wrapper.update(); - await act(async () => { - wrapper.find('Button[aria-label="confirm delete"]').prop('onClick')(); + wrapper + .find('ExecutionEnvironmentListItem') + .at(1) + .invoke('onSelect')(); + }); + wrapper.update(); + await act(async () => { + wrapper.find('ToolbarDeleteButton').invoke('onDelete')(); }); - expect(ExecutionEnvironmentsAPI.destroy).toBeCalledWith( - executionEnvironments.data.results[0].id - ); + expect(ExecutionEnvironmentsAPI.destroy).toHaveBeenCalledTimes(2); }); test('should render deletion error modal', async () => { @@ -117,19 +119,24 @@ describe('', () => { }, }) ); - ExecutionEnvironmentsAPI.read.mockResolvedValue(executionEnvironments); - ExecutionEnvironmentsAPI.readOptions.mockResolvedValue(options); await act(async () => { wrapper = mountWithContexts(); }); waitForElement(wrapper, 'ExecutionEnvironmentList', el => el.length > 0); wrapper - .find('input#select-execution-environment-1') + .find('ExecutionEnvironmentListItem') + .at(0) + .find('input') .simulate('change', 'a'); wrapper.update(); + expect( - wrapper.find('input#select-execution-environment-1').prop('checked') + wrapper + .find('ExecutionEnvironmentListItem') + .at(0) + .find('input') + .prop('checked') ).toBe(true); await act(async () => @@ -156,7 +163,6 @@ describe('', () => { }, }) ); - ExecutionEnvironmentsAPI.readOptions.mockResolvedValue(options); await act(async () => { wrapper = mountWithContexts(); }); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx index 749fe8893e..9d94085520 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx @@ -8,10 +8,14 @@ import { ExecutionEnvironmentsAPI } from '../../../api'; import { getQSConfig, parseQueryString } from '../../../util/qs'; import useRequest, { useDeleteItems } from '../../../util/useRequest'; import useSelected from '../../../util/useSelected'; -import PaginatedDataList, { +import { ToolbarDeleteButton, ToolbarAddButton, } from '../../../components/PaginatedDataList'; +import PaginatedTable, { + HeaderRow, + HeaderCell, +} from '../../../components/PaginatedTable'; import ErrorDetail from '../../../components/ErrorDetail'; import AlertModal from '../../../components/AlertModal'; import DatalistToolbar from '../../../components/DataListToolbar'; @@ -21,7 +25,7 @@ import ExecutionEnvironmentsListItem from './ExecutionEnvironmentListItem'; const QS_CONFIG = getQSConfig('execution_environments', { page: 1, page_size: 20, - order_by: 'image', + order_by: 'name', }); function ExecutionEnvironmentList({ i18n }) { @@ -106,7 +110,7 @@ function ExecutionEnvironmentList({ i18n }) { <> - + {i18n._(t`Name`)} + {i18n._(t`Image`)} + {i18n._(t`Organization`)} + + } renderToolbar={props => ( )} - renderItem={executionEnvironment => ( + renderRow={(executionEnvironment, index) => ( handleSelect(executionEnvironment)} diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx index 8e41c5a0f8..fbc8387d84 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx @@ -3,18 +3,11 @@ import { string, bool, func } from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Link } from 'react-router-dom'; -import { - Button, - DataListAction, - DataListCheck, - DataListItem, - DataListItemRow, - DataListItemCells, - Tooltip, -} from '@patternfly/react-core'; +import { Button } from '@patternfly/react-core'; +import { Tr, Td } from '@patternfly/react-table'; import { PencilAltIcon } from '@patternfly/react-icons'; -import DataListCell from '../../../components/DataListCell'; +import { ActionsTd, ActionItem } from '../../../components/PaginatedTable'; import { ExecutionEnvironment } from '../../../types'; function ExecutionEnvironmentListItem({ @@ -23,55 +16,56 @@ function ExecutionEnvironmentListItem({ isSelected, onSelect, i18n, + rowIndex, }) { const labelId = `check-action-${executionEnvironment.id}`; return ( - - - - - - {executionEnvironment.image} - - , - ]} - /> - - + + + + {executionEnvironment.name} + + + + {executionEnvironment.image} + + + {executionEnvironment.organization ? ( + - - - - - + {executionEnvironment?.summary_fields?.organization?.name} + + ) : ( + i18n._(t`Globally Available`) + )} + + + + + + + ); } diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.test.jsx index 4f51a51672..0e7c037aed 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.test.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.test.jsx @@ -8,21 +8,27 @@ import ExecutionEnvironmentListItem from './ExecutionEnvironmentListItem'; describe('', () => { let wrapper; const executionEnvironment = { + name: 'Foo', id: 1, image: 'https://registry.com/r/image/manifest', organization: null, credential: null, + summary_fields: { user_capabilities: { edit: true } }, }; test('should mount successfully', async () => { await act(async () => { wrapper = mountWithContexts( - {}} - /> + + + {}} + /> + +
); }); expect(wrapper.find('ExecutionEnvironmentListItem').length).toBe(1); @@ -31,22 +37,38 @@ describe('', () => { test('should render the proper data', async () => { await act(async () => { wrapper = mountWithContexts( - {}} - /> + + + {}} + /> + +
); }); expect( wrapper - .find('DataListCell[aria-label="execution environment image"]') + .find('Td') + .at(1) + .text() + ).toBe(executionEnvironment.name); + expect( + wrapper + .find('Td') + .at(2) .text() ).toBe(executionEnvironment.image); - expect(wrapper.find('PencilAltIcon').length).toBe(1); + expect( - wrapper.find('input#select-execution-environment-1').prop('checked') - ).toBe(false); + wrapper + .find('Td') + .at(3) + .text() + ).toBe('Globally Available'); + + expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy(); }); }); From 05ef51f710dad8f8036bc5acee4097db4adc0d71 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 15 Feb 2021 17:10:43 -0500 Subject: [PATCH 126/178] Add migration to reset custom pod specs --- .../migrations/0127_reset_pod_spec_override.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 awx/main/migrations/0127_reset_pod_spec_override.py diff --git a/awx/main/migrations/0127_reset_pod_spec_override.py b/awx/main/migrations/0127_reset_pod_spec_override.py new file mode 100644 index 0000000000..c3ebe0b504 --- /dev/null +++ b/awx/main/migrations/0127_reset_pod_spec_override.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.16 on 2021-02-15 22:02 + +from django.db import migrations + +def reset_pod_specs(apps, schema_editor): + InstanceGroup = apps.get_model('main', 'InstanceGroup') + InstanceGroup.objects.update(pod_spec_override="") + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0126_executionenvironment_container_options'), + ] + + operations = [ + migrations.RunPython(reset_pod_specs) + ] From 0bd8012fd9b72ec3cc45bb8633c8bfedf4bd17c0 Mon Sep 17 00:00:00 2001 From: nixocio Date: Wed, 17 Feb 2021 16:55:22 -0500 Subject: [PATCH 127/178] Update selectors on EE details page to ease testing Update selectors on EE details page to ease testing. --- .../ExecutionEnvironmentDetails.jsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx index 866b74f5fb..13839bb95b 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx @@ -46,10 +46,15 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) { value={image} dataCy="execution-environment-detail-image" /> - + {executionEnvironment.summary_fields.credential && (
@@ -78,6 +85,7 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) { aria-label={i18n._(t`edit`)} component={Link} to={`/execution_environments/${id}/edit`} + ouiaId="edit-button" > {i18n._(t`Edit`)} @@ -86,6 +94,7 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) { modalTitle={i18n._(t`Delete Execution Environment`)} onConfirm={deleteExecutionEnvironment} isDisabled={isLoading} + ouiaId="delete-button" > {i18n._(t`Delete`)} From 20ee73ce73160fe723cd82cb16aa18a03067f70a Mon Sep 17 00:00:00 2001 From: Rebeccah Date: Tue, 16 Feb 2021 14:11:56 -0500 Subject: [PATCH 128/178] default pull options for container images to None, also adding pull options to awxkit --- .../0128_set_default_pull_to_none.py | 18 ++++++++++++++++++ awx/main/models/execution_environments.py | 2 +- .../modules/tower_execution_environment.py | 2 +- .../awxkit/api/pages/execution_environments.py | 7 ++++--- 4 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 awx/main/migrations/0128_set_default_pull_to_none.py diff --git a/awx/main/migrations/0128_set_default_pull_to_none.py b/awx/main/migrations/0128_set_default_pull_to_none.py new file mode 100644 index 0000000000..3643c610e1 --- /dev/null +++ b/awx/main/migrations/0128_set_default_pull_to_none.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.16 on 2021-02-16 20:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0127_reset_pod_spec_override'), + ] + + operations = [ + migrations.AlterField( + model_name='executionenvironment', + name='pull', + field=models.CharField(choices=[('always', 'Always pull container before running.'), ('missing', 'No pull option has been selected.'), ('never', 'Never pull container before running.')], default=None, help_text='Pull image before running?', max_length=1024), + ), + ] diff --git a/awx/main/models/execution_environments.py b/awx/main/models/execution_environments.py index 681cdf94db..cddd8c41e5 100644 --- a/awx/main/models/execution_environments.py +++ b/awx/main/models/execution_environments.py @@ -44,7 +44,7 @@ class ExecutionEnvironment(CommonModel): pull = models.CharField( max_length=1024, choices=PULL_CHOICES, - default='missing', + default=None, help_text=_('Pull image before running?'), ) diff --git a/awx_collection/plugins/modules/tower_execution_environment.py b/awx_collection/plugins/modules/tower_execution_environment.py index 862f4e3ab6..e738f9a3bf 100644 --- a/awx_collection/plugins/modules/tower_execution_environment.py +++ b/awx_collection/plugins/modules/tower_execution_environment.py @@ -54,7 +54,7 @@ options: description: - determine image pull behavior choices: ["always", "missing", "never"] - default: "missing" + default: None type: str extends_documentation_fragment: awx.awx.auth ''' diff --git a/awxkit/awxkit/api/pages/execution_environments.py b/awxkit/awxkit/api/pages/execution_environments.py index a01aa91011..11e3e2f881 100644 --- a/awxkit/awxkit/api/pages/execution_environments.py +++ b/awxkit/awxkit/api/pages/execution_environments.py @@ -21,9 +21,9 @@ class ExecutionEnvironment(HasCreate, base.Base): NATURAL_KEY = ('name',) # fields are name, image, organization, managed_by_tower, credential - def create(self, name='', image='quay.io/ansible/ansible-runner:devel', credential=None, **kwargs): + def create(self, name='', image='quay.io/ansible/ansible-runner:devel', credential=None, pull=None, **kwargs): # we do not want to make a credential by default - payload = self.create_payload(name=name, image=image, credential=credential, **kwargs) + payload = self.create_payload(name=name, image=image, credential=credential, pull=pull, **kwargs) ret = self.update_identity(ExecutionEnvironments(self.connection).post(payload)) return ret @@ -33,12 +33,13 @@ class ExecutionEnvironment(HasCreate, base.Base): payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store) return payload - def payload(self, name='', image=None, organization=None, credential=None, **kwargs): + def payload(self, name='', image=None, organization=None, credential=None, pull=None, **kwargs): payload = PseudoNamespace( name=name or "EE - {}".format(random_title()), image=image or random_title(10), organization=organization.id if organization else None, credential=credential.id if credential else None, + pull=pull if pull else None, **kwargs ) From b1361c8fe28a3682b1d27e652eb9acc6f4716f52 Mon Sep 17 00:00:00 2001 From: Rebeccah Date: Tue, 16 Feb 2021 17:41:44 -0500 Subject: [PATCH 129/178] edit original migration file, add blank string as acceptable to model --- ...6_executionenvironment_container_options.py | 2 +- .../0128_set_default_pull_to_none.py | 18 ------------------ awx/main/models/execution_environments.py | 5 +++-- .../modules/tower_execution_environment.py | 2 +- .../awxkit/api/pages/execution_environments.py | 6 +++--- 5 files changed, 8 insertions(+), 25 deletions(-) delete mode 100644 awx/main/migrations/0128_set_default_pull_to_none.py diff --git a/awx/main/migrations/0126_executionenvironment_container_options.py b/awx/main/migrations/0126_executionenvironment_container_options.py index c2fd2e77b5..d26fcb9298 100644 --- a/awx/main/migrations/0126_executionenvironment_container_options.py +++ b/awx/main/migrations/0126_executionenvironment_container_options.py @@ -13,6 +13,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='executionenvironment', name='pull', - field=models.CharField(choices=[('always', 'Always pull container before running.'), ('missing', 'No pull option has been selected.'), ('never', 'Never pull container before running.')], default='missing', help_text='Pull image before running?', max_length=1024), + field=models.CharField(choices=[('always', 'Always pull container before running.'), ('missing', 'No pull option has been selected.'), ('never', 'Never pull container before running.')], blank=True, default='', help_text='Pull image before running?', max_length=16), ), ] diff --git a/awx/main/migrations/0128_set_default_pull_to_none.py b/awx/main/migrations/0128_set_default_pull_to_none.py deleted file mode 100644 index 3643c610e1..0000000000 --- a/awx/main/migrations/0128_set_default_pull_to_none.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.16 on 2021-02-16 20:38 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0127_reset_pod_spec_override'), - ] - - operations = [ - migrations.AlterField( - model_name='executionenvironment', - name='pull', - field=models.CharField(choices=[('always', 'Always pull container before running.'), ('missing', 'No pull option has been selected.'), ('never', 'Never pull container before running.')], default=None, help_text='Pull image before running?', max_length=1024), - ), - ] diff --git a/awx/main/models/execution_environments.py b/awx/main/models/execution_environments.py index cddd8c41e5..eabd0cce7c 100644 --- a/awx/main/models/execution_environments.py +++ b/awx/main/models/execution_environments.py @@ -42,9 +42,10 @@ class ExecutionEnvironment(CommonModel): on_delete=models.SET_NULL, ) pull = models.CharField( - max_length=1024, + max_length=16, choices=PULL_CHOICES, - default=None, + blank=True, + default='', help_text=_('Pull image before running?'), ) diff --git a/awx_collection/plugins/modules/tower_execution_environment.py b/awx_collection/plugins/modules/tower_execution_environment.py index e738f9a3bf..320141721d 100644 --- a/awx_collection/plugins/modules/tower_execution_environment.py +++ b/awx_collection/plugins/modules/tower_execution_environment.py @@ -54,7 +54,7 @@ options: description: - determine image pull behavior choices: ["always", "missing", "never"] - default: None + default: '' type: str extends_documentation_fragment: awx.awx.auth ''' diff --git a/awxkit/awxkit/api/pages/execution_environments.py b/awxkit/awxkit/api/pages/execution_environments.py index 11e3e2f881..94f53c1094 100644 --- a/awxkit/awxkit/api/pages/execution_environments.py +++ b/awxkit/awxkit/api/pages/execution_environments.py @@ -21,7 +21,7 @@ class ExecutionEnvironment(HasCreate, base.Base): NATURAL_KEY = ('name',) # fields are name, image, organization, managed_by_tower, credential - def create(self, name='', image='quay.io/ansible/ansible-runner:devel', credential=None, pull=None, **kwargs): + def create(self, name='', image='quay.io/ansible/ansible-runner:devel', credential=None, pull='', **kwargs): # we do not want to make a credential by default payload = self.create_payload(name=name, image=image, credential=credential, pull=pull, **kwargs) ret = self.update_identity(ExecutionEnvironments(self.connection).post(payload)) @@ -33,13 +33,13 @@ class ExecutionEnvironment(HasCreate, base.Base): payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store) return payload - def payload(self, name='', image=None, organization=None, credential=None, pull=None, **kwargs): + def payload(self, name='', image=None, organization=None, credential=None, pull='', **kwargs): payload = PseudoNamespace( name=name or "EE - {}".format(random_title()), image=image or random_title(10), organization=organization.id if organization else None, credential=credential.id if credential else None, - pull=pull if pull else None, + pull=pull, **kwargs ) From ae5a1117d4960e16188128a7aabc251406c1e881 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 22 Feb 2021 14:51:12 -0500 Subject: [PATCH 130/178] Use official Receptor 0.9.5 release --- tools/ansible/roles/dockerfile/templates/Dockerfile.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index 15c25c2d3a..e17a0b04d8 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -168,7 +168,7 @@ COPY --from=builder /var/lib/awx /var/lib/awx RUN ln -s /var/lib/awx/venv/awx/bin/awx-manage /usr/bin/awx-manage {%if build_dev|bool %} -COPY --from=quay.io/shanemcd/receptor:latest /usr/bin/receptor /usr/bin/receptor +COPY --from=quay.io/project-receptor/receptor:0.9.5 /usr/bin/receptor /usr/bin/receptor RUN openssl req -nodes -newkey rsa:2048 -keyout /etc/nginx/nginx.key -out /etc/nginx/nginx.csr \ -subj "/C=US/ST=North Carolina/L=Durham/O=Ansible/OU=AWX Development/CN=awx.localhost" && \ openssl x509 -req -days 365 -in /etc/nginx/nginx.csr -signkey /etc/nginx/nginx.key -out /etc/nginx/nginx.crt && \ From 05dded397d67541b66a77d38600581e0e0887831 Mon Sep 17 00:00:00 2001 From: Elijah DeLee Date: Mon, 22 Feb 2021 14:00:33 -0500 Subject: [PATCH 131/178] make sure we use built in credential type this way we can pass kind="registry" to akit creat method and we get the correct built in type --- awxkit/awxkit/api/pages/credentials.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awxkit/awxkit/api/pages/credentials.py b/awxkit/awxkit/api/pages/credentials.py index f964ae38e4..e5b80cf7d0 100644 --- a/awxkit/awxkit/api/pages/credentials.py +++ b/awxkit/awxkit/api/pages/credentials.py @@ -82,6 +82,7 @@ def config_cred_from_kind(kind): credential_type_name_to_config_kind_map = { 'amazon web services': 'aws', + 'container registry': 'registry', 'ansible galaxy/automation hub api token': 'galaxy', 'ansible tower': 'tower', 'google compute engine': 'gce', From a39e1a528b6ff40372c7e9ae04728b96bd01fac7 Mon Sep 17 00:00:00 2001 From: nixocio Date: Mon, 5 Oct 2020 16:45:57 -0400 Subject: [PATCH 132/178] Add execution environment list to Organizations Add execution environment list to Organizations See: https://github.com/ansible/awx/issues/8210 --- awx/ui_next/src/api/models/Organizations.js | 11 ++ .../src/screens/Organization/Organization.jsx | 11 ++ .../Organization/Organization.test.jsx | 4 +- .../OrganizationExecEnvList.jsx | 126 ++++++++++++++++++ .../OrganizationExecEnvList.test.jsx | 116 ++++++++++++++++ .../OrganizationExecEnvListItem.jsx | 51 +++++++ .../OrganizationExecEnvListItem.test.jsx | 44 ++++++ .../OrganizationExecEnvList/index.js | 1 + .../screens/Organization/Organizations.jsx | 3 + 9 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx create mode 100644 awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.test.jsx create mode 100644 awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.jsx create mode 100644 awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.test.jsx create mode 100644 awx/ui_next/src/screens/Organization/OrganizationExecEnvList/index.js diff --git a/awx/ui_next/src/api/models/Organizations.js b/awx/ui_next/src/api/models/Organizations.js index ce236067b4..ec4a14c549 100644 --- a/awx/ui_next/src/api/models/Organizations.js +++ b/awx/ui_next/src/api/models/Organizations.js @@ -29,6 +29,17 @@ class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) { params, }); } + readExecutionEnvironments(id, params) { + return this.http.get(`${this.baseUrl}${id}/execution_environments/`, { + params, + }); + } + + readExecutionEnvironmentsOptions(id, params) { + return this.http.options(`${this.baseUrl}${id}/execution_environments/`, { + params, + }); + } createUser(id, data) { return this.http.post(`${this.baseUrl}${id}/users/`, data); diff --git a/awx/ui_next/src/screens/Organization/Organization.jsx b/awx/ui_next/src/screens/Organization/Organization.jsx index 1a2ee641c9..a826ef641d 100644 --- a/awx/ui_next/src/screens/Organization/Organization.jsx +++ b/awx/ui_next/src/screens/Organization/Organization.jsx @@ -22,6 +22,7 @@ import OrganizationDetail from './OrganizationDetail'; import OrganizationEdit from './OrganizationEdit'; import OrganizationTeams from './OrganizationTeams'; import { OrganizationsAPI } from '../../api'; +import OrganizationExecEnvList from './OrganizationExecEnvList'; function Organization({ i18n, setBreadcrumb, me }) { const location = useLocation(); @@ -122,6 +123,11 @@ function Organization({ i18n, setBreadcrumb, me }) { { name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 }, { name: i18n._(t`Access`), link: `${match.url}/access`, id: 1 }, { name: i18n._(t`Teams`), link: `${match.url}/teams`, id: 2 }, + { + name: i18n._(t`Execution Environments`), + link: `${match.url}/execution_environments`, + id: 4, + }, ]; if (canSeeNotificationsTab) { @@ -208,6 +214,11 @@ function Organization({ i18n, setBreadcrumb, me }) { />
)} + {organization && ( + + + + )} {!organizationLoading && !rolesLoading && ( diff --git a/awx/ui_next/src/screens/Organization/Organization.test.jsx b/awx/ui_next/src/screens/Organization/Organization.test.jsx index 10982505d5..487ebff36b 100644 --- a/awx/ui_next/src/screens/Organization/Organization.test.jsx +++ b/awx/ui_next/src/screens/Organization/Organization.test.jsx @@ -68,7 +68,7 @@ describe('', () => { const tabs = await waitForElement( wrapper, '.pf-c-tabs__item', - el => el.length === 5 + el => el.length === 6 ); expect(tabs.last().text()).toEqual('Notifications'); wrapper.unmount(); @@ -92,7 +92,7 @@ describe('', () => { const tabs = await waitForElement( wrapper, '.pf-c-tabs__item', - el => el.length === 4 + el => el.length === 5 ); tabs.forEach(tab => expect(tab.text()).not.toEqual('Notifications')); wrapper.unmount(); diff --git a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx new file mode 100644 index 0000000000..48caaff802 --- /dev/null +++ b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx @@ -0,0 +1,126 @@ +import React, { useEffect, useCallback } from 'react'; +import { useLocation } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Card } from '@patternfly/react-core'; + +import { OrganizationsAPI } from '../../../api'; +import { getQSConfig, parseQueryString } from '../../../util/qs'; +import useRequest from '../../../util/useRequest'; +import PaginatedDataList from '../../../components/PaginatedDataList'; +import DatalistToolbar from '../../../components/DataListToolbar'; + +import OrganizationExecEnvListItem from './OrganizationExecEnvListItem'; + +const QS_CONFIG = getQSConfig('organizations', { + page: 1, + page_size: 20, + order_by: 'image', +}); + +function OrganizationExecEnvList({ i18n, organization }) { + const { id } = organization; + const location = useLocation(); + + const { + error: contentError, + isLoading, + request: fetchExecutionEnvironments, + result: { + executionEnvironments, + executionEnvironmentsCount, + relatedSearchableKeys, + searchableKeys, + }, + } = useRequest( + useCallback(async () => { + const params = parseQueryString(QS_CONFIG, location.search); + + const [response, responseActions] = await Promise.all([ + OrganizationsAPI.readExecutionEnvironments(id, params), + OrganizationsAPI.readExecutionEnvironmentsOptions(id, params), + ]); + + return { + executionEnvironments: response.data.results, + executionEnvironmentsCount: response.data.count, + actions: responseActions.data.actions, + relatedSearchableKeys: ( + responseActions?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + responseActions.data.actions?.GET || {} + ).filter(key => responseActions.data.actions?.GET[key].filterable), + }; + }, [location, id]), + { + executionEnvironments: [], + executionEnvironmentsCount: 0, + actions: {}, + relatedSearchableKeys: [], + searchableKeys: [], + } + ); + + useEffect(() => { + fetchExecutionEnvironments(); + }, [fetchExecutionEnvironments]); + + return ( + <> + + ( + + )} + renderItem={executionEnvironment => ( + + )} + /> + + + ); +} + +export default withI18n()(OrganizationExecEnvList); diff --git a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.test.jsx b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.test.jsx new file mode 100644 index 0000000000..07e8a53ea5 --- /dev/null +++ b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.test.jsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; + +import { + mountWithContexts, + waitForElement, +} from '../../../../testUtils/enzymeHelpers'; + +import { OrganizationsAPI } from '../../../api'; +import OrganizationExecEnvList from './OrganizationExecEnvList'; + +jest.mock('../../../api/'); + +const executionEnvironments = { + data: { + count: 3, + results: [ + { + id: 1, + type: 'execution_environment', + url: '/api/v2/execution_environments/1/', + related: { + organization: '/api/v2/organizations/1/', + }, + organization: 1, + image: 'https://localhost.com/image/disk', + managed_by_tower: false, + credential: null, + }, + { + id: 2, + type: 'execution_environment', + url: '/api/v2/execution_environments/2/', + related: { + organization: '/api/v2/organizations/1/', + }, + organization: 1, + image: 'test/image123', + managed_by_tower: false, + credential: null, + }, + { + id: 3, + type: 'execution_environment', + url: '/api/v2/execution_environments/3/', + related: { + organization: '/api/v2/organizations/1/', + }, + organization: 1, + image: 'test/test', + managed_by_tower: false, + credential: null, + }, + ], + }, +}; + +const mockOrganization = { + id: 1, + type: 'organization', + name: 'Default', +}; + +const options = { data: { actions: { POST: {}, GET: {} } } }; + +describe('', () => { + let wrapper; + + test('should mount successfully', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await waitForElement( + wrapper, + 'OrganizationExecEnvList', + el => el.length > 0 + ); + }); + + test('should have data fetched and render 3 rows', async () => { + OrganizationsAPI.readExecutionEnvironments.mockResolvedValue( + executionEnvironments + ); + + OrganizationsAPI.readExecutionEnvironmentsOptions.mockResolvedValue( + options + ); + + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await waitForElement( + wrapper, + 'OrganizationExecEnvList', + el => el.length > 0 + ); + + expect(wrapper.find('OrganizationExecEnvListItem').length).toBe(3); + expect(OrganizationsAPI.readExecutionEnvironments).toBeCalled(); + expect(OrganizationsAPI.readExecutionEnvironmentsOptions).toBeCalled(); + }); + + test('should not render add button', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + waitForElement(wrapper, 'OrganizationExecEnvList', el => el.length > 0); + expect(wrapper.find('ToolbarAddButton').length).toBe(0); + }); +}); diff --git a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.jsx b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.jsx new file mode 100644 index 0000000000..1998e8c783 --- /dev/null +++ b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { string } from 'prop-types'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Link } from 'react-router-dom'; +import { + DataListItem, + DataListItemRow, + DataListItemCells, +} from '@patternfly/react-core'; + +import DataListCell from '../../../components/DataListCell'; +import { ExecutionEnvironment } from '../../../types'; + +function OrganizationExecEnvListItem({ + executionEnvironment, + detailUrl, + i18n, +}) { + const labelId = `check-action-${executionEnvironment.id}`; + + return ( + + + + + {executionEnvironment.image} + + , + ]} + /> + + + ); +} + +OrganizationExecEnvListItem.prototype = { + executionEnvironment: ExecutionEnvironment.isRequired, + detailUrl: string.isRequired, +}; + +export default withI18n()(OrganizationExecEnvListItem); diff --git a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.test.jsx b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.test.jsx new file mode 100644 index 0000000000..9e4a2492aa --- /dev/null +++ b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.test.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; + +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; + +import OrganizationExecEnvListItem from './OrganizationExecEnvListItem'; + +describe('', () => { + let wrapper; + const executionEnvironment = { + id: 1, + image: 'https://registry.com/r/image/manifest', + organization: 1, + credential: null, + }; + + test('should mount successfully', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(wrapper.find('OrganizationExecEnvListItem').length).toBe(1); + }); + + test('should render the proper data', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect( + wrapper + .find('DataListCell[aria-label="Execution environment image"]') + .text() + ).toBe(executionEnvironment.image); + }); +}); diff --git a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/index.js b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/index.js new file mode 100644 index 0000000000..668a3beb61 --- /dev/null +++ b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/index.js @@ -0,0 +1 @@ +export { default } from './OrganizationExecEnvList'; diff --git a/awx/ui_next/src/screens/Organization/Organizations.jsx b/awx/ui_next/src/screens/Organization/Organizations.jsx index 6c7b17dc69..fcf1b8398b 100644 --- a/awx/ui_next/src/screens/Organization/Organizations.jsx +++ b/awx/ui_next/src/screens/Organization/Organizations.jsx @@ -34,6 +34,9 @@ function Organizations({ i18n }) { [`/organizations/${organization.id}/notifications`]: i18n._( t`Notifications` ), + [`/organizations/${organization.id}/execution_environments`]: i18n._( + t`Execution Environments` + ), }; setBreadcrumbConfig(breadcrumb); }, From cf513b33ee641ed8d5a4bec21eae57a186f5bc8f Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Mon, 22 Feb 2021 16:19:21 -0500 Subject: [PATCH 133/178] Add name field --- awx/ui_next/src/api/models/Organizations.js | 1 + .../OrganizationExecEnvList.jsx | 13 +++++++++++-- .../OrganizationExecEnvListItem.jsx | 12 +++++++++--- .../OrganizationExecEnvListItem.test.jsx | 2 ++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/awx/ui_next/src/api/models/Organizations.js b/awx/ui_next/src/api/models/Organizations.js index ec4a14c549..fd980fece8 100644 --- a/awx/ui_next/src/api/models/Organizations.js +++ b/awx/ui_next/src/api/models/Organizations.js @@ -29,6 +29,7 @@ class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) { params, }); } + readExecutionEnvironments(id, params) { return this.http.get(`${this.baseUrl}${id}/execution_environments/`, { params, diff --git a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx index 48caaff802..9f2c4ae817 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx @@ -15,7 +15,7 @@ import OrganizationExecEnvListItem from './OrganizationExecEnvListItem'; const QS_CONFIG = getQSConfig('organizations', { page: 1, page_size: 20, - order_by: 'image', + order_by: 'name', }); function OrganizationExecEnvList({ i18n, organization }) { @@ -79,10 +79,15 @@ function OrganizationExecEnvList({ i18n, organization }) { toolbarSearchableKeys={searchableKeys} toolbarRelatedSearchableKeys={relatedSearchableKeys} toolbarSearchColumns={[ + { + name: i18n._(t`Name`), + key: 'name__icontains', + isDefault: true, + }, { name: i18n._(t`Image`), key: 'image__icontains', - isDefault: true, + isDefault: false, }, { name: i18n._(t`Created By (Username)`), @@ -94,6 +99,10 @@ function OrganizationExecEnvList({ i18n, organization }) { }, ]} toolbarSortColumns={[ + { + name: i18n._(t`Name`), + key: 'name', + }, { name: i18n._(t`Image`), key: 'image', diff --git a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.jsx b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.jsx index 1998e8c783..0d2715d7a6 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.jsx @@ -28,13 +28,19 @@ function OrganizationExecEnvListItem({ + + {executionEnvironment.name} + + , - - {executionEnvironment.image} - + {executionEnvironment.image} , ]} /> diff --git a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.test.jsx b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.test.jsx index 9e4a2492aa..29181f4ec3 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.test.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.test.jsx @@ -10,8 +10,10 @@ describe('', () => { const executionEnvironment = { id: 1, image: 'https://registry.com/r/image/manifest', + name: 'foo', organization: 1, credential: null, + pull: 'always', }; test('should mount successfully', async () => { From eaa74b40c106f63c69f4766713f5f8522b4dd044 Mon Sep 17 00:00:00 2001 From: Rebeccah Date: Wed, 17 Feb 2021 14:54:17 -0500 Subject: [PATCH 134/178] add org admins as able to control EEs even if they don't have the ee_admin role for the specific ee and prevent managed_by_tower EEs from being edited/deleted --- awx/main/access.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index a8a110e9c3..8817e7b917 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1325,7 +1325,7 @@ class ExecutionEnvironmentAccess(BaseAccess): def filtered_queryset(self): return ExecutionEnvironment.objects.filter( - Q(organization__in=Organization.accessible_pk_qs(self.user, 'execution_environment_admin_role')) | + Q(organization__in=Organization.accessible_pk_qs(self.user, 'member_role')) | Q(organization__isnull=True) ).distinct() @@ -1337,9 +1337,11 @@ class ExecutionEnvironmentAccess(BaseAccess): @check_superuser def can_change(self, obj, data): + if obj.managed_by_tower is True: + raise PermissionDenied if obj and obj.organization_id is None: raise PermissionDenied - if self.user not in obj.organization.execution_environment_admin_role: + if self.user not in obj.organization.execution_environment_admin_role and self.user not in obj.organization.admin_role: raise PermissionDenied org_pk = get_pk_from_dict(data, 'organization') if obj and obj.organization_id != org_pk: From 41fb21911e684a0c6363d77c19459571051f611c Mon Sep 17 00:00:00 2001 From: Rebeccah Date: Thu, 18 Feb 2021 17:24:17 -0500 Subject: [PATCH 135/178] add execution_environment_admin_role to the an organizations read role, which access.py uses for determining access to reading an ee within an organization, add migration file for execution_env_admin role addition to read_roles within an organization, and set check related to mandatory --- awx/main/access.py | 6 +++--- .../0128_organiaztion_read_roles_ee_admin.py | 20 +++++++++++++++++++ awx/main/models/organization.py | 3 ++- 3 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 awx/main/migrations/0128_organiaztion_read_roles_ee_admin.py diff --git a/awx/main/access.py b/awx/main/access.py index 8817e7b917..9e9631751c 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1325,7 +1325,7 @@ class ExecutionEnvironmentAccess(BaseAccess): def filtered_queryset(self): return ExecutionEnvironment.objects.filter( - Q(organization__in=Organization.accessible_pk_qs(self.user, 'member_role')) | + Q(organization__in=Organization.accessible_pk_qs(self.user, 'read_role')) | Q(organization__isnull=True) ).distinct() @@ -1333,7 +1333,7 @@ class ExecutionEnvironmentAccess(BaseAccess): def can_add(self, data): if not data: # So the browseable API will work return Organization.accessible_objects(self.user, 'execution_environment_admin_role').exists() - return self.check_related('organization', Organization, data) + return self.check_related('organization', Organization, data, mandatory=True) @check_superuser def can_change(self, obj, data): @@ -1341,7 +1341,7 @@ class ExecutionEnvironmentAccess(BaseAccess): raise PermissionDenied if obj and obj.organization_id is None: raise PermissionDenied - if self.user not in obj.organization.execution_environment_admin_role and self.user not in obj.organization.admin_role: + if self.user not in obj.organization.execution_environment_admin_role: raise PermissionDenied org_pk = get_pk_from_dict(data, 'organization') if obj and obj.organization_id != org_pk: diff --git a/awx/main/migrations/0128_organiaztion_read_roles_ee_admin.py b/awx/main/migrations/0128_organiaztion_read_roles_ee_admin.py new file mode 100644 index 0000000000..f03a4e0ba2 --- /dev/null +++ b/awx/main/migrations/0128_organiaztion_read_roles_ee_admin.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.16 on 2021-02-18 22:57 + +import awx.main.fields +from django.db import migrations +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0127_reset_pod_spec_override'), + ] + + operations = [ + migrations.AlterField( + model_name='organization', + name='read_role', + field=awx.main.fields.ImplicitRoleField(editable=False, null='True', on_delete=django.db.models.deletion.CASCADE, parent_role=['member_role', 'auditor_role', 'execute_role', 'project_admin_role', 'inventory_admin_role', 'workflow_admin_role', 'notification_admin_role', 'credential_admin_role', 'job_template_admin_role', 'approval_role', 'execution_environment_admin_role'], related_name='+', to='main.Role'), + ), + ] diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index bdf1e38d7d..f0ecfea5c7 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -109,7 +109,8 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi 'execute_role', 'project_admin_role', 'inventory_admin_role', 'workflow_admin_role', 'notification_admin_role', 'credential_admin_role', - 'job_template_admin_role', 'approval_role',], + 'job_template_admin_role', 'approval_role', + 'execution_environment_admin_role',], ) approval_role = ImplicitRoleField( parent_role='admin_role', From 5b2adc89cfa85ec9e88aa961596f7ea9bddf53a1 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Fri, 19 Feb 2021 14:06:01 -0500 Subject: [PATCH 136/178] Make the managed_by_tower field read-only for EEs (similar to how we deal with it not being settable for Credentials) and add permissions checking for Org EE Admins. can_add: gets an explicit role to check against, `'execution_environment_admin_role'` can_change: leverages `self.check_related()` for the case where the Org is not changing, but also adds an explicit check for the EE Admin Role when the Org is changing to an explicit different Org. --- awx/api/serializers.py | 1 + awx/main/access.py | 32 +++++++++++++++++--------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 22b7ab3a7a..523171e43d 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1362,6 +1362,7 @@ class ProjectOptionsSerializer(BaseSerializer): class ExecutionEnvironmentSerializer(BaseSerializer): show_capabilities = ['edit', 'delete'] + managed_by_tower = serializers.ReadOnlyField() class Meta: model = ExecutionEnvironment diff --git a/awx/main/access.py b/awx/main/access.py index 9e9631751c..7da6709c41 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1329,29 +1329,31 @@ class ExecutionEnvironmentAccess(BaseAccess): Q(organization__isnull=True) ).distinct() - @check_superuser def can_add(self, data): if not data: # So the browseable API will work return Organization.accessible_objects(self.user, 'execution_environment_admin_role').exists() - return self.check_related('organization', Organization, data, mandatory=True) - - @check_superuser - def can_change(self, obj, data): - if obj.managed_by_tower is True: + if obj.managed_by_tower: raise PermissionDenied + if self.user.is_superuser: + return True + return self.check_related('organization', Organization, data, mandatory=True, + role_field='execution_environment_admin_role') + + def can_change(self, obj, data): + if obj.managed_by_tower: + raise PermissionDenied + if self.user.is_superuser: + return True if obj and obj.organization_id is None: raise PermissionDenied if self.user not in obj.organization.execution_environment_admin_role: raise PermissionDenied - org_pk = get_pk_from_dict(data, 'organization') - if obj and obj.organization_id != org_pk: - # Prevent moving an EE to a different organization, unless a superuser or admin on both orgs. - if obj.organization_id is None or org_pk is None: - raise PermissionDenied - if self.user not in Organization.objects.get(id=org_pk).execution_environment_admin_role: - raise PermissionDenied - - return True + if data and 'organization' in data: + new_org = get_object_from_data('organization', Organization, data, obj=obj) + if not new_org or self.user not in new_org.execution_environment_admin_role: + return False + return self.check_related('organization', Organization, data, obj=obj, mandatory=True, + role_field='execution_environment_admin_role') def can_delete(self, obj): return self.can_change(obj, None) From b417fc38032809ce5155a6a746c22cfe527b609d Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Mon, 22 Feb 2021 17:07:43 -0500 Subject: [PATCH 137/178] Turn off permissions check bypassing for admins when hitting the execution environment list and detail views. --- awx/api/views/__init__.py | 2 ++ awx/main/access.py | 5 +---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 01ad6d98ed..855d9e54e0 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -688,6 +688,7 @@ class TeamAccessList(ResourceAccessList): class ExecutionEnvironmentList(ListCreateAPIView): + always_allow_superuser = False model = models.ExecutionEnvironment serializer_class = serializers.ExecutionEnvironmentSerializer swagger_topic = "Execution Environments" @@ -695,6 +696,7 @@ class ExecutionEnvironmentList(ListCreateAPIView): class ExecutionEnvironmentDetail(RetrieveUpdateDestroyAPIView): + always_allow_superuser = False model = models.ExecutionEnvironment serializer_class = serializers.ExecutionEnvironmentSerializer swagger_topic = "Execution Environments" diff --git a/awx/main/access.py b/awx/main/access.py index 7da6709c41..d2a2aa9f3b 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1329,13 +1329,10 @@ class ExecutionEnvironmentAccess(BaseAccess): Q(organization__isnull=True) ).distinct() + @check_superuser def can_add(self, data): if not data: # So the browseable API will work return Organization.accessible_objects(self.user, 'execution_environment_admin_role').exists() - if obj.managed_by_tower: - raise PermissionDenied - if self.user.is_superuser: - return True return self.check_related('organization', Organization, data, mandatory=True, role_field='execution_environment_admin_role') From 86a3a79be4fb603089ab2224d3840626f8314ea0 Mon Sep 17 00:00:00 2001 From: beeankha Date: Tue, 16 Feb 2021 17:40:53 -0500 Subject: [PATCH 138/178] Enable utilized EE Collections name and version info to be detected --- .../0129_unifiedjob_installed_collections.py | 19 +++++++++++++++++++ awx/main/models/unified_jobs.py | 8 +++++++- awx/main/tasks.py | 8 +++++++- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 awx/main/migrations/0129_unifiedjob_installed_collections.py diff --git a/awx/main/migrations/0129_unifiedjob_installed_collections.py b/awx/main/migrations/0129_unifiedjob_installed_collections.py new file mode 100644 index 0000000000..897708a631 --- /dev/null +++ b/awx/main/migrations/0129_unifiedjob_installed_collections.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.16 on 2021-02-16 20:27 + +import awx.main.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0128_organiaztion_read_roles_ee_admin'), + ] + + operations = [ + migrations.AddField( + model_name='unifiedjob', + name='installed_collections', + field=awx.main.fields.JSONBField(blank=True, default=dict, editable=False, help_text='The Collections names and versions installed in the execution environment.'), + ), + ] diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index cf22430f7b..45d7739ee3 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -50,7 +50,7 @@ from awx.main.utils import ( from awx.main.constants import ACTIVE_STATES, CAN_CANCEL from awx.main.redact import UriCleaner, REPLACE_STR from awx.main.consumers import emit_channel_notification -from awx.main.fields import JSONField, AskForField, OrderedManyToManyField +from awx.main.fields import JSONField, JSONBField, AskForField, OrderedManyToManyField __all__ = ['UnifiedJobTemplate', 'UnifiedJob', 'StdoutMaxBytesExceeded'] @@ -722,6 +722,12 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique 'Credential', related_name='%(class)ss', ) + installed_collections = JSONBField( + blank=True, + default=dict, + editable=False, + help_text=_("The Collections names and versions installed in the execution environment."), + ) def get_absolute_url(self, request=None): RealClass = self.get_real_instance_class() diff --git a/awx/main/tasks.py b/awx/main/tasks.py index ab9484eba9..bdfcf18c90 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1179,11 +1179,17 @@ class BaseTask(object): instance.log_lifecycle("finalize_run") job_profiling_dir = os.path.join(private_data_dir, 'artifacts/playbook_profiling') awx_profiling_dir = '/var/log/tower/playbook_profiling/' + collections_info = os.path.join(private_data_dir, 'artifacts/') + if not os.path.exists(awx_profiling_dir): os.mkdir(awx_profiling_dir) if os.path.isdir(job_profiling_dir): shutil.copytree(job_profiling_dir, os.path.join(awx_profiling_dir, str(instance.pk))) - + if os.path.exists(collections_info): + with open(collections_info + 'collections.json') as ee_json_info: + ee_collections_info = ee_json_info.read() + instance.installed_collections = ee_collections_info + instance.save(update_fields=['installed_collections']) def event_handler(self, event_data): # From cb95de0862d84d1055b298bfc67606a0a3069b4d Mon Sep 17 00:00:00 2001 From: beeankha Date: Thu, 18 Feb 2021 15:54:07 -0500 Subject: [PATCH 139/178] Assign entire file path to variable --- awx/main/tasks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index bdfcf18c90..55074683d0 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1179,14 +1179,14 @@ class BaseTask(object): instance.log_lifecycle("finalize_run") job_profiling_dir = os.path.join(private_data_dir, 'artifacts/playbook_profiling') awx_profiling_dir = '/var/log/tower/playbook_profiling/' - collections_info = os.path.join(private_data_dir, 'artifacts/') + collections_info = os.path.join(private_data_dir, 'artifacts/', 'collections.json') if not os.path.exists(awx_profiling_dir): os.mkdir(awx_profiling_dir) if os.path.isdir(job_profiling_dir): shutil.copytree(job_profiling_dir, os.path.join(awx_profiling_dir, str(instance.pk))) if os.path.exists(collections_info): - with open(collections_info + 'collections.json') as ee_json_info: + with open(collections_info) as ee_json_info: ee_collections_info = ee_json_info.read() instance.installed_collections = ee_collections_info instance.save(update_fields=['installed_collections']) From 0e80f663ab47528d154474eccb9c390deeef82ec Mon Sep 17 00:00:00 2001 From: beeankha Date: Mon, 22 Feb 2021 16:02:15 -0500 Subject: [PATCH 140/178] Add installed_collections column to unified job query --- awx/main/analytics/collectors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/analytics/collectors.py b/awx/main/analytics/collectors.py index b0ac43cc65..58b78c83a6 100644 --- a/awx/main/analytics/collectors.py +++ b/awx/main/analytics/collectors.py @@ -334,7 +334,8 @@ def unified_jobs_table(since, full_path, until, **kwargs): main_unifiedjob.finished, main_unifiedjob.elapsed, main_unifiedjob.job_explanation, - main_unifiedjob.instance_group_id + main_unifiedjob.instance_group_id, + main_unifiedjob.installed_collections FROM main_unifiedjob JOIN django_content_type ON main_unifiedjob.polymorphic_ctype_id = django_content_type.id LEFT JOIN main_job ON main_unifiedjob.id = main_job.unifiedjob_ptr_id From 5b17ab6873d806e518d88458aafe7f9c0a5bb735 Mon Sep 17 00:00:00 2001 From: beeankha Date: Tue, 23 Feb 2021 11:54:30 -0500 Subject: [PATCH 141/178] Enable EE collections info to be loaded as valid JSON vs stringified JSON --- awx/main/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 55074683d0..10e07312c1 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1187,7 +1187,7 @@ class BaseTask(object): shutil.copytree(job_profiling_dir, os.path.join(awx_profiling_dir, str(instance.pk))) if os.path.exists(collections_info): with open(collections_info) as ee_json_info: - ee_collections_info = ee_json_info.read() + ee_collections_info = json.loads(ee_json_info.read()) instance.installed_collections = ee_collections_info instance.save(update_fields=['installed_collections']) From 60827143bbfe2e72faf8a37c74de1d795697e8eb Mon Sep 17 00:00:00 2001 From: beeankha Date: Tue, 23 Feb 2021 12:50:05 -0500 Subject: [PATCH 142/178] Bump up unified_jobs_table version for new column addition --- awx/main/analytics/collectors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/analytics/collectors.py b/awx/main/analytics/collectors.py index 58b78c83a6..89bc28ea56 100644 --- a/awx/main/analytics/collectors.py +++ b/awx/main/analytics/collectors.py @@ -311,7 +311,7 @@ def events_table(since, full_path, until, **kwargs): return _copy_table(table='events', query=events_query, path=full_path) -@register('unified_jobs_table', '1.1', format='csv', description=_('Data on jobs run'), expensive=True) +@register('unified_jobs_table', '1.2', format='csv', description=_('Data on jobs run'), expensive=True) def unified_jobs_table(since, full_path, until, **kwargs): unified_job_query = '''COPY (SELECT main_unifiedjob.id, main_unifiedjob.polymorphic_ctype_id, From 883fa4906aea59dd28d43ec80ed47c8bb03d0de2 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Tue, 23 Feb 2021 16:25:26 -0500 Subject: [PATCH 143/178] Fix receptor.conf path in dev env --- .../ansible/roles/sources/templates/docker-compose.yml.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 index 00f136ef49..1d0799c7b9 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 @@ -33,10 +33,10 @@ services: - "../../docker-compose/_sources/websocket_secret.py:/etc/tower/conf.d/websocket_secret.py" - "../../docker-compose/_sources/local_settings.py:/etc/tower/conf.d/local_settings.py" - "../../docker-compose/_sources/SECRET_KEY:/etc/tower/SECRET_KEY" + - "../../docker-compose/receptor.conf:/etc/receptor/receptor.conf" - "redis_socket:/var/run/redis/:rw" - "receptor:/var/run/receptor/" - "/sys/fs/cgroup:/sys/fs/cgroup" - - "./docker-compose/receptor.conf:/etc/receptor/receptor.conf" - "~/.kube/config:/var/lib/awx/.kube/config" privileged: true tty: true From befc658042a755164ff4fb37633caa915c864e02 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Tue, 23 Feb 2021 16:48:50 -0500 Subject: [PATCH 144/178] Wire up --pull option for EEs --- awx/main/tasks.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 10e07312c1..0f02f3a507 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -909,6 +909,11 @@ class BaseTask(object): "process_isolation": True, "container_options": ['--user=root'], } + + pull = instance.execution_environment.pull + if pull: + params['container_options'].append(f'--pull={pull}') + if settings.AWX_PROOT_SHOW_PATHS: params['container_volume_mounts'] = [] for this_path in settings.AWX_PROOT_SHOW_PATHS: From f2801e0c034773dc7f53ad1c854e7d4b50fb9551 Mon Sep 17 00:00:00 2001 From: nixocio Date: Tue, 23 Feb 2021 19:55:29 -0500 Subject: [PATCH 145/178] Minor update EE tables * Add table header `actions` * Add `name` as default search See: https://github.com/ansible/awx/issues/7884 Also: https://github.com/ansible/awx/issues/9087 --- .../ExecutionEnvironmentList/ExecutionEnvironmentList.jsx | 7 ++++++- .../ExecutionEnvironmentListItem.jsx | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx index 9d94085520..312b18f2cf 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx @@ -121,10 +121,14 @@ function ExecutionEnvironmentList({ i18n }) { toolbarSearchableKeys={searchableKeys} toolbarRelatedSearchableKeys={relatedSearchableKeys} toolbarSearchColumns={[ + { + name: i18n._(t`Name`), + key: 'name__icontains', + isDefault: true, + }, { name: i18n._(t`Image`), key: 'image__icontains', - isDefault: true, }, ]} toolbarSortColumns={[ @@ -150,6 +154,7 @@ function ExecutionEnvironmentList({ i18n }) { {i18n._(t`Name`)} {i18n._(t`Image`)} {i18n._(t`Organization`)} + {i18n._(t`Actions`)} } renderToolbar={props => ( diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx index fbc8387d84..bb814a1921 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx @@ -50,7 +50,7 @@ function ExecutionEnvironmentListItem({ i18n._(t`Globally Available`) )} - + Date: Wed, 10 Feb 2021 16:52:23 -0500 Subject: [PATCH 146/178] adding needed url endpoint for copy functionality and the beginning of some testing that can be fleshed out more fully in later work --- awx/api/urls/execution_environments.py | 2 ++ awx/api/views/__init__.py | 6 ++++++ awx/main/models/execution_environments.py | 17 +++++++++++++++++ awx/main/tests/functional/test_rbac_team.py | 8 +++++++- awx/main/tests/functional/test_rbac_user.py | 2 +- .../awxkit/api/pages/execution_environments.py | 4 ++-- 6 files changed, 35 insertions(+), 4 deletions(-) diff --git a/awx/api/urls/execution_environments.py b/awx/api/urls/execution_environments.py index 08f852be08..99b9cb3ddc 100644 --- a/awx/api/urls/execution_environments.py +++ b/awx/api/urls/execution_environments.py @@ -4,6 +4,7 @@ from awx.api.views import ( ExecutionEnvironmentList, ExecutionEnvironmentDetail, ExecutionEnvironmentJobTemplateList, + ExecutionEnvironmentCopy, ExecutionEnvironmentActivityStreamList, ) @@ -12,6 +13,7 @@ urls = [ url(r'^$', ExecutionEnvironmentList.as_view(), name='execution_environment_list'), url(r'^(?P[0-9]+)/$', ExecutionEnvironmentDetail.as_view(), name='execution_environment_detail'), url(r'^(?P[0-9]+)/unified_job_templates/$', ExecutionEnvironmentJobTemplateList.as_view(), name='execution_environment_job_template_list'), + url(r'^(?P[0-9]+)/copy/$', ExecutionEnvironmentCopy.as_view(), name='execution_environment_copy'), url(r'^(?P[0-9]+)/activity_stream/$', ExecutionEnvironmentActivityStreamList.as_view(), name='execution_environment_activity_stream_list'), ] diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 855d9e54e0..2d29519de8 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -710,6 +710,12 @@ class ExecutionEnvironmentJobTemplateList(SubListAPIView): relationship = 'unifiedjobtemplates' +class ExecutionEnvironmentCopy(CopyAPIView): + + model = models.ExecutionEnvironment + copy_return_serializer_class = serializers.ExecutionEnvironmentSerializer + + class ExecutionEnvironmentActivityStreamList(SubListAPIView): model = models.ActivityStream diff --git a/awx/main/models/execution_environments.py b/awx/main/models/execution_environments.py index eabd0cce7c..b0fd2bb857 100644 --- a/awx/main/models/execution_environments.py +++ b/awx/main/models/execution_environments.py @@ -3,6 +3,7 @@ from django.utils.translation import ugettext_lazy as _ from awx.api.versioning import reverse from awx.main.models.base import CommonModel +from awx.main.utils import copy_model_by_class, copy_m2m_relationships __all__ = ['ExecutionEnvironment'] @@ -49,5 +50,21 @@ class ExecutionEnvironment(CommonModel): help_text=_('Pull image before running?'), ) + def copy_execution_environment(self): + ''' + Returns saved object, including related fields. + Create a copy of this unified job template. + ''' + execution_environment_class = self.__class__ + fields = (f.name for f in self.Meta.fields) + execution_environment_copy = copy_model_by_class(self, execution_environment_class, fields, {}) + + time_now = now() + execution_environment_copy.name = execution_environment_copy.name.split('@', 1)[0] + ' @ ' + time_now.strftime('%I:%M:%S %p') + + execution_environment_copy.save() + copy_m2m_relationships(self, execution_environment_copy, fields) + return execution_environment_copy + def get_absolute_url(self, request=None): return reverse('api:execution_environment_detail', kwargs={'pk': self.pk}, request=request) diff --git a/awx/main/tests/functional/test_rbac_team.py b/awx/main/tests/functional/test_rbac_team.py index a18a69a94b..ed76e7e4a8 100644 --- a/awx/main/tests/functional/test_rbac_team.py +++ b/awx/main/tests/functional/test_rbac_team.py @@ -2,7 +2,7 @@ import pytest from unittest import mock from awx.main.access import TeamAccess -from awx.main.models import Project, Organization, Team +from awx.main.models import Project, Organization, Team, ExecutionEnvironment @pytest.mark.django_db @@ -143,6 +143,12 @@ def test_team_member_org_role_access_inventory(team, rando, inventory, organizat team.member_role.children.add(organization.inventory_admin_role) assert rando in inventory.admin_role +# @pytest.mark.django_db +# def test_team_member_org_role_access_execution_environment(team, rando, execution_environment, organization): +# team.member_role.members.add(rando) +# assert rando not in execution_environment.read_role +# team.member_role.children.add(organization.execution_environment_admin_role) +# assert rando in execution_environment.admin_role @pytest.mark.django_db def test_org_admin_team_access(organization, team, user, project): diff --git a/awx/main/tests/functional/test_rbac_user.py b/awx/main/tests/functional/test_rbac_user.py index b62a0db25f..376272bdfe 100644 --- a/awx/main/tests/functional/test_rbac_user.py +++ b/awx/main/tests/functional/test_rbac_user.py @@ -4,7 +4,7 @@ from unittest import mock from django.test import TransactionTestCase from awx.main.access import UserAccess, RoleAccess, TeamAccess -from awx.main.models import User, Organization, Inventory, Role +from awx.main.models import User, Organization, Inventory, Role, ExecutionEnvironment class TestSysAuditorTransactional(TransactionTestCase): diff --git a/awxkit/awxkit/api/pages/execution_environments.py b/awxkit/awxkit/api/pages/execution_environments.py index 94f53c1094..0471b1f1d3 100644 --- a/awxkit/awxkit/api/pages/execution_environments.py +++ b/awxkit/awxkit/api/pages/execution_environments.py @@ -1,6 +1,6 @@ import logging -from awxkit.api.mixins import DSAdapter, HasCreate +from awxkit.api.mixins import DSAdapter, HasCreate, HasCopy from awxkit.api.pages import ( Credential, Organization, @@ -15,7 +15,7 @@ from . import page log = logging.getLogger(__name__) -class ExecutionEnvironment(HasCreate, base.Base): +class ExecutionEnvironment(HasCreate, HasCopy, base.Base): dependencies = [Organization, Credential] NATURAL_KEY = ('name',) From 4d2fcfd8c1b9678e1b9d6a9e317e28d0083474e8 Mon Sep 17 00:00:00 2001 From: Rebeccah Date: Fri, 12 Feb 2021 15:55:32 -0500 Subject: [PATCH 147/178] add a functional test for creating an EE, remove bum copy function because it's not needed, copy works from the base class moved AWXKit pull additions to separate PR and made some changes that were causing linting errors in tests and add copy to show_capabilities for the ee serializer --- awx/api/serializers.py | 2 +- awx/main/models/execution_environments.py | 17 ----------------- awx/main/tests/functional/conftest.py | 6 ++++++ .../functional/test_execution_environments.py | 19 +++++++++++++++++++ awx/main/tests/functional/test_rbac_team.py | 8 +------- awx/main/tests/functional/test_rbac_user.py | 2 +- 6 files changed, 28 insertions(+), 26 deletions(-) create mode 100644 awx/main/tests/functional/test_execution_environments.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 523171e43d..dce312a5a8 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1361,7 +1361,7 @@ class ProjectOptionsSerializer(BaseSerializer): class ExecutionEnvironmentSerializer(BaseSerializer): - show_capabilities = ['edit', 'delete'] + show_capabilities = ['edit', 'delete', 'copy'] managed_by_tower = serializers.ReadOnlyField() class Meta: diff --git a/awx/main/models/execution_environments.py b/awx/main/models/execution_environments.py index b0fd2bb857..eabd0cce7c 100644 --- a/awx/main/models/execution_environments.py +++ b/awx/main/models/execution_environments.py @@ -3,7 +3,6 @@ from django.utils.translation import ugettext_lazy as _ from awx.api.versioning import reverse from awx.main.models.base import CommonModel -from awx.main.utils import copy_model_by_class, copy_m2m_relationships __all__ = ['ExecutionEnvironment'] @@ -50,21 +49,5 @@ class ExecutionEnvironment(CommonModel): help_text=_('Pull image before running?'), ) - def copy_execution_environment(self): - ''' - Returns saved object, including related fields. - Create a copy of this unified job template. - ''' - execution_environment_class = self.__class__ - fields = (f.name for f in self.Meta.fields) - execution_environment_copy = copy_model_by_class(self, execution_environment_class, fields, {}) - - time_now = now() - execution_environment_copy.name = execution_environment_copy.name.split('@', 1)[0] + ' @ ' + time_now.strftime('%I:%M:%S %p') - - execution_environment_copy.save() - copy_m2m_relationships(self, execution_environment_copy, fields) - return execution_environment_copy - def get_absolute_url(self, request=None): return reverse('api:execution_environment_detail', kwargs={'pk': self.pk}, request=request) diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 7111950003..4cbd5a40d3 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -52,6 +52,7 @@ from awx.main.models.events import ( from awx.main.models.workflow import WorkflowJobTemplate from awx.main.models.ad_hoc_commands import AdHocCommand from awx.main.models.oauth import OAuth2Application as Application +from awx.main.models.execution_environments import ExecutionEnvironment __SWAGGER_REQUESTS__ = {} @@ -850,3 +851,8 @@ def slice_job_factory(slice_jt_factory): node.save() return slice_job return r + + +@pytest.fixture +def execution_environment(organization): + return ExecutionEnvironment.objects.create(name="test-ee", description="test-ee", organization=organization) diff --git a/awx/main/tests/functional/test_execution_environments.py b/awx/main/tests/functional/test_execution_environments.py new file mode 100644 index 0000000000..5f1e430fe8 --- /dev/null +++ b/awx/main/tests/functional/test_execution_environments.py @@ -0,0 +1,19 @@ +import pytest + +from awx.main.models import (ExecutionEnvironment) + + +@pytest.mark.django_db +def test_execution_environment_creation(execution_environment, organization): + execution_env = ExecutionEnvironment.objects.create( + name='Hello Environment', + image='', + organization=organization, + managed_by_tower=False, + credential=None, + pull='missing' + ) + assert type(execution_env) is type(execution_environment) + assert execution_env.organization == organization + assert execution_env.name == 'Hello Environment' + assert execution_env.pull == 'missing' diff --git a/awx/main/tests/functional/test_rbac_team.py b/awx/main/tests/functional/test_rbac_team.py index ed76e7e4a8..a18a69a94b 100644 --- a/awx/main/tests/functional/test_rbac_team.py +++ b/awx/main/tests/functional/test_rbac_team.py @@ -2,7 +2,7 @@ import pytest from unittest import mock from awx.main.access import TeamAccess -from awx.main.models import Project, Organization, Team, ExecutionEnvironment +from awx.main.models import Project, Organization, Team @pytest.mark.django_db @@ -143,12 +143,6 @@ def test_team_member_org_role_access_inventory(team, rando, inventory, organizat team.member_role.children.add(organization.inventory_admin_role) assert rando in inventory.admin_role -# @pytest.mark.django_db -# def test_team_member_org_role_access_execution_environment(team, rando, execution_environment, organization): -# team.member_role.members.add(rando) -# assert rando not in execution_environment.read_role -# team.member_role.children.add(organization.execution_environment_admin_role) -# assert rando in execution_environment.admin_role @pytest.mark.django_db def test_org_admin_team_access(organization, team, user, project): diff --git a/awx/main/tests/functional/test_rbac_user.py b/awx/main/tests/functional/test_rbac_user.py index 376272bdfe..b62a0db25f 100644 --- a/awx/main/tests/functional/test_rbac_user.py +++ b/awx/main/tests/functional/test_rbac_user.py @@ -4,7 +4,7 @@ from unittest import mock from django.test import TransactionTestCase from awx.main.access import UserAccess, RoleAccess, TeamAccess -from awx.main.models import User, Organization, Inventory, Role, ExecutionEnvironment +from awx.main.models import User, Organization, Inventory, Role class TestSysAuditorTransactional(TransactionTestCase): From adf708366a112f502551233afa5071b475947796 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Tue, 23 Feb 2021 18:47:47 -0500 Subject: [PATCH 148/178] Add "copy" to EE related links --- awx/api/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index dce312a5a8..6f8dcd3fad 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1373,6 +1373,7 @@ class ExecutionEnvironmentSerializer(BaseSerializer): res.update( activity_stream=self.reverse('api:execution_environment_activity_stream_list', kwargs={'pk': obj.pk}), unified_job_templates=self.reverse('api:execution_environment_job_template_list', kwargs={'pk': obj.pk}), + copy=self.reverse('api:execution_environment_copy', kwargs={'pk': obj.pk}), ) if obj.organization: res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) From 6e67ae68fd416ee63a715e69472bc7f9122aa8c2 Mon Sep 17 00:00:00 2001 From: nixocio Date: Tue, 9 Feb 2021 17:23:47 -0500 Subject: [PATCH 149/178] Add Execution Environments into a few screens Add EE to the following screens: * Job Template * Organization * Project * Workflow Job Template Also, add a new lookup component - ExecutionEnvironmentLoookup. See: https://github.com/ansible/awx/issues/9189 --- .../Lookup/ExecutionEnvironmentLookup.jsx | 167 ++++++++++++++++++ .../ExecutionEnvironmentLookup.test.jsx | 76 ++++++++ awx/ui_next/src/components/Lookup/index.js | 1 + .../OrganizationAdd/OrganizationAdd.jsx | 5 +- .../OrganizationAdd/OrganizationAdd.test.jsx | 7 +- .../OrganizationDetail/OrganizationDetail.jsx | 6 + .../OrganizationDetail.test.jsx | 11 +- .../OrganizationEdit/OrganizationEdit.jsx | 5 +- .../OrganizationEdit.test.jsx | 8 + .../Organization/shared/OrganizationForm.jsx | 35 +++- .../shared/OrganizationForm.test.jsx | 20 ++- .../screens/Project/ProjectAdd/ProjectAdd.jsx | 1 + .../Project/ProjectAdd/ProjectAdd.test.jsx | 8 +- .../Project/ProjectDetail/ProjectDetail.jsx | 9 + .../ProjectDetail/ProjectDetail.test.jsx | 12 +- .../Project/ProjectEdit/ProjectEdit.jsx | 1 + .../screens/Project/shared/ProjectForm.jsx | 30 ++++ .../JobTemplateAdd/JobTemplateAdd.jsx | 5 +- .../JobTemplateAdd/JobTemplateAdd.test.jsx | 17 +- .../JobTemplateDetail/JobTemplateDetail.jsx | 6 + .../JobTemplateDetail.test.jsx | 10 ++ .../JobTemplateEdit/JobTemplateEdit.jsx | 5 +- .../WorkflowJobTemplateAdd.jsx | 5 +- .../WorkflowJobTemplateDetail.jsx | 6 + .../WorkflowJobTemplateDetail.test.jsx | 12 ++ .../WorkflowJobTemplateEdit.jsx | 7 +- .../Template/shared/JobTemplateForm.jsx | 68 ++++++- .../shared/WorkflowJobTemplateForm.jsx | 29 ++- .../Template/shared/data.job_template.json | 9 +- awx/ui_next/src/types.js | 1 + 30 files changed, 558 insertions(+), 24 deletions(-) create mode 100644 awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.jsx create mode 100644 awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.test.jsx diff --git a/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.jsx b/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.jsx new file mode 100644 index 0000000000..ecab4f3b44 --- /dev/null +++ b/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.jsx @@ -0,0 +1,167 @@ +import React, { useCallback, useEffect } from 'react'; +import { string, func, bool } from 'prop-types'; +import { withRouter, useLocation } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { FormGroup, Tooltip } from '@patternfly/react-core'; + +import { ExecutionEnvironmentsAPI } from '../../api'; +import { ExecutionEnvironment } from '../../types'; +import { getQSConfig, parseQueryString, mergeParams } from '../../util/qs'; +import Popover from '../Popover'; +import OptionsList from '../OptionsList'; +import useRequest from '../../util/useRequest'; + +import Lookup from './Lookup'; +import LookupErrorMessage from './shared/LookupErrorMessage'; + +const QS_CONFIG = getQSConfig('execution_environments', { + page: 1, + page_size: 5, + order_by: 'name', +}); + +function ExecutionEnvironmentLookup({ + globallyAvailable, + i18n, + isDefaultEnvironment, + isDisabled, + onChange, + organizationId, + popoverContent, + tooltip, + value, + onBlur, +}) { + const location = useLocation(); + + const { + result: { + executionEnvironments, + count, + relatedSearchableKeys, + searchableKeys, + }, + request: fetchExecutionEnvironments, + error, + isLoading, + } = useRequest( + useCallback(async () => { + const params = parseQueryString(QS_CONFIG, location.search); + const globallyAvailableParams = globallyAvailable + ? { or__organization__isnull: 'True' } + : {}; + const organizationIdParams = organizationId + ? { or__organization__id: organizationId } + : {}; + const [{ data }, actionsResponse] = await Promise.all([ + ExecutionEnvironmentsAPI.read( + mergeParams(params, { + ...globallyAvailableParams, + ...organizationIdParams, + }) + ), + ExecutionEnvironmentsAPI.readOptions(), + ]); + return { + executionEnvironments: data.results, + count: data.count, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), + }; + }, [location, globallyAvailable, organizationId]), + { + executionEnvironments: [], + count: 0, + relatedSearchableKeys: [], + searchableKeys: [], + } + ); + + useEffect(() => { + fetchExecutionEnvironments(); + }, [fetchExecutionEnvironments]); + + const renderLookup = () => ( + <> + ( + dispatch({ type: 'SELECT_ITEM', item })} + deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })} + /> + )} + /> + + ); + + return ( + } + > + {isDisabled ? ( + {renderLookup()} + ) : ( + renderLookup() + )} + + + + ); +} + +ExecutionEnvironmentLookup.propTypes = { + value: ExecutionEnvironment, + popoverContent: string, + onChange: func.isRequired, + isDefaultEnvironment: bool, +}; + +ExecutionEnvironmentLookup.defaultProps = { + popoverContent: '', + isDefaultEnvironment: false, + value: null, +}; + +export default withI18n()(withRouter(ExecutionEnvironmentLookup)); diff --git a/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.test.jsx b/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.test.jsx new file mode 100644 index 0000000000..783d43707b --- /dev/null +++ b/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.test.jsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; +import ExecutionEnvironmentLookup from './ExecutionEnvironmentLookup'; +import { ExecutionEnvironmentsAPI } from '../../api'; + +jest.mock('../../api'); + +const mockedExecutionEnvironments = { + count: 1, + results: [ + { + id: 2, + name: 'Foo', + image: 'quay.io/ansible/awx-ee', + pull: 'missing', + }, + ], +}; + +const executionEnvironment = { + id: 42, + name: 'Bar', + image: 'quay.io/ansible/bar', + pull: 'missing', +}; + +describe('ExecutionEnvironmentLookup', () => { + let wrapper; + + beforeEach(() => { + ExecutionEnvironmentsAPI.read.mockResolvedValue( + mockedExecutionEnvironments + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + wrapper.unmount(); + }); + + test('should render successfully', async () => { + ExecutionEnvironmentsAPI.readOptions.mockReturnValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); + await act(async () => { + wrapper = mountWithContexts( + {}} + /> + ); + }); + wrapper.update(); + expect(ExecutionEnvironmentsAPI.read).toHaveBeenCalledTimes(1); + expect(wrapper.find('ExecutionEnvironmentLookup')).toHaveLength(1); + }); + + test('should fetch execution environments', async () => { + await act(async () => { + wrapper = mountWithContexts( + {}} + /> + ); + }); + expect(ExecutionEnvironmentsAPI.read).toHaveBeenCalledTimes(1); + }); +}); diff --git a/awx/ui_next/src/components/Lookup/index.js b/awx/ui_next/src/components/Lookup/index.js index a2fcfbe570..7c8b6845b1 100644 --- a/awx/ui_next/src/components/Lookup/index.js +++ b/awx/ui_next/src/components/Lookup/index.js @@ -7,3 +7,4 @@ export { default as CredentialLookup } from './CredentialLookup'; export { default as ApplicationLookup } from './ApplicationLookup'; export { default as HostFilterLookup } from './HostFilterLookup'; export { default as OrganizationLookup } from './OrganizationLookup'; +export { default as ExecutionEnvironmentLookup } from './ExecutionEnvironmentLookup'; diff --git a/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.jsx b/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.jsx index d9c14765ac..adbe04820d 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.jsx @@ -13,7 +13,10 @@ function OrganizationAdd() { const handleSubmit = async (values, groupsToAssociate) => { try { - const { data: response } = await OrganizationsAPI.create(values); + const { data: response } = await OrganizationsAPI.create({ + ...values, + default_environment: values.default_environment?.id, + }); await Promise.all( groupsToAssociate .map(id => OrganizationsAPI.associateInstanceGroup(response.id, id)) diff --git a/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx b/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx index 8fa4e2cbc2..d99634ea09 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx @@ -17,13 +17,18 @@ describe('', () => { description: 'new description', custom_virtualenv: 'Buzz', galaxy_credentials: [], + default_environment: { id: 1, name: 'Foo' }, }; OrganizationsAPI.create.mockResolvedValueOnce({ data: {} }); await act(async () => { const wrapper = mountWithContexts(); wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, []); }); - expect(OrganizationsAPI.create).toHaveBeenCalledWith(updatedOrgData); + expect(OrganizationsAPI.create).toHaveBeenCalledWith({ + ...updatedOrgData, + default_environment: 1, + }); + expect(OrganizationsAPI.create).toHaveBeenCalledTimes(1); }); test('should navigate to organizations list when cancel is clicked', async () => { diff --git a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx index 6b55780333..a1abee28a8 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx @@ -94,6 +94,12 @@ function OrganizationDetail({ i18n, organization }) { label={i18n._(t`Ansible Environment`)} value={custom_virtualenv} /> + {summary_fields?.default_environment?.name && ( + + )} ', () => { const mockOrganization = { + id: 12, name: 'Foo', description: 'Bar', custom_virtualenv: 'Fizz', @@ -24,7 +25,14 @@ describe('', () => { edit: true, delete: true, }, + default_environment: { + id: 1, + name: 'Default EE', + description: '', + image: 'quay.io/ansible/awx-ee', + }, }, + default_environment: 1, }; const mockInstanceGroups = { data: { @@ -43,7 +51,7 @@ describe('', () => { jest.clearAllMocks(); }); - test('initially renders succesfully', async () => { + test('initially renders successfully', async () => { await act(async () => { mountWithContexts(); }); @@ -86,6 +94,7 @@ describe('', () => { { label: 'Created', value: '7/7/2015, 5:21:26 PM' }, { label: 'Last Modified', value: '8/11/2019, 7:47:37 PM' }, { label: 'Max Hosts', value: '0' }, + { label: 'Default Execution Environment', value: 'Default EE' }, ]; for (let i = 0; i < testParams.length; i++) { const { label, value } = testParams[i]; diff --git a/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.jsx b/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.jsx index 849a273ef5..3297d2fd6f 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.jsx @@ -28,7 +28,10 @@ function OrganizationEdit({ organization }) { const addedCredentialIds = addedCredentials.map(({ id }) => id); const removedCredentialIds = removedCredentials.map(({ id }) => id); - await OrganizationsAPI.update(organization.id, values); + await OrganizationsAPI.update(organization.id, { + ...values, + default_environment: values.default_environment?.id || null, + }); await Promise.all( groupsToAssociate .map(id => diff --git a/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.test.jsx b/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.test.jsx index ea62e38c9e..5556ee05d5 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.test.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.test.jsx @@ -19,6 +19,13 @@ describe('', () => { related: { instance_groups: '/api/v2/organizations/1/instance_groups', }, + default_environment: 1, + summary_fields: { + default_environment: { + id: 1, + name: 'Baz', + }, + }, }; test('onSubmit should call api update', async () => { @@ -31,6 +38,7 @@ describe('', () => { name: 'new name', description: 'new description', custom_virtualenv: 'Buzz', + default_environment: null, }; wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, [], []); diff --git a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx index 094e6ac5b6..eb46f8c5cc 100644 --- a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx +++ b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx @@ -12,16 +12,21 @@ import ContentError from '../../../components/ContentError'; import ContentLoading from '../../../components/ContentLoading'; import FormField, { FormSubmitError } from '../../../components/FormField'; import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup'; -import { InstanceGroupsLookup } from '../../../components/Lookup'; +import { + InstanceGroupsLookup, + ExecutionEnvironmentLookup, +} from '../../../components/Lookup'; import { getAddedAndRemoved } from '../../../util/lists'; import { required, minMaxValue } from '../../../util/validators'; import { FormColumnLayout } from '../../../components/FormLayout'; import CredentialLookup from '../../../components/Lookup/CredentialLookup'; function OrganizationFormFields({ i18n, instanceGroups, setInstanceGroups }) { + const { license_info = {}, me = {} } = useConfig(); + const { custom_virtualenvs } = useContext(ConfigContext); + const { setFieldValue } = useFormikContext(); const [venvField] = useField('custom_virtualenv'); - const { license_info = {}, me = {} } = useConfig(); const [ galaxyCredentialsField, @@ -29,12 +34,19 @@ function OrganizationFormFields({ i18n, instanceGroups, setInstanceGroups }) { galaxyCredentialsHelpers, ] = useField('galaxy_credentials'); + const [ + executionEnvironmentField, + executionEnvironmentMeta, + executionEnvironmentHelpers, + ] = useField({ + name: 'default_environment', + }); + const defaultVenv = { label: i18n._(t`Use Default Ansible Environment`), value: '/var/lib/awx/venv/ansible/', key: 'default', }; - const { custom_virtualenvs } = useContext(ConfigContext); const handleCredentialUpdate = useCallback( value => { @@ -100,6 +112,20 @@ function OrganizationFormFields({ i18n, instanceGroups, setInstanceGroups }) { t`Select the Instance Groups for this Organization to run on.` )} /> + executionEnvironmentHelpers.setTouched()} + value={executionEnvironmentField.value} + onChange={value => executionEnvironmentHelpers.setValue(value)} + popoverContent={i18n._( + t`Select the default execution environment for this organization.` + )} + globallyAvailable + isDefaultEnvironment + /> @@ -221,6 +249,7 @@ OrganizationForm.defaultProps = { description: '', max_hosts: '0', custom_virtualenv: '', + default_environment: '', }, submitError: null, }; diff --git a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx index 67cf0a60d6..7dfbca620c 100644 --- a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx +++ b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx @@ -4,7 +4,7 @@ import { mountWithContexts, waitForElement, } from '../../../../testUtils/enzymeHelpers'; -import { OrganizationsAPI } from '../../../api'; +import { OrganizationsAPI, ExecutionEnvironmentsAPI } from '../../../api'; import OrganizationForm from './OrganizationForm'; @@ -32,6 +32,8 @@ describe('', () => { { name: 'Two', id: 2 }, ]; + const mockExecutionEnvironment = [{ name: 'EE' }]; + afterEach(() => { jest.clearAllMocks(); }); @@ -132,6 +134,11 @@ describe('', () => { results: mockInstanceGroups, }, }); + ExecutionEnvironmentsAPI.read.mockReturnValue({ + data: { + results: mockExecutionEnvironment, + }, + }); let wrapper; const onSubmit = jest.fn(); await act(async () => { @@ -155,10 +162,15 @@ describe('', () => { wrapper.find('input#org-max_hosts').simulate('change', { target: { value: 134, name: 'max_hosts' }, }); + wrapper.find('ExecutionEnvironmentLookup').invoke('onChange')({ + id: 1, + name: 'Test EE', + }); }); await act(async () => { wrapper.find('button[aria-label="Save"]').simulate('click'); }); + wrapper.update(); expect(onSubmit).toHaveBeenCalledTimes(1); expect(onSubmit.mock.calls[0][0]).toEqual({ name: 'new foo', @@ -166,6 +178,7 @@ describe('', () => { galaxy_credentials: [], custom_virtualenv: 'Fizz', max_hosts: 134, + default_environment: { id: 1, name: 'Test EE' }, }); }); @@ -209,12 +222,16 @@ describe('', () => { results: mockInstanceGroups, }, }); + ExecutionEnvironmentsAPI.read.mockReturnValue({ + data: { results: mockExecutionEnvironment }, + }); const mockDataForm = { name: 'Foo', description: 'Bar', galaxy_credentials: [], max_hosts: 1, custom_virtualenv: 'Fizz', + default_environment: '', }; const onSubmit = jest.fn(); OrganizationsAPI.update.mockResolvedValue(1, mockDataForm); @@ -320,6 +337,7 @@ describe('', () => { galaxy_credentials: [], max_hosts: 0, custom_virtualenv: 'Fizz', + default_environment: '', }, [], [] diff --git a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.jsx b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.jsx index d0190830e6..eaaa4274f3 100644 --- a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.jsx +++ b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.jsx @@ -27,6 +27,7 @@ function ProjectAdd() { } = await ProjectsAPI.create({ ...values, organization: values.organization.id, + default_environment: values.default_environment?.id, }); history.push(`/projects/${id}/details`); } catch (error) { diff --git a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx index 8bc136b889..76bfd49256 100644 --- a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx @@ -20,11 +20,12 @@ describe('', () => { scm_clean: true, credential: 100, local_path: '', - organization: 2, + organization: { id: 2, name: 'Bar' }, scm_update_on_launch: true, scm_update_cache_timeout: 3, allow_override: false, custom_virtualenv: '/var/lib/awx/venv/custom-env', + default_environment: { id: 1, name: 'Foo' }, }; const projectOptionsResolve = { @@ -102,6 +103,11 @@ describe('', () => { await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); wrapper.find('ProjectForm').invoke('handleSubmit')(projectData); expect(ProjectsAPI.create).toHaveBeenCalledTimes(1); + expect(ProjectsAPI.create).toHaveBeenCalledWith({ + ...projectData, + organization: 2, + default_environment: 1, + }); }); test('handleSubmit should throw an error', async () => { diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx index 4c92c9695e..431e6e02b3 100644 --- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx +++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx @@ -124,10 +124,18 @@ function ProjectDetail({ project, i18n }) { label={i18n._(t`Cache Timeout`)} value={`${scm_update_cache_timeout} ${i18n._(t`Seconds`)}`} /> + + {summary_fields?.default_environment?.name && ( + + )} + {({ project_base_dir }) => ( + ', () => { id: 10, name: 'Foo', }, + default_environment: { + id: 12, + name: 'Bar', + image: 'quay.io/ansible/awx-ee', + }, credential: { id: 1000, name: 'qux', @@ -67,9 +72,10 @@ describe('', () => { scm_update_cache_timeout: 5, allow_override: true, custom_virtualenv: '/custom-venv', + default_environment: 1, }; - test('initially renders succesfully', () => { + test('initially renders successfully', () => { mountWithContexts(); }); @@ -95,6 +101,10 @@ describe('', () => { `${mockProject.scm_update_cache_timeout} Seconds` ); assertDetail('Ansible Environment', mockProject.custom_virtualenv); + assertDetail( + 'Execution Environment', + mockProject.summary_fields.default_environment.name + ); const dateDetails = wrapper.find('UserDateDetail'); expect(dateDetails).toHaveLength(2); expect(dateDetails.at(0).prop('label')).toEqual('Created'); diff --git a/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.jsx b/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.jsx index 6642e03503..3682a01cdc 100644 --- a/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.jsx +++ b/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.jsx @@ -26,6 +26,7 @@ function ProjectEdit({ project }) { } = await ProjectsAPI.update(project.id, { ...values, organization: values.organization.id, + default_environment: values.default_environment?.id || null, }); history.push(`/projects/${id}/details`); } catch (error) { diff --git a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx index 8c52218c1e..b2b5b80486 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx @@ -12,6 +12,7 @@ import ContentLoading from '../../../components/ContentLoading'; import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup'; import FormField, { FormSubmitError } from '../../../components/FormField'; import OrganizationLookup from '../../../components/Lookup/OrganizationLookup'; +import ExecutionEnvironmentLookup from '../../../components/Lookup/ExecutionEnvironmentLookup'; import { CredentialTypesAPI, ProjectsAPI } from '../../../api'; import { required } from '../../../util/validators'; import { @@ -101,6 +102,14 @@ function ProjectFormFields({ validate: required(i18n._(t`Select a value for this field`), i18n), }); + const [ + executionEnvironmentField, + executionEnvironmentMeta, + executionEnvironmentHelpers, + ] = useField({ + name: 'default_environment', + }); + /* Save current scm subform field values to state */ const saveSubFormState = form => { const currentScmFormFields = { ...scmFormFields }; @@ -178,6 +187,25 @@ function ProjectFormFields({ required autoPopulate={!project?.id} /> + executionEnvironmentHelpers.setTouched()} + value={executionEnvironmentField.value} + onChange={value => executionEnvironmentHelpers.setValue(value)} + popoverContent={i18n._( + t`Select the default execution environment for this project.` + )} + tooltip={i18n._( + t`Select an organization before editing the default execution environment.` + )} + globallyAvailable + isDisabled={!organizationField.value} + organizationId={organizationField.value?.id} + isDefaultEnvironment + /> diff --git a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx index 562987ba4b..bbb5e47b84 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx @@ -27,7 +27,10 @@ function JobTemplateAdd() { try { const { data: { id, type }, - } = await JobTemplatesAPI.create(remainingValues); + } = await JobTemplatesAPI.create({ + ...remainingValues, + execution_environment: values.execution_environment?.id, + }); await Promise.all([ submitLabels(id, labels, values.project.summary_fields.organization.id), submitInstanceGroups(id, instanceGroups), diff --git a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx index 7b8675cc65..9860d968c5 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx @@ -58,6 +58,7 @@ const jobTemplateData = { timeout: 0, use_fact_cache: false, verbosity: '0', + execution_environment: { id: 1, name: 'Foo' }, }; describe('', () => { @@ -77,6 +78,12 @@ describe('', () => { beforeEach(() => { LabelsAPI.read.mockResolvedValue({ data: { results: [] } }); + ProjectsAPI.readDetail.mockReturnValue({ + name: 'foo', + id: 1, + allow_override: true, + organization: 1, + }); }); afterEach(() => { @@ -126,12 +133,13 @@ describe('', () => { ...jobTemplateData, }, }); + let wrapper; await act(async () => { wrapper = mountWithContexts(); }); await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); - act(() => { + await act(() => { wrapper.find('input#template-name').simulate('change', { target: { value: 'Bar', name: 'name' }, }); @@ -144,6 +152,10 @@ describe('', () => { name: 'project', summary_fields: { organization: { id: 1, name: 'Org Foo' } }, }); + wrapper.find('ExecutionEnvironmentLookup').invoke('onChange')({ + id: 1, + name: 'Foo', + }); wrapper.update(); wrapper.find('Select#template-playbook').prop('onToggle')(); wrapper.update(); @@ -170,6 +182,7 @@ describe('', () => { inventory: 2, webhook_credential: undefined, webhook_service: '', + execution_environment: 1, }); }); @@ -190,7 +203,7 @@ describe('', () => { }); }); await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); - act(() => { + await act(async () => { wrapper.find('input#template-name').simulate('change', { target: { value: 'Foo', name: 'name' }, }); diff --git a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx index 3feed6bb7d..171891fe37 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx @@ -206,6 +206,12 @@ function JobTemplateDetail({ i18n, template }) { ) : ( )} + {summary_fields?.execution_environment && ( + + )} ', () => { el => el.length === 0 ); }); + test('webhook fields should render properly', () => { expect(wrapper.find('Detail[label="Webhook Service"]').length).toBe(1); expect(wrapper.find('Detail[label="Webhook Service"]').prop('value')).toBe( @@ -154,4 +155,13 @@ describe('', () => { expect(wrapper.find('Detail[label="Webhook Key"]').length).toBe(1); expect(wrapper.find('Detail[label="Webhook Credential"]').length).toBe(1); }); + + test('execution environment field should render properly', () => { + expect(wrapper.find('Detail[label="Execution Environment"]').length).toBe( + 1 + ); + expect( + wrapper.find('Detail[label="Execution Environment"]').prop('value') + ).toBe('Default EE'); + }); }); diff --git a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx index 213900d40d..6f109604b5 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx @@ -57,7 +57,10 @@ function JobTemplateEdit({ template }) { remainingValues.project = values.project.id; remainingValues.webhook_credential = webhook_credential?.id || null; try { - await JobTemplatesAPI.update(template.id, remainingValues); + await JobTemplatesAPI.update(template.id, { + ...remainingValues, + execution_environment: values.execution_environment?.id, + }); await Promise.all([ submitLabels(labels, template?.organization), submitInstanceGroups(instanceGroups, initialInstanceGroups), diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.jsx index d9edd77030..3656c73c02 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.jsx @@ -28,7 +28,10 @@ function WorkflowJobTemplateAdd() { try { const { data: { id }, - } = await WorkflowJobTemplatesAPI.create(templatePayload); + } = await WorkflowJobTemplatesAPI.create({ + ...templatePayload, + execution_environment: values.execution_environment?.id, + }); await Promise.all(await submitLabels(id, labels, organizationId)); history.push(`/templates/workflow_job_template/${id}/visualizer`); } catch (err) { diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx index 0ce65ca092..60331770ca 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx @@ -125,6 +125,12 @@ function WorkflowJobTemplateDetail({ template, i18n }) { } /> )} + {summary_fields?.execution_environment && ( + + )} {summary_fields.inventory && ( ', () => { created_by: { id: 1, username: 'Athena' }, modified_by: { id: 1, username: 'Apollo' }, organization: { id: 1, name: 'Org' }, + execution_environment: { + id: 4, + name: 'Demo EE', + description: '', + image: 'quay.io/ansible/awx-ee', + }, inventory: { kind: 'Foo', id: 1, name: 'Bar' }, labels: { results: [ @@ -40,6 +46,7 @@ describe('', () => { }, webhook_service: 'Github', webhook_key: 'Foo webhook key', + execution_environment: 4, }; beforeEach(async () => { @@ -127,6 +134,11 @@ describe('', () => { prop: 'value', value: 'Workflow Job Template', }, + { + element: 'Detail[label="Execution Environment"]', + prop: 'value', + value: 'Demo EE', + }, ]; const organization = wrapper diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.jsx index be61a0ee43..cb963e634f 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.jsx @@ -20,7 +20,7 @@ function WorkflowJobTemplateEdit({ template }) { ...templatePayload } = values; templatePayload.inventory = inventory?.id || null; - templatePayload.organization = organization?.id; + templatePayload.organization = organization?.id || null; templatePayload.webhook_credential = webhook_credential?.id || null; const formOrgId = @@ -29,7 +29,10 @@ function WorkflowJobTemplateEdit({ template }) { await Promise.all( await submitLabels(labels, formOrgId, template.organization) ); - await WorkflowJobTemplatesAPI.update(template.id, templatePayload); + await WorkflowJobTemplatesAPI.update(template.id, { + ...templatePayload, + execution_environment: values.execution_environment?.id, + }); history.push(`/templates/workflow_job_template/${template.id}/details`); } catch (err) { setFormSubmitError(err); diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index f504224507..c267e157bb 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -37,9 +37,10 @@ import { InstanceGroupsLookup, ProjectLookup, MultiCredentialsLookup, + ExecutionEnvironmentLookup, } from '../../../components/Lookup'; import Popover from '../../../components/Popover'; -import { JobTemplatesAPI } from '../../../api'; +import { JobTemplatesAPI, ProjectsAPI } from '../../../api'; import LabelSelect from './LabelSelect'; import PlaybookSelect from './PlaybookSelect'; import WebhookSubForm from './WebhookSubForm'; @@ -101,10 +102,40 @@ function JobTemplateForm({ 'webhook_credential' ); + const [ + executionEnvironmentField, + executionEnvironmentMeta, + executionEnvironmentHelpers, + ] = useField({ name: 'execution_environment' }); + + const projectId = projectField.value?.id; + + const { + request: fetchProject, + error: fetchProjectError, + isLoading: fetchProjectLoading, + result: projectData, + } = useRequest( + useCallback(async () => { + if (!projectId) { + return {}; + } + const { data } = await ProjectsAPI.readDetail(projectId); + return data; + }, [projectId]), + { + projectData: null, + } + ); + + useEffect(() => { + fetchProject(); + }, [fetchProject]); + const { request: loadRelatedInstanceGroups, error: instanceGroupError, - contentLoading: instanceGroupLoading, + isLoading: instanceGroupLoading, } = useRequest( useCallback(async () => { if (!template?.id) { @@ -182,12 +213,16 @@ function JobTemplateForm({ callbackUrl = `${origin}${path}`; } - if (instanceGroupLoading) { + if (instanceGroupLoading || fetchProjectLoading) { return ; } - if (contentError || instanceGroupError) { - return ; + if (contentError || instanceGroupError || fetchProjectError) { + return ( + + ); } return ( @@ -258,6 +293,7 @@ function JobTemplateForm({ isOverrideDisabled={isOverrideDisabledLookup} /> + projectHelpers.setTouched()} @@ -270,6 +306,26 @@ function JobTemplateForm({ autoPopulate={!template?.id} isOverrideDisabled={isOverrideDisabledLookup} /> + + executionEnvironmentHelpers.setTouched()} + value={executionEnvironmentField.value} + onChange={value => executionEnvironmentHelpers.setValue(value)} + popoverContent={i18n._( + t`Select the execution environment for this job template.` + )} + tooltip={i18n._( + t`Select a project before editing the execution environment.` + )} + globallyAvailable + isDisabled={!projectField.value} + organizationId={projectData?.organization} + /> + {projectField.value?.allow_override && ( { diff --git a/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx index fd15c92e28..5ee34bbe12 100644 --- a/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx @@ -22,7 +22,10 @@ import { SubFormLayout, } from '../../../components/FormLayout'; import OrganizationLookup from '../../../components/Lookup/OrganizationLookup'; -import { InventoryLookup } from '../../../components/Lookup'; +import { + InventoryLookup, + ExecutionEnvironmentLookup, +} from '../../../components/Lookup'; import { VariablesField } from '../../../components/CodeMirrorInput'; import FormActionGroup from '../../../components/FormActionGroup'; import ContentError from '../../../components/ContentError'; @@ -63,6 +66,14 @@ function WorkflowJobTemplateForm({ 'webhook_credential' ); + const [ + executionEnvironmentField, + executionEnvironmentMeta, + executionEnvironmentHelpers, + ] = useField({ + name: 'execution_environment', + }); + useEffect(() => { if (enableWebhooks) { webhookServiceHelpers.setValue(webhookServiceMeta.initialValue); @@ -178,6 +189,20 @@ function WorkflowJobTemplateForm({ }} /> + executionEnvironmentHelpers.setTouched()} + value={executionEnvironmentField.value} + onChange={value => executionEnvironmentHelpers.setValue(value)} + tooltip={i18n._( + t`Select the default execution environment for this organization to run on.` + )} + globallyAvailable + organizationId={organizationField.value?.id} + />
{ diff --git a/awx/ui_next/src/screens/Template/shared/data.job_template.json b/awx/ui_next/src/screens/Template/shared/data.job_template.json index 804c3b72a2..fa516d46db 100644 --- a/awx/ui_next/src/screens/Template/shared/data.job_template.json +++ b/awx/ui_next/src/screens/Template/shared/data.job_template.json @@ -133,6 +133,12 @@ "id": "1", "name": "Webhook Credential" + }, + "execution_environment": { + "id": 1, + "name": "Default EE", + "description": "", + "image": "quay.io/ansible/awx-ee" } }, "created": "2019-09-30T16:18:34.564820Z", @@ -177,5 +183,6 @@ "job_slice_count": 1, "webhook_credential": 1, "webhook_key": "asertdyuhjkhgfd234567kjgfds", - "webhook_service": "github" + "webhook_service": "github", + "execution_environment": 1 } diff --git a/awx/ui_next/src/types.js b/awx/ui_next/src/types.js index 53c7ed3b3d..744360eaea 100644 --- a/awx/ui_next/src/types.js +++ b/awx/ui_next/src/types.js @@ -416,4 +416,5 @@ export const ExecutionEnvironment = shape({ url: string, summary_fields: shape({}), description: string, + pull: string, }); From 065b943870a4d8574c21432f774a71aa29652221 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 1 Mar 2021 20:20:25 -0500 Subject: [PATCH 150/178] Fix mode for k8s launch scripts --- tools/ansible/roles/dockerfile/files/launch_awx.sh | 0 tools/ansible/roles/dockerfile/files/launch_awx_task.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 tools/ansible/roles/dockerfile/files/launch_awx.sh mode change 100644 => 100755 tools/ansible/roles/dockerfile/files/launch_awx_task.sh diff --git a/tools/ansible/roles/dockerfile/files/launch_awx.sh b/tools/ansible/roles/dockerfile/files/launch_awx.sh old mode 100644 new mode 100755 diff --git a/tools/ansible/roles/dockerfile/files/launch_awx_task.sh b/tools/ansible/roles/dockerfile/files/launch_awx_task.sh old mode 100644 new mode 100755 From 03c5cc779bac10cf250e766ce3a5865c6c14507a Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 1 Mar 2021 20:20:54 -0500 Subject: [PATCH 151/178] Only install podman in local dev env --- .../ansible/roles/dockerfile/templates/Dockerfile.j2 | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index e17a0b04d8..3f7666a615 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -148,7 +148,6 @@ RUN dnf -y install \ nss \ make \ patch \ - podman \ socat \ tmux \ wget \ @@ -162,6 +161,11 @@ RUN dnf -y install \ RUN dnf --enablerepo=debuginfo -y install python3-debuginfo || : {% endif %} +{% if build_dev|bool %} +RUN dnf install -y podman +RUN echo -e 'cgroup_manager = "cgroupfs"\nevents_logger = "file"' > /etc/containers/libpod.conf +{% endif %} + # Copy app from builder COPY --from=builder /var/lib/awx /var/lib/awx @@ -245,8 +249,6 @@ RUN ln -sf /dev/stdout /var/log/nginx/access.log && \ ln -sf /dev/stderr /var/log/nginx/error.log {% endif %} -RUN echo -e 'cgroup_manager = "cgroupfs"\nevents_logger = "file"' > /etc/containers/libpod.conf - ENV HOME="/var/lib/awx" ENV PATH="/usr/pgsql-10/bin:${PATH}" @@ -264,6 +266,5 @@ EXPOSE 8052 ENTRYPOINT ["/usr/bin/tini", "--"] CMD /usr/bin/launch_awx.sh VOLUME /var/lib/nginx -{% endif %} - VOLUME /var/lib/awx/.local/share/containers/storage +{% endif %} From eba12a62072eeac1fa5caf3254978efc032c8232 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Tue, 2 Mar 2021 16:16:20 -0500 Subject: [PATCH 152/178] Add ui_next to .dockerignore --- .dockerignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.dockerignore b/.dockerignore index 46c83b0467..07c13d382d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,3 @@ awx/ui/node_modules +awx/ui_next/node_modules +Dockerfile From e23a2b4506f788e7318e3f2a77192de888cb5f4e Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Tue, 2 Mar 2021 16:16:40 -0500 Subject: [PATCH 153/178] Use var instead of set_fact --- tools/ansible/roles/image_build/defaults/main.yml | 2 +- tools/ansible/roles/image_build/tasks/main.yml | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/tools/ansible/roles/image_build/defaults/main.yml b/tools/ansible/roles/image_build/defaults/main.yml index 0d45e047d8..076a4f47b3 100644 --- a/tools/ansible/roles/image_build/defaults/main.yml +++ b/tools/ansible/roles/image_build/defaults/main.yml @@ -1,5 +1,5 @@ --- -create_preload_data: true +awx_image: quay.io/ansible/awx # Helper vars to construct the proper download URL for the current architecture tini_architecture: '{{ { "x86_64": "amd64", "aarch64": "arm64", "armv7": "arm" }[ansible_facts.architecture] }}' diff --git a/tools/ansible/roles/image_build/tasks/main.yml b/tools/ansible/roles/image_build/tasks/main.yml index ae6e30b7a3..13c5c48a3d 100644 --- a/tools/ansible/roles/image_build/tasks/main.yml +++ b/tools/ansible/roles/image_build/tasks/main.yml @@ -17,10 +17,6 @@ dest: "../awx/ui_next/public/static/media/" when: awx_official|default(false)|bool -- name: Set awx image name - set_fact: - awx_image: "{{ awx_image|default('awx') }}" - # Calling Docker directly because docker-py doesnt support BuildKit - name: Build AWX image command: docker build -t {{ awx_image }}:{{ awx_version }} -f ../../{{ dockerfile_name }} ../.. From 609b17aa209ca368f6c15ac8ca46d63b16d175e6 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Tue, 2 Mar 2021 16:31:04 -0500 Subject: [PATCH 154/178] Use receptor 0.9.6 --- tools/ansible/roles/dockerfile/templates/Dockerfile.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index 3f7666a615..66a0eeebbd 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -172,7 +172,7 @@ COPY --from=builder /var/lib/awx /var/lib/awx RUN ln -s /var/lib/awx/venv/awx/bin/awx-manage /usr/bin/awx-manage {%if build_dev|bool %} -COPY --from=quay.io/project-receptor/receptor:0.9.5 /usr/bin/receptor /usr/bin/receptor +COPY --from=quay.io/project-receptor/receptor:0.9.6 /usr/bin/receptor /usr/bin/receptor RUN openssl req -nodes -newkey rsa:2048 -keyout /etc/nginx/nginx.key -out /etc/nginx/nginx.csr \ -subj "/C=US/ST=North Carolina/L=Durham/O=Ansible/OU=AWX Development/CN=awx.localhost" && \ openssl x509 -req -days 365 -in /etc/nginx/nginx.csr -signkey /etc/nginx/nginx.key -out /etc/nginx/nginx.crt && \ From 33d7342ffec4fe967f14d06711012722fe4062b2 Mon Sep 17 00:00:00 2001 From: nixocio Date: Tue, 2 Mar 2021 13:04:30 -0500 Subject: [PATCH 155/178] Linkify reference to EE on details page Linkify reference to EE on a few details page. See: https://github.com/ansible/awx/issues/9189 --- .../OrganizationDetail/OrganizationDetail.jsx | 8 +++++++- .../src/screens/Project/ProjectDetail/ProjectDetail.jsx | 9 +++++++-- .../Template/JobTemplateDetail/JobTemplateDetail.jsx | 8 +++++++- .../JobTemplateDetail/JobTemplateDetail.test.jsx | 2 +- .../WorkflowJobTemplateDetail.jsx | 8 +++++++- .../WorkflowJobTemplateDetail.test.jsx | 9 ++++----- 6 files changed, 33 insertions(+), 11 deletions(-) diff --git a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx index a1abee28a8..e3b544c091 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx @@ -97,7 +97,13 @@ function OrganizationDetail({ i18n, organization }) { {summary_fields?.default_environment?.name && ( + {summary_fields.default_environment.name} + + } /> )} + {summary_fields.default_environment.name} + + } /> )} - {({ project_base_dir }) => ( + {summary_fields.execution_environment.name} + + } /> )} ', () => { 1 ); expect( - wrapper.find('Detail[label="Execution Environment"]').prop('value') + wrapper.find(`Detail[label="Execution Environment"] dd`).text() ).toBe('Default EE'); }); }); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx index 60331770ca..4f8237e8c2 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx @@ -128,7 +128,13 @@ function WorkflowJobTemplateDetail({ template, i18n }) { {summary_fields?.execution_environment && ( + {summary_fields.execution_environment.name} + + } /> )} diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.test.jsx index 17a6593fda..0c1be1e8e9 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.test.jsx @@ -134,11 +134,6 @@ describe('', () => { prop: 'value', value: 'Workflow Job Template', }, - { - element: 'Detail[label="Execution Environment"]', - prop: 'value', - value: 'Demo EE', - }, ]; const organization = wrapper @@ -162,6 +157,10 @@ describe('', () => { }; renderedValues.map(value => assertValue(value)); + + expect( + wrapper.find(`Detail[label="Execution Environment"] dd`).text() + ).toBe('Demo EE'); }); test('link out resource have the correct url', () => { From 29ff69a7749d792d586a8f8e85349a657e9fbcc1 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Tue, 2 Mar 2021 19:32:15 -0500 Subject: [PATCH 156/178] For container group pods, use namespace Tower is deployed into by default --- awx/settings/defaults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 845bbe74d8..ba8b8c69cf 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -72,7 +72,7 @@ AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE = 'quay.io/ansible/awx-ee' AWX_CONTAINER_GROUP_K8S_API_TIMEOUT = 10 AWX_CONTAINER_GROUP_POD_LAUNCH_RETRIES = 100 AWX_CONTAINER_GROUP_POD_LAUNCH_RETRY_DELAY = 5 -AWX_CONTAINER_GROUP_DEFAULT_NAMESPACE = 'default' +AWX_CONTAINER_GROUP_DEFAULT_NAMESPACE = os.getenv('MY_POD_NAMESPACE', 'default') # TODO: remove this setting in favor of a default execution environment AWX_CONTAINER_GROUP_DEFAULT_IMAGE = AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE From aab58f5ae7c7ae369dae7434a338d3534a0378bd Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Tue, 2 Mar 2021 19:11:56 -0500 Subject: [PATCH 157/178] Dont require credential for Container Groups Might want to follow up and make this only apply to Tower on Kubernetes --- .../InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.jsx | 2 +- .../src/screens/InstanceGroup/shared/ContainerGroupForm.jsx | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.jsx b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.jsx index 3bf66a9d4d..9f4454c0a8 100644 --- a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.jsx @@ -37,7 +37,7 @@ function ContainerGroupEdit({ instanceGroup }) { try { await InstanceGroupsAPI.update(instanceGroup.id, { name: values.name, - credential: values.credential.id, + credential: values.credential ? values.credential.id : null, pod_spec_override: values.override ? values.pod_spec_override : null, }); history.push(detailsIUrl); diff --git a/awx/ui_next/src/screens/InstanceGroup/shared/ContainerGroupForm.jsx b/awx/ui_next/src/screens/InstanceGroup/shared/ContainerGroupForm.jsx index fda18b73bc..e41ab0a1b0 100644 --- a/awx/ui_next/src/screens/InstanceGroup/shared/ContainerGroupForm.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/shared/ContainerGroupForm.jsx @@ -25,7 +25,6 @@ function ContainerGroupFormFields({ i18n, instanceGroup }) { const { setFieldValue } = useFormikContext(); const [credentialField, credentialMeta, credentialHelpers] = useField({ name: 'credential', - validate: required(i18n._(t`Select a value for this field`), i18n), }); const [overrideField] = useField('override'); @@ -55,9 +54,8 @@ function ContainerGroupFormFields({ i18n, instanceGroup }) { onBlur={() => credentialHelpers.setTouched()} onChange={onCredentialChange} value={credentialField.value} - required tooltip={i18n._( - t`Credential to authenticate with Kubernetes or OpenShift. Must be of type "Kubernetes/OpenShift API Bearer Token”.` + t`Credential to authenticate with Kubernetes or OpenShift. Must be of type "Kubernetes/OpenShift API Bearer Token". If left blank, the underlying Pod's service account will be used.` )} autoPopulate={!instanceGroup?.id} /> From fd21603c0ecdab6bb95d537a0845ec04a72f6c17 Mon Sep 17 00:00:00 2001 From: nixocio Date: Mon, 1 Mar 2021 11:10:31 -0500 Subject: [PATCH 158/178] Update EE breadcrumb to use name instead of image Update EE breadcrumb to use name instead of image to be consistent with the other screens. See: https://github.com/ansible/awx/issues/9087 --- .../src/screens/ExecutionEnvironment/ExecutionEnvironments.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.jsx index d95c15959f..802e78a679 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironments.jsx @@ -22,7 +22,7 @@ function ExecutionEnvironments({ i18n }) { setBreadcrumbConfig({ '/execution_environments': i18n._(t`Execution environments`), '/execution_environments/add': i18n._(t`Create Execution environments`), - [`/execution_environments/${executionEnvironments.id}`]: `${executionEnvironments.image}`, + [`/execution_environments/${executionEnvironments.id}`]: `${executionEnvironments.name}`, [`/execution_environments/${executionEnvironments.id}/edit`]: i18n._( t`Edit details` ), From 62215ca432f00a863a1a2f61ed85decd0aedbd7e Mon Sep 17 00:00:00 2001 From: nixocio Date: Mon, 1 Mar 2021 10:49:51 -0500 Subject: [PATCH 159/178] Add organization to EE details page Add organization to EE details page. See: https://github.com/ansible/awx/issues/9432 --- .../ExecutionEnvironmentDetails.jsx | 25 +++++++++++- .../ExecutionEnvironmentDetails.test.jsx | 39 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx index 13839bb95b..68b9f9879d 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx @@ -18,7 +18,15 @@ import { ExecutionEnvironmentsAPI } from '../../../api'; function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) { const history = useHistory(); - const { id, name, image, description, pull } = executionEnvironment; + const { + id, + name, + image, + description, + pull, + organization, + summary_fields, + } = executionEnvironment; const { request: deleteExecutionEnvironment, @@ -51,6 +59,21 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) { value={description} dataCy="execution-environment-detail-description" /> + + {summary_fields.organization.name} + + ) : ( + i18n._(t`Globally Available`) + ) + } + dataCy="execution-environment-detail-organization" + /> ', () => { expect(wrapper.find('Detail[label="Description"]').prop('value')).toEqual( 'Foo' ); + expect(wrapper.find('Detail[label="Organization"]').prop('value')).toEqual( + 'Globally Available' + ); + expect( + wrapper.find('Detail[label="Credential"]').prop('value').props.children + ).toEqual(executionEnvironment.summary_fields.credential.name); + const dates = wrapper.find('UserDateDetail'); + expect(dates).toHaveLength(2); + expect(dates.at(0).prop('date')).toEqual(executionEnvironment.created); + expect(dates.at(1).prop('date')).toEqual(executionEnvironment.modified); + }); + + test('should render organization detail', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + wrapper.update(); + + expect(wrapper.find('Detail[label="Image"]').prop('value')).toEqual( + executionEnvironment.image + ); + expect(wrapper.find('Detail[label="Description"]').prop('value')).toEqual( + 'Foo' + ); + expect(wrapper.find(`Detail[label="Organization"] dd`).text()).toBe('Bar'); expect( wrapper.find('Detail[label="Credential"]').prop('value').props.children ).toEqual(executionEnvironment.summary_fields.credential.name); From 0a4a1bed0aebd2745fac9291c678483242e3b9f3 Mon Sep 17 00:00:00 2001 From: nixocio Date: Tue, 2 Mar 2021 09:22:09 -0500 Subject: [PATCH 160/178] Add EE to inventory sources Add EE to inventory sources See: https://github.com/ansible/awx/issues/9189 --- .../Lookup/ExecutionEnvironmentLookup.jsx | 4 +- .../InventorySourceAdd/InventorySourceAdd.jsx | 9 ++-- .../InventorySourceAdd.test.jsx | 42 ++++++++++++++----- .../InventorySourceDetail.jsx | 13 ++++++ .../InventorySourceEdit.jsx | 9 ++-- .../InventorySourceEdit.test.jsx | 20 ++++++--- .../InventorySources/InventorySources.jsx | 2 +- .../Inventory/shared/InventorySourceForm.jsx | 30 ++++++++++++- .../shared/InventorySourceForm.test.jsx | 1 + 9 files changed, 104 insertions(+), 26 deletions(-) diff --git a/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.jsx b/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.jsx index ecab4f3b44..86f659c430 100644 --- a/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.jsx +++ b/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.jsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect } from 'react'; import { string, func, bool } from 'prop-types'; -import { withRouter, useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { FormGroup, Tooltip } from '@patternfly/react-core'; @@ -164,4 +164,4 @@ ExecutionEnvironmentLookup.defaultProps = { value: null, }; -export default withI18n()(withRouter(ExecutionEnvironmentLookup)); +export default withI18n()(ExecutionEnvironmentLookup); diff --git a/awx/ui_next/src/screens/Inventory/InventorySourceAdd/InventorySourceAdd.jsx b/awx/ui_next/src/screens/Inventory/InventorySourceAdd/InventorySourceAdd.jsx index 0db70f2dbf..67ea90f4ea 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySourceAdd/InventorySourceAdd.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySourceAdd/InventorySourceAdd.jsx @@ -1,14 +1,14 @@ import React, { useCallback, useEffect } from 'react'; -import { useHistory, useParams } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import { Card } from '@patternfly/react-core'; import { InventorySourcesAPI } from '../../../api'; import useRequest from '../../../util/useRequest'; import { CardBody } from '../../../components/Card'; import InventorySourceForm from '../shared/InventorySourceForm'; -function InventorySourceAdd() { +function InventorySourceAdd({ inventory }) { const history = useHistory(); - const { id } = useParams(); + const { id, organization } = inventory; const { error, request, result } = useRequest( useCallback(async values => { @@ -31,6 +31,7 @@ function InventorySourceAdd() { source_path, source_project, source_script, + execution_environment, ...remainingForm } = form; @@ -46,6 +47,7 @@ function InventorySourceAdd() { credential: credential?.id || null, inventory: id, source_script: source_script?.id || null, + execution_environment: execution_environment?.id || null, ...sourcePath, ...sourceProject, ...remainingForm, @@ -63,6 +65,7 @@ function InventorySourceAdd() { onCancel={handleCancel} onSubmit={handleSubmit} submitError={error} + organizationId={organization} /> diff --git a/awx/ui_next/src/screens/Inventory/InventorySourceAdd/InventorySourceAdd.test.jsx b/awx/ui_next/src/screens/Inventory/InventorySourceAdd/InventorySourceAdd.test.jsx index afc4d69d0e..c186d4dcb7 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySourceAdd/InventorySourceAdd.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySourceAdd/InventorySourceAdd.test.jsx @@ -35,6 +35,12 @@ describe('', () => { verbosity: 1, }; + const mockInventory = { + id: 111, + name: 'Foo', + organization: 2, + }; + InventorySourcesAPI.readOptions.mockResolvedValue({ data: { actions: { @@ -72,9 +78,12 @@ describe('', () => { custom_virtualenvs: ['venv/foo', 'venv/bar'], }; await act(async () => { - wrapper = mountWithContexts(, { - context: { config }, - }); + wrapper = mountWithContexts( + , + { + context: { config }, + } + ); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); expect(wrapper.find('FormGroup[label="Name"]')).toHaveLength(1); @@ -88,9 +97,12 @@ describe('', () => { test('should navigate to inventory sources list when cancel is clicked', async () => { const history = createMemoryHistory({}); await act(async () => { - wrapper = mountWithContexts(, { - context: { router: { history } }, - }); + wrapper = mountWithContexts( + , + { + context: { router: { history } }, + } + ); }); await act(async () => { wrapper.find('InventorySourceForm').invoke('onCancel')(); @@ -103,7 +115,9 @@ describe('', () => { test('should post to the api when submit is clicked', async () => { InventorySourcesAPI.create.mockResolvedValueOnce({ data: {} }); await act(async () => { - wrapper = mountWithContexts(); + wrapper = mountWithContexts( + + ); }); await act(async () => { wrapper.find('InventorySourceForm').invoke('onSubmit')(invSourceData); @@ -114,6 +128,7 @@ describe('', () => { credential: 222, source_project: 999, source_script: null, + execution_environment: null, }); }); @@ -123,9 +138,12 @@ describe('', () => { data: { id: 123, inventory: 111 }, }); await act(async () => { - wrapper = mountWithContexts(, { - context: { router: { history } }, - }); + wrapper = mountWithContexts( + , + { + context: { router: { history } }, + } + ); }); await act(async () => { wrapper.find('InventorySourceForm').invoke('onSubmit')(invSourceData); @@ -143,7 +161,9 @@ describe('', () => { }; InventorySourcesAPI.create.mockImplementation(() => Promise.reject(error)); await act(async () => { - wrapper = mountWithContexts(); + wrapper = mountWithContexts( + + ); }); expect(wrapper.find('FormSubmitError').length).toBe(0); await act(async () => { diff --git a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx index f3fcdebfca..4383998994 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx @@ -50,6 +50,7 @@ function InventorySourceDetail({ inventorySource, i18n }) { organization, source_project, user_capabilities, + execution_environment, }, } = inventorySource; const [deletionError, setDeletionError] = useState(false); @@ -214,6 +215,18 @@ function InventorySourceDetail({ inventorySource, i18n }) { } /> )} + {execution_environment?.name && ( + + {execution_environment.name} + + } + /> + )} {source === 'scm' ? ( diff --git a/awx/ui_next/src/screens/Inventory/InventorySourceEdit/InventorySourceEdit.test.jsx b/awx/ui_next/src/screens/Inventory/InventorySourceEdit/InventorySourceEdit.test.jsx index 87ec0288c1..cc8ad163b2 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySourceEdit/InventorySourceEdit.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySourceEdit/InventorySourceEdit.test.jsx @@ -18,7 +18,7 @@ jest.mock('react-router-dom', () => ({ }), })); -describe('', () => { +describe('', () => { let wrapper; let history; const mockInvSrc = { @@ -37,6 +37,11 @@ describe('', () => { update_on_project_update: false, verbosity: 1, }; + const mockInventory = { + id: 1, + name: 'Foo', + organization: 1, + }; InventorySourcesAPI.readOptions.mockResolvedValue({ data: { actions: { @@ -89,9 +94,12 @@ describe('', () => { beforeAll(async () => { history = createMemoryHistory(); await act(async () => { - wrapper = mountWithContexts(, { - context: { router: { history } }, - }); + wrapper = mountWithContexts( + , + { + context: { router: { history } }, + } + ); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); }); @@ -133,7 +141,9 @@ describe('', () => { }; InventorySourcesAPI.replace.mockImplementation(() => Promise.reject(error)); await act(async () => { - wrapper = mountWithContexts(); + wrapper = mountWithContexts( + + ); }); expect(wrapper.find('FormSubmitError').length).toBe(0); await act(async () => { diff --git a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySources.jsx b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySources.jsx index 2e8f1a3785..e55125187d 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySources.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySources.jsx @@ -9,7 +9,7 @@ function InventorySources({ inventory, setBreadcrumb }) { return ( - + diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.jsx index 3bc824fca8..05bbd4e370 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.jsx @@ -31,6 +31,7 @@ import { VMwareSubForm, VirtualizationSubForm, } from './InventorySourceSubForms'; +import { ExecutionEnvironmentLookup } from '../../../components/Lookup'; const buildSourceChoiceOptions = options => { const sourceChoices = options.actions.GET.source.choices.map( @@ -39,7 +40,12 @@ const buildSourceChoiceOptions = options => { return sourceChoices.filter(({ key }) => key !== 'file'); }; -const InventorySourceFormFields = ({ source, sourceOptions, i18n }) => { +const InventorySourceFormFields = ({ + source, + sourceOptions, + organizationId, + i18n, +}) => { const { values, initialValues, @@ -51,6 +57,13 @@ const InventorySourceFormFields = ({ source, sourceOptions, i18n }) => { name: 'source', validate: required(i18n._(t`Set a value for this field`), i18n), }); + const [ + executionEnvironmentField, + executionEnvironmentMeta, + executionEnvironmentHelpers, + ] = useField({ + name: 'execution_environment', + }); const { custom_virtualenvs } = useContext(ConfigContext); const [venvField] = useField('custom_virtualenv'); const defaultVenv = { @@ -111,6 +124,17 @@ const InventorySourceFormFields = ({ source, sourceOptions, i18n }) => { name="description" type="text" /> + executionEnvironmentHelpers.setTouched()} + value={executionEnvironmentField.value} + onChange={value => executionEnvironmentHelpers.setValue(value)} + globallyAvailable + organizationId={organizationId} + /> { const initialValues = { credential: source?.summary_fields?.credential || null, @@ -264,6 +289,8 @@ const InventorySourceForm = ({ enabled_var: source?.enabled_var || '', enabled_value: source?.enabled_value || '', host_filter: source?.host_filter || '', + execution_environment: + source?.summary_fields?.execution_environment || null, }; const { @@ -306,6 +333,7 @@ const InventorySourceForm = ({ i18n={i18n} source={source} sourceOptions={sourceOptions} + organizationId={organizationId} /> {submitError && } ', () => { expect( wrapper.find('FormGroup[label="Ansible Environment"]') ).toHaveLength(1); + expect(wrapper.find('ExecutionEnvironmentLookup')).toHaveLength(1); }); test('should display subform when source dropdown has a value', async () => { From a3a47834fd4da033ea1c1cd5722bd45fe15310d6 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Thu, 4 Mar 2021 11:26:43 -0500 Subject: [PATCH 161/178] Create admin user / run create_preload_data when dev env boots --- tools/docker-compose/bootstrap_development.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tools/docker-compose/bootstrap_development.sh b/tools/docker-compose/bootstrap_development.sh index 88a7e62941..b1cd48e4eb 100755 --- a/tools/docker-compose/bootstrap_development.sh +++ b/tools/docker-compose/bootstrap_development.sh @@ -15,6 +15,15 @@ make version_file make migrate make init + +if output=$(awx-manage createsuperuser --noinput --username=admin --email=admin@localhost 2> /dev/null); then + echo $output + admin_password=$(openssl rand -base64 12) + echo "Admin password: ${admin_password}" + awx-manage update_password --username=admin --password=${admin_password} +fi +awx-manage create_preload_data + mkdir -p /awx_devel/awx/public/static mkdir -p /awx_devel/awx/ui/static mkdir -p /awx_devel/awx/ui_next/build/static From 0505e380717009dc4a88cd08c39b9ff9e679965c Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Thu, 4 Mar 2021 11:02:37 -0500 Subject: [PATCH 162/178] Remove ansible venvs & collection infrastructure. --- Makefile | 65 +-- awx/main/tests/functional/test_licenses.py | 2 +- awx/main/tests/unit/utils/test_common.py | 24 - docs/licenses/apache-libcloud.txt | 202 ------- docs/licenses/appdirs.txt | 22 - docs/licenses/applicationinsights.txt | 21 - docs/licenses/argcomplete.txt | 177 ------ docs/licenses/azure-cli-core.txt | 21 - docs/licenses/azure-cli-nspkg.txt | 21 - docs/licenses/azure-graphrbac.txt | 21 - docs/licenses/azure-mgmt-authorization.txt | 21 - docs/licenses/azure-mgmt-automation.txt | 21 - docs/licenses/azure-mgmt-batch.txt | 21 - docs/licenses/azure-mgmt-cdn.txt | 21 - docs/licenses/azure-mgmt-compute.txt | 21 - .../licenses/azure-mgmt-containerinstance.txt | 21 - .../licenses/azure-mgmt-containerregistry.txt | 21 - docs/licenses/azure-mgmt-containerservice.txt | 21 - docs/licenses/azure-mgmt-cosmosdb.txt | 21 - docs/licenses/azure-mgmt-devtestlabs.txt | 21 - docs/licenses/azure-mgmt-dns.txt | 21 - docs/licenses/azure-mgmt-hdinsight.txt | 21 - docs/licenses/azure-mgmt-iothub.txt | 21 - docs/licenses/azure-mgmt-keyvault.txt | 21 - docs/licenses/azure-mgmt-loganalytics.txt | 21 - .../azure-mgmt-marketplaceordering.txt | 21 - docs/licenses/azure-mgmt-monitor.txt | 21 - docs/licenses/azure-mgmt-network.txt | 21 - docs/licenses/azure-mgmt-nspkg.txt | 21 - docs/licenses/azure-mgmt-rdbms.txt | 21 - docs/licenses/azure-mgmt-redis.txt | 21 - docs/licenses/azure-mgmt-resource.txt | 21 - docs/licenses/azure-mgmt-servicebus.txt | 21 - docs/licenses/azure-mgmt-sql.txt | 21 - docs/licenses/azure-mgmt-storage.txt | 21 - docs/licenses/azure-mgmt-trafficmanager.txt | 21 - docs/licenses/azure-mgmt-web.txt | 21 - docs/licenses/azure-storage.txt | 21 - .../licenses/backports.ssl-match-hostname.txt | 96 ---- docs/licenses/bcrypt.txt | 201 ------- docs/licenses/boto.txt | 18 - docs/licenses/boto3.txt | 12 - docs/licenses/botocore.txt | 12 - docs/licenses/colorama.txt | 28 - docs/licenses/decorator.txt | 26 - docs/licenses/dogpile.cache.txt | 27 - docs/licenses/enum34.txt | 32 -- docs/licenses/futures.txt | 48 -- docs/licenses/humanfriendly.txt | 20 - docs/licenses/ipaddress.txt | 50 -- docs/licenses/iso8601.txt | 20 - docs/licenses/jmespath.txt | 20 - docs/licenses/jsonpatch.txt | 25 - docs/licenses/jsonpointer.txt | 25 - docs/licenses/keystoneauth1.txt | 209 -------- docs/licenses/knack.txt | 21 - docs/licenses/monotonic.txt | 201 ------- docs/licenses/munch.txt | 19 - docs/licenses/ncclient.txt | 202 ------- docs/licenses/netifaces.txt | 4 - docs/licenses/ntlm-auth.txt | 163 ------ docs/licenses/openstacksdk.txt | 175 ------ docs/licenses/os-service-types.txt | 175 ------ docs/licenses/ovirt-engine-sdk-python.txt | 177 ------ docs/licenses/packaging.txt | 177 ------ docs/licenses/paramiko.txt | 503 ------------------ docs/licenses/pbr.txt | 176 ------ docs/licenses/pycurl.txt | 11 - docs/licenses/pygments.txt | 20 - docs/licenses/pykerberos.txt | 202 ------- docs/licenses/pynacl.txt | 174 ------ docs/licenses/pyvmomi.txt | 203 ------- docs/licenses/pywinrm.txt | 19 - docs/licenses/requests-credssp.txt | 15 - docs/licenses/requests-kerberos.txt | 15 - docs/licenses/requests-ntlm.txt | 15 - docs/licenses/requestsexceptions.txt | 202 ------- docs/licenses/ruamel.ordereddict.txt | 23 - docs/licenses/s3transfer.txt | 203 ------- docs/licenses/selectors2.txt | 21 - docs/licenses/stevedore.txt | 202 ------- docs/licenses/tabulate.txt | 20 - docs/licenses/typing.txt | 254 --------- docs/licenses/wheel.txt | 22 - docs/licenses/xmltodict.txt | 7 - requirements/collections_requirements.yml | 21 - requirements/requirements_ansible.in | 68 --- requirements/requirements_ansible.txt | 125 ----- requirements/requirements_ansible_git.txt | 1 - .../requirements_ansible_uninstall.txt | 1 - .../roles/dockerfile/templates/Dockerfile.j2 | 9 +- tools/audit-api-license.sh | 20 - 92 files changed, 7 insertions(+), 5892 deletions(-) delete mode 100644 docs/licenses/apache-libcloud.txt delete mode 100644 docs/licenses/appdirs.txt delete mode 100644 docs/licenses/applicationinsights.txt delete mode 100644 docs/licenses/argcomplete.txt delete mode 100644 docs/licenses/azure-cli-core.txt delete mode 100644 docs/licenses/azure-cli-nspkg.txt delete mode 100644 docs/licenses/azure-graphrbac.txt delete mode 100644 docs/licenses/azure-mgmt-authorization.txt delete mode 100644 docs/licenses/azure-mgmt-automation.txt delete mode 100644 docs/licenses/azure-mgmt-batch.txt delete mode 100644 docs/licenses/azure-mgmt-cdn.txt delete mode 100644 docs/licenses/azure-mgmt-compute.txt delete mode 100644 docs/licenses/azure-mgmt-containerinstance.txt delete mode 100644 docs/licenses/azure-mgmt-containerregistry.txt delete mode 100644 docs/licenses/azure-mgmt-containerservice.txt delete mode 100644 docs/licenses/azure-mgmt-cosmosdb.txt delete mode 100644 docs/licenses/azure-mgmt-devtestlabs.txt delete mode 100644 docs/licenses/azure-mgmt-dns.txt delete mode 100644 docs/licenses/azure-mgmt-hdinsight.txt delete mode 100644 docs/licenses/azure-mgmt-iothub.txt delete mode 100644 docs/licenses/azure-mgmt-keyvault.txt delete mode 100644 docs/licenses/azure-mgmt-loganalytics.txt delete mode 100644 docs/licenses/azure-mgmt-marketplaceordering.txt delete mode 100644 docs/licenses/azure-mgmt-monitor.txt delete mode 100644 docs/licenses/azure-mgmt-network.txt delete mode 100644 docs/licenses/azure-mgmt-nspkg.txt delete mode 100644 docs/licenses/azure-mgmt-rdbms.txt delete mode 100644 docs/licenses/azure-mgmt-redis.txt delete mode 100644 docs/licenses/azure-mgmt-resource.txt delete mode 100644 docs/licenses/azure-mgmt-servicebus.txt delete mode 100644 docs/licenses/azure-mgmt-sql.txt delete mode 100644 docs/licenses/azure-mgmt-storage.txt delete mode 100644 docs/licenses/azure-mgmt-trafficmanager.txt delete mode 100644 docs/licenses/azure-mgmt-web.txt delete mode 100644 docs/licenses/azure-storage.txt delete mode 100644 docs/licenses/backports.ssl-match-hostname.txt delete mode 100644 docs/licenses/bcrypt.txt delete mode 100644 docs/licenses/boto.txt delete mode 100644 docs/licenses/boto3.txt delete mode 100644 docs/licenses/botocore.txt delete mode 100644 docs/licenses/colorama.txt delete mode 100644 docs/licenses/decorator.txt delete mode 100644 docs/licenses/dogpile.cache.txt delete mode 100644 docs/licenses/enum34.txt delete mode 100644 docs/licenses/futures.txt delete mode 100644 docs/licenses/humanfriendly.txt delete mode 100644 docs/licenses/ipaddress.txt delete mode 100644 docs/licenses/iso8601.txt delete mode 100644 docs/licenses/jmespath.txt delete mode 100644 docs/licenses/jsonpatch.txt delete mode 100644 docs/licenses/jsonpointer.txt delete mode 100644 docs/licenses/keystoneauth1.txt delete mode 100644 docs/licenses/knack.txt delete mode 100644 docs/licenses/monotonic.txt delete mode 100644 docs/licenses/munch.txt delete mode 100644 docs/licenses/ncclient.txt delete mode 100644 docs/licenses/netifaces.txt delete mode 100644 docs/licenses/ntlm-auth.txt delete mode 100644 docs/licenses/openstacksdk.txt delete mode 100644 docs/licenses/os-service-types.txt delete mode 100644 docs/licenses/ovirt-engine-sdk-python.txt delete mode 100644 docs/licenses/packaging.txt delete mode 100644 docs/licenses/paramiko.txt delete mode 100644 docs/licenses/pbr.txt delete mode 100644 docs/licenses/pycurl.txt delete mode 100644 docs/licenses/pygments.txt delete mode 100644 docs/licenses/pykerberos.txt delete mode 100644 docs/licenses/pynacl.txt delete mode 100644 docs/licenses/pyvmomi.txt delete mode 100644 docs/licenses/pywinrm.txt delete mode 100644 docs/licenses/requests-credssp.txt delete mode 100644 docs/licenses/requests-kerberos.txt delete mode 100644 docs/licenses/requests-ntlm.txt delete mode 100644 docs/licenses/requestsexceptions.txt delete mode 100644 docs/licenses/ruamel.ordereddict.txt delete mode 100644 docs/licenses/s3transfer.txt delete mode 100644 docs/licenses/selectors2.txt delete mode 100644 docs/licenses/stevedore.txt delete mode 100644 docs/licenses/tabulate.txt delete mode 100644 docs/licenses/typing.txt delete mode 100644 docs/licenses/wheel.txt delete mode 100644 docs/licenses/xmltodict.txt delete mode 100644 requirements/collections_requirements.yml delete mode 100644 requirements/requirements_ansible.in delete mode 100644 requirements/requirements_ansible.txt delete mode 100644 requirements/requirements_ansible_git.txt delete mode 100644 requirements/requirements_ansible_uninstall.txt delete mode 100755 tools/audit-api-license.sh diff --git a/Makefile b/Makefile index 858a6f67e4..337d1e461d 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,6 @@ COMPOSE_TAG ?= $(GIT_BRANCH) COMPOSE_HOST ?= $(shell hostname) VENV_BASE ?= /var/lib/awx/venv/ -COLLECTION_BASE ?= /var/lib/awx/vendor/awx_ansible_collections SCL_PREFIX ?= CELERY_SCHEDULE_FILE ?= /var/lib/awx/beat.db @@ -115,31 +114,7 @@ guard-%: exit 1; \ fi -virtualenv: virtualenv_ansible virtualenv_awx - -# virtualenv_* targets do not use --system-site-packages to prevent bugs installing packages -# but Ansible venvs are expected to have this, so that must be done after venv creation -virtualenv_ansible: - if [ "$(VENV_BASE)" ]; then \ - if [ ! -d "$(VENV_BASE)" ]; then \ - mkdir $(VENV_BASE); \ - fi; \ - if [ ! -d "$(VENV_BASE)/ansible" ]; then \ - virtualenv -p python $(VENV_BASE)/ansible && \ - $(VENV_BASE)/ansible/bin/pip install $(PIP_OPTIONS) $(VENV_BOOTSTRAP); \ - fi; \ - fi - -virtualenv_ansible_py3: - if [ "$(VENV_BASE)" ]; then \ - if [ ! -d "$(VENV_BASE)" ]; then \ - mkdir $(VENV_BASE); \ - fi; \ - if [ ! -d "$(VENV_BASE)/ansible" ]; then \ - virtualenv -p $(PYTHON) $(VENV_BASE)/ansible; \ - $(VENV_BASE)/ansible/bin/pip install $(PIP_OPTIONS) $(VENV_BOOTSTRAP); \ - fi; \ - fi +virtualenv: virtualenv_awx # flit is needed for offline install of certain packages, specifically ptyprocess # it is needed for setup, but not always recognized as a setup dependency @@ -155,32 +130,6 @@ virtualenv_awx: fi; \ fi -# --ignore-install flag is not used because *.txt files should specify exact versions -requirements_ansible: virtualenv_ansible - if [[ "$(PIP_OPTIONS)" == *"--no-index"* ]]; then \ - cat requirements/requirements_ansible.txt requirements/requirements_ansible_local.txt | PYCURL_SSL_LIBRARY=$(PYCURL_SSL_LIBRARY) $(VENV_BASE)/ansible/bin/pip install $(PIP_OPTIONS) -r /dev/stdin ; \ - else \ - cat requirements/requirements_ansible.txt requirements/requirements_ansible_git.txt | PYCURL_SSL_LIBRARY=$(PYCURL_SSL_LIBRARY) $(VENV_BASE)/ansible/bin/pip install $(PIP_OPTIONS) --no-binary $(SRC_ONLY_PKGS) -r /dev/stdin ; \ - fi - $(VENV_BASE)/ansible/bin/pip uninstall --yes -r requirements/requirements_ansible_uninstall.txt - # Same effect as using --system-site-packages flag on venv creation - rm $(shell ls -d $(VENV_BASE)/ansible/lib/python* | head -n 1)/no-global-site-packages.txt - -requirements_ansible_py3: virtualenv_ansible_py3 - if [[ "$(PIP_OPTIONS)" == *"--no-index"* ]]; then \ - cat requirements/requirements_ansible.txt requirements/requirements_ansible_local.txt | PYCURL_SSL_LIBRARY=$(PYCURL_SSL_LIBRARY) $(VENV_BASE)/ansible/bin/pip3 install $(PIP_OPTIONS) -r /dev/stdin ; \ - else \ - cat requirements/requirements_ansible.txt requirements/requirements_ansible_git.txt | PYCURL_SSL_LIBRARY=$(PYCURL_SSL_LIBRARY) $(VENV_BASE)/ansible/bin/pip3 install $(PIP_OPTIONS) --no-binary $(SRC_ONLY_PKGS) -r /dev/stdin ; \ - fi - $(VENV_BASE)/ansible/bin/pip3 uninstall --yes -r requirements/requirements_ansible_uninstall.txt - # Same effect as using --system-site-packages flag on venv creation - rm $(shell ls -d $(VENV_BASE)/ansible/lib/python* | head -n 1)/no-global-site-packages.txt - -requirements_ansible_dev: - if [ "$(VENV_BASE)" ]; then \ - $(VENV_BASE)/ansible/bin/pip install pytest mock; \ - fi - # Install third-party requirements needed for AWX's environment. # this does not use system site packages intentionally requirements_awx: virtualenv_awx @@ -194,17 +143,9 @@ requirements_awx: virtualenv_awx requirements_awx_dev: $(VENV_BASE)/awx/bin/pip install -r requirements/requirements_dev.txt -requirements_collections: - mkdir -p $(COLLECTION_BASE) - n=0; \ - until [ "$$n" -ge 5 ]; do \ - ansible-galaxy collection install -r requirements/collections_requirements.yml -p $(COLLECTION_BASE) && break; \ - n=$$((n+1)); \ - done +requirements: requirements_awx -requirements: requirements_ansible requirements_awx requirements_collections - -requirements_dev: requirements_awx requirements_ansible_py3 requirements_awx_dev requirements_ansible_dev +requirements_dev: requirements_awx requirements_awx_dev requirements_test: requirements diff --git a/awx/main/tests/functional/test_licenses.py b/awx/main/tests/functional/test_licenses.py index 757349ee13..46700d38a8 100644 --- a/awx/main/tests/functional/test_licenses.py +++ b/awx/main/tests/functional/test_licenses.py @@ -49,7 +49,7 @@ def test_python_and_js_licenses(): def read_api_requirements(path): ret = {} - for req_file in ['requirements.txt', 'requirements_ansible.txt', 'requirements_git.txt', 'requirements_ansible_git.txt']: + for req_file in ['requirements.txt', 'requirements_git.txt']: fname = '%s/%s' % (path, req_file) for reqt in parse_requirements(fname, session=''): diff --git a/awx/main/tests/unit/utils/test_common.py b/awx/main/tests/unit/utils/test_common.py index 8c07020c53..b86798ec51 100644 --- a/awx/main/tests/unit/utils/test_common.py +++ b/awx/main/tests/unit/utils/test_common.py @@ -9,9 +9,6 @@ import json import yaml from unittest import mock -from backports.tempfile import TemporaryDirectory -from django.conf import settings - from rest_framework.exceptions import ParseError from awx.main.utils import common @@ -194,24 +191,3 @@ def test_extract_ansible_vars(): redacted, var_list = common.extract_ansible_vars(json.dumps(my_dict)) assert var_list == set(['ansible_connetion_setting']) assert redacted == {"foobar": "baz"} - - -def test_get_custom_venv_choices(): - bundled_venv = os.path.join(settings.BASE_VENV_PATH, 'ansible', '') - assert sorted(common.get_custom_venv_choices()) == [bundled_venv] - - with TemporaryDirectory(dir=settings.BASE_VENV_PATH, prefix='tmp') as temp_dir: - os.makedirs(os.path.join(temp_dir, 'bin', 'activate')) - - custom_venv_dir = os.path.join(temp_dir, 'custom') - custom_venv_1 = os.path.join(custom_venv_dir, 'venv-1') - custom_venv_awx = os.path.join(custom_venv_dir, 'custom', 'awx') - - os.makedirs(os.path.join(custom_venv_1, 'bin', 'activate')) - os.makedirs(os.path.join(custom_venv_awx, 'bin', 'activate')) - - assert sorted(common.get_custom_venv_choices([custom_venv_dir])) == [ - bundled_venv, - os.path.join(temp_dir, ''), - os.path.join(custom_venv_1, '') - ] diff --git a/docs/licenses/apache-libcloud.txt b/docs/licenses/apache-libcloud.txt deleted file mode 100644 index d645695673..0000000000 --- a/docs/licenses/apache-libcloud.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/docs/licenses/appdirs.txt b/docs/licenses/appdirs.txt deleted file mode 100644 index f0bbd69f0c..0000000000 --- a/docs/licenses/appdirs.txt +++ /dev/null @@ -1,22 +0,0 @@ -# This is the MIT license - -Copyright (c) 2010 ActiveState Software Inc. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/licenses/applicationinsights.txt b/docs/licenses/applicationinsights.txt deleted file mode 100644 index cd7af0736c..0000000000 --- a/docs/licenses/applicationinsights.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/argcomplete.txt b/docs/licenses/argcomplete.txt deleted file mode 100644 index f433b1a53f..0000000000 --- a/docs/licenses/argcomplete.txt +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/docs/licenses/azure-cli-core.txt b/docs/licenses/azure-cli-core.txt deleted file mode 100644 index e9acabf9dc..0000000000 --- a/docs/licenses/azure-cli-core.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) Microsoft Corporation. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/docs/licenses/azure-cli-nspkg.txt b/docs/licenses/azure-cli-nspkg.txt deleted file mode 100644 index 2efacf37b1..0000000000 --- a/docs/licenses/azure-cli-nspkg.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) Microsoft Corporation. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-graphrbac.txt b/docs/licenses/azure-graphrbac.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-graphrbac.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-authorization.txt b/docs/licenses/azure-mgmt-authorization.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-authorization.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-automation.txt b/docs/licenses/azure-mgmt-automation.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-automation.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-batch.txt b/docs/licenses/azure-mgmt-batch.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-batch.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-cdn.txt b/docs/licenses/azure-mgmt-cdn.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-cdn.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-compute.txt b/docs/licenses/azure-mgmt-compute.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-compute.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-containerinstance.txt b/docs/licenses/azure-mgmt-containerinstance.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-containerinstance.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-containerregistry.txt b/docs/licenses/azure-mgmt-containerregistry.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-containerregistry.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-containerservice.txt b/docs/licenses/azure-mgmt-containerservice.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-containerservice.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-cosmosdb.txt b/docs/licenses/azure-mgmt-cosmosdb.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-cosmosdb.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-devtestlabs.txt b/docs/licenses/azure-mgmt-devtestlabs.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-devtestlabs.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-dns.txt b/docs/licenses/azure-mgmt-dns.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-dns.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-hdinsight.txt b/docs/licenses/azure-mgmt-hdinsight.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-hdinsight.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-iothub.txt b/docs/licenses/azure-mgmt-iothub.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-iothub.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-keyvault.txt b/docs/licenses/azure-mgmt-keyvault.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-keyvault.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-loganalytics.txt b/docs/licenses/azure-mgmt-loganalytics.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-loganalytics.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-marketplaceordering.txt b/docs/licenses/azure-mgmt-marketplaceordering.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-marketplaceordering.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-monitor.txt b/docs/licenses/azure-mgmt-monitor.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-monitor.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-network.txt b/docs/licenses/azure-mgmt-network.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-network.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-nspkg.txt b/docs/licenses/azure-mgmt-nspkg.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-nspkg.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-rdbms.txt b/docs/licenses/azure-mgmt-rdbms.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-rdbms.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-redis.txt b/docs/licenses/azure-mgmt-redis.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-redis.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-resource.txt b/docs/licenses/azure-mgmt-resource.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-resource.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-servicebus.txt b/docs/licenses/azure-mgmt-servicebus.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-servicebus.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-sql.txt b/docs/licenses/azure-mgmt-sql.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-sql.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-storage.txt b/docs/licenses/azure-mgmt-storage.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-storage.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-trafficmanager.txt b/docs/licenses/azure-mgmt-trafficmanager.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-trafficmanager.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-mgmt-web.txt b/docs/licenses/azure-mgmt-web.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-mgmt-web.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/azure-storage.txt b/docs/licenses/azure-storage.txt deleted file mode 100644 index dc1cf39d13..0000000000 --- a/docs/licenses/azure-storage.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/backports.ssl-match-hostname.txt b/docs/licenses/backports.ssl-match-hostname.txt deleted file mode 100644 index f73bd13921..0000000000 --- a/docs/licenses/backports.ssl-match-hostname.txt +++ /dev/null @@ -1,96 +0,0 @@ -PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 - - 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated documentation. - - 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation; All Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee. - - 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python. - - 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. - - 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - - 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. - - 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. - - 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this License Agreement. - - -BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 - -BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 - - 1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the Individual or Organization ("Licensee") accessing and otherwise using this software in source or binary form and its associated documentation ("the Software"). - - 2. Subject to the terms and conditions of this BeOpen Python License Agreement, BeOpen hereby grants Licensee a non-exclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use the Software alone or in any derivative version, provided, however, that the BeOpen Python License is retained in the Software, alone or in any derivative version prepared by Licensee. - - 3. BeOpen is making the Software available to Licensee on an "AS IS" basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. - - 4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - - 5. This License Agreement will automatically terminate upon a material breach of its terms and conditions. - - 6. This License Agreement shall be governed by and interpreted in all respects by the law of the State of California, excluding conflict of law provisions. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between BeOpen and Licensee. This License Agreement does not grant permission to use BeOpen trademarks or trade names in a trademark sense to endorse or promote products or services of Licensee, or any third party. As an exception, the "BeOpen Python" logos available at http://www.pythonlabs.com/logos.html may be used according to the permissions granted on that web page. - - 7. By copying, installing or otherwise using the software, Licensee agrees to be bound by the terms and conditions of this License Agreement. - - -CNRI OPEN SOURCE LICENSE AGREEMENT (for Python 1.6b1) - -IMPORTANT: PLEASE READ THE FOLLOWING AGREEMENT CAREFULLY. - -BY CLICKING ON "ACCEPT" WHERE INDICATED BELOW, OR BY COPYING, INSTALLING OR OTHERWISE USING PYTHON 1.6, beta 1 SOFTWARE, YOU ARE DEEMED TO HAVE AGREED TO THE TERMS AND CONDITIONS OF THIS LICENSE AGREEMENT. - - 1. This LICENSE AGREEMENT is between the Corporation for National Research Initiatives, having an office at 1895 Preston White Drive, Reston, VA 20191 ("CNRI"), and the Individual or Organization ("Licensee") accessing and otherwise using Python 1.6, beta 1 software in source or binary form and its associated documentation, as released at the www.python.org Internet site on August 4, 2000 ("Python 1.6b1"). - - 2. Subject to the terms and conditions of this License Agreement, CNRI hereby grants Licensee a non-exclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python 1.6b1 alone or in any derivative version, provided, however, that CNRIs License Agreement is retained in Python 1.6b1, alone or in any derivative version prepared by Licensee. - - Alternately, in lieu of CNRIs License Agreement, Licensee may substitute the following text (omitting the quotes): "Python 1.6, beta 1, is made available subject to the terms and conditions in CNRIs License Agreement. This Agreement may be located on the Internet using the following unique, persistent identifier (known as a handle): 1895.22/1011. This Agreement may also be obtained from a proxy server on the Internet using the URL:http://hdl.handle.net/1895.22/1011". - - 3. In the event Licensee prepares a derivative work that is based on or incorporates Python 1.6b1 or any part thereof, and wants to make the derivative work available to the public as provided herein, then Licensee hereby agrees to indicate in any such work the nature of the modifications made to Python 1.6b1. - - 4. CNRI is making Python 1.6b1 available to Licensee on an "AS IS" basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6b1 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. - - 5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF USING, MODIFYING OR DISTRIBUTING PYTHON 1.6b1, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - - 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. - - 7. This License Agreement shall be governed by and interpreted in all respects by the law of the State of Virginia, excluding conflict of law provisions. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between CNRI and Licensee. This License Agreement does not grant permission to use CNRI trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. - - 8. By clicking on the "ACCEPT" button where indicated, or by copying, installing or otherwise using Python 1.6b1, Licensee agrees to be bound by the terms and conditions of this License Agreement. - -ACCEPT - - -CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 - -Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, The Netherlands. All rights reserved. - - Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Stichting Mathematisch Centrum or CWI not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. - - STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - or -Zope Public License (ZPL) Version 2.0 - -This software is Copyright (c) Zope Corporation (tm) and Contributors. All rights reserved. - -This license has been certified as open source. It has also been designated as GPL compatible by the Free Software Foundation (FSF). - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - 1. Redistributions in source code must retain the above copyright notice, this list of conditions, and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions, and the following disclaimer in the documentation and/or other materials provided with the distribution. - - 3. The name Zope Corporation (tm) must not be used to endorse or promote products derived from this software without prior written permission from Zope Corporation. - - 4. The right to distribute this software or to use it for any purpose does not give you the right to use Servicemarks (sm) or Trademarks (tm) of Zope Corporation. Use of them is covered in a separate agreement (see http://www.zope.com/Marks). - - 5. If any files are modified, you must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. - -Disclaimer - -THIS SOFTWARE IS PROVIDED BY ZOPE CORPORATION ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ZOPE CORPORATION OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -This software consists of contributions made by Zope Corporation and many individuals on behalf of Zope Corporation. Specific attributions are listed in the accompanying credits file. \ No newline at end of file diff --git a/docs/licenses/bcrypt.txt b/docs/licenses/bcrypt.txt deleted file mode 100644 index e4f139cb49..0000000000 --- a/docs/licenses/bcrypt.txt +++ /dev/null @@ -1,201 +0,0 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, -and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by -the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all -other entities that control, are controlled by, or are under common -control with that entity. For the purposes of this definition, -"control" means (i) the power, direct or indirect, to cause the -direction or management of such entity, whether by contract or -otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity -exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, -including but not limited to software source code, documentation -source, and configuration files. - -"Object" form shall mean any form resulting from mechanical -transformation or translation of a Source form, including but -not limited to compiled object code, generated documentation, -and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or -Object form, made available under the License, as indicated by a -copyright notice that is included in or attached to the work -(an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object -form, that is based on (or derived from) the Work and for which the -editorial revisions, annotations, elaborations, or other modifications -represent, as a whole, an original work of authorship. For the purposes -of this License, Derivative Works shall not include works that remain -separable from, or merely link (or bind by name) to the interfaces of, -the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including -the original version of the Work and any modifications or additions -to that Work or Derivative Works thereof, that is intentionally -submitted to Licensor for inclusion in the Work by the copyright owner -or by an individual or Legal Entity authorized to submit on behalf of -the copyright owner. For the purposes of this definition, "submitted" -means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, -and issue tracking systems that are managed by, or on behalf of, the -Licensor for the purpose of discussing and improving the Work, but -excluding communication that is conspicuously marked or otherwise -designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity -on behalf of whom a Contribution has been received by Licensor and -subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the -Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -(except as stated in this section) patent license to make, have made, -use, offer to sell, sell, import, and otherwise transfer the Work, -where such license applies only to those patent claims licensable -by such Contributor that are necessarily infringed by their -Contribution(s) alone or by combination of their Contribution(s) -with the Work to which such Contribution(s) was submitted. If You -institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work -or a Contribution incorporated within the Work constitutes direct -or contributory patent infringement, then any patent licenses -granted to You under this License for that Work shall terminate -as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the -Work or Derivative Works thereof in any medium, with or without -modifications, and in Source or Object form, provided that You -meet the following conditions: - -(a) You must give any other recipients of the Work or -Derivative Works a copy of this License; and - -(b) You must cause any modified files to carry prominent notices -stating that You changed the files; and - -(c) You must retain, in the Source form of any Derivative Works -that You distribute, all copyright, patent, trademark, and -attribution notices from the Source form of the Work, -excluding those notices that do not pertain to any part of -the Derivative Works; and - -(d) If the Work includes a "NOTICE" text file as part of its -distribution, then any Derivative Works that You distribute must -include a readable copy of the attribution notices contained -within such NOTICE file, excluding those notices that do not -pertain to any part of the Derivative Works, in at least one -of the following places: within a NOTICE text file distributed -as part of the Derivative Works; within the Source form or -documentation, if provided along with the Derivative Works; or, -within a display generated by the Derivative Works, if and -wherever such third-party notices normally appear. The contents -of the NOTICE file are for informational purposes only and -do not modify the License. You may add Your own attribution -notices within Derivative Works that You distribute, alongside -or as an addendum to the NOTICE text from the Work, provided -that such additional attribution notices cannot be construed -as modifying the License. - -You may add Your own copyright statement to Your modifications and -may provide additional or different license terms and conditions -for use, reproduction, or distribution of Your modifications, or -for any such Derivative Works as a whole, provided Your use, -reproduction, and distribution of the Work otherwise complies with -the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, -any Contribution intentionally submitted for inclusion in the Work -by You to the Licensor shall be under the terms and conditions of -this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify -the terms of any separate license agreement you may have executed -with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade -names, trademarks, service marks, or product names of the Licensor, -except as required for reasonable and customary use in describing the -origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or -agreed to in writing, Licensor provides the Work (and each -Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -implied, including, without limitation, any warranties or conditions -of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A -PARTICULAR PURPOSE. You are solely responsible for determining the -appropriateness of using or redistributing the Work and assume any -risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, -whether in tort (including negligence), contract, or otherwise, -unless required by applicable law (such as deliberate and grossly -negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, -incidental, or consequential damages of any character arising as a -result of this License or out of the use or inability to use the -Work (including but not limited to damages for loss of goodwill, -work stoppage, computer failure or malfunction, or any and all -other commercial damages or losses), even if such Contributor -has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing -the Work or Derivative Works thereof, You may choose to offer, -and charge a fee for, acceptance of support, warranty, indemnity, -or other liability obligations and/or rights consistent with this -License. However, in accepting such obligations, You may act only -on Your own behalf and on Your sole responsibility, not on behalf -of any other Contributor, and only if You agree to indemnify, -defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason -of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following -boilerplate notice, with the fields enclosed by brackets "[]" -replaced with your own identifying information. (Don't include -the brackets!) The text should be enclosed in the appropriate -comment syntax for the file format. We also recommend that a -file or class name and description of purpose be included on the -same "printed page" as the copyright notice for easier -identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. \ No newline at end of file diff --git a/docs/licenses/boto.txt b/docs/licenses/boto.txt deleted file mode 100644 index 07d9e8c75f..0000000000 --- a/docs/licenses/boto.txt +++ /dev/null @@ -1,18 +0,0 @@ -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, dis- -tribute, sublicense, and/or sell copies of the Software, and to permit -persons to whom the Software is furnished to do so, subject to the fol- -lowing conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- -ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. diff --git a/docs/licenses/boto3.txt b/docs/licenses/boto3.txt deleted file mode 100644 index 761ea7cfcd..0000000000 --- a/docs/licenses/boto3.txt +++ /dev/null @@ -1,12 +0,0 @@ -Copyright 2013-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - -Licensed under the Apache License, Version 2.0 (the "License"). You -may not use this file except in compliance with the License. A copy of -the License is located at - - http://aws.amazon.com/apache2.0/ - -or in the "license" file accompanying this file. This file is -distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -ANY KIND, either express or implied. See the License for the specific -language governing permissions and limitations under the License. diff --git a/docs/licenses/botocore.txt b/docs/licenses/botocore.txt deleted file mode 100644 index 4c00dd5630..0000000000 --- a/docs/licenses/botocore.txt +++ /dev/null @@ -1,12 +0,0 @@ -Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - -Licensed under the Apache License, Version 2.0 (the "License"). You -may not use this file except in compliance with the License. A copy of -the License is located at - - http://aws.amazon.com/apache2.0/ - -or in the "license" file accompanying this file. This file is -distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -ANY KIND, either express or implied. See the License for the specific -language governing permissions and limitations under the License. diff --git a/docs/licenses/colorama.txt b/docs/licenses/colorama.txt deleted file mode 100644 index 5f567799f3..0000000000 --- a/docs/licenses/colorama.txt +++ /dev/null @@ -1,28 +0,0 @@ -Copyright (c) 2010 Jonathan Hartley -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holders, nor those of its contributors - may be used to endorse or promote products derived from this software without - specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - diff --git a/docs/licenses/decorator.txt b/docs/licenses/decorator.txt deleted file mode 100644 index e9fe3d98df..0000000000 --- a/docs/licenses/decorator.txt +++ /dev/null @@ -1,26 +0,0 @@ -Copyright (c) 2005-2017, Michele Simionato -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - Redistributions in bytecode form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the - distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS -OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR -TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH -DAMAGE. diff --git a/docs/licenses/dogpile.cache.txt b/docs/licenses/dogpile.cache.txt deleted file mode 100644 index 79399ef2e2..0000000000 --- a/docs/licenses/dogpile.cache.txt +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2011-2016 Mike Bayer - -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -3. The name of the author or contributors may not be used to endorse or - promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -SUCH DAMAGE. diff --git a/docs/licenses/enum34.txt b/docs/licenses/enum34.txt deleted file mode 100644 index 9003b8850e..0000000000 --- a/docs/licenses/enum34.txt +++ /dev/null @@ -1,32 +0,0 @@ -Copyright (c) 2013, Ethan Furman. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - - Redistributions of source code must retain the above - copyright notice, this list of conditions and the - following disclaimer. - - Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials - provided with the distribution. - - Neither the name Ethan Furman nor the names of any - contributors may be used to endorse or promote products - derived from this software without specific prior written - permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/futures.txt b/docs/licenses/futures.txt deleted file mode 100644 index a8d65b16b6..0000000000 --- a/docs/licenses/futures.txt +++ /dev/null @@ -1,48 +0,0 @@ -PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 --------------------------------------------- - -1. This LICENSE AGREEMENT is between the Python Software Foundation -("PSF"), and the Individual or Organization ("Licensee") accessing and -otherwise using this software ("Python") in source or binary form and -its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, PSF -hereby grants Licensee a nonexclusive, royalty-free, world-wide -license to reproduce, analyze, test, perform and/or display publicly, -prepare derivative works, distribute, and otherwise use Python -alone or in any derivative version, provided, however, that PSF's -License Agreement and PSF's notice of copyright, i.e., "Copyright (c) -2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation; All Rights -Reserved" are retained in Python alone or in any derivative version -prepared by Licensee. - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python. - -4. PSF is making Python available to Licensee on an "AS IS" -basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. Nothing in this License Agreement shall be deemed to create any -relationship of agency, partnership, or joint venture between PSF and -Licensee. This License Agreement does not grant permission to use PSF -trademarks or trade name in a trademark sense to endorse or promote -products or services of Licensee, or any third party. - -8. By copying, installing or otherwise using Python, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. diff --git a/docs/licenses/humanfriendly.txt b/docs/licenses/humanfriendly.txt deleted file mode 100644 index 9b3290e3d2..0000000000 --- a/docs/licenses/humanfriendly.txt +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2018 Peter Odding - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/docs/licenses/ipaddress.txt b/docs/licenses/ipaddress.txt deleted file mode 100644 index c1a4d7bffd..0000000000 --- a/docs/licenses/ipaddress.txt +++ /dev/null @@ -1,50 +0,0 @@ -This package is a modified version of cpython's ipaddress module. -It is therefore distributed under the PSF license, as follows: - -PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 --------------------------------------------- - -1. This LICENSE AGREEMENT is between the Python Software Foundation -("PSF"), and the Individual or Organization ("Licensee") accessing and -otherwise using this software ("Python") in source or binary form and -its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, PSF hereby -grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, -analyze, test, perform and/or display publicly, prepare derivative works, -distribute, and otherwise use Python alone or in any derivative version, -provided, however, that PSF's License Agreement and PSF's notice of copyright, -i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -2011, 2012, 2013, 2014 Python Software Foundation; All Rights Reserved" are -retained in Python alone or in any derivative version prepared by Licensee. - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python. - -4. PSF is making Python available to Licensee on an "AS IS" -basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. Nothing in this License Agreement shall be deemed to create any -relationship of agency, partnership, or joint venture between PSF and -Licensee. This License Agreement does not grant permission to use PSF -trademarks or trade name in a trademark sense to endorse or promote -products or services of Licensee, or any third party. - -8. By copying, installing or otherwise using Python, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. diff --git a/docs/licenses/iso8601.txt b/docs/licenses/iso8601.txt deleted file mode 100644 index 4eb54eaff9..0000000000 --- a/docs/licenses/iso8601.txt +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2007 - 2015 Michael Twomey - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/licenses/jmespath.txt b/docs/licenses/jmespath.txt deleted file mode 100644 index aa68928536..0000000000 --- a/docs/licenses/jmespath.txt +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, dis- -tribute, sublicense, and/or sell copies of the Software, and to permit -persons to whom the Software is furnished to do so, subject to the fol- -lowing conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- -ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. diff --git a/docs/licenses/jsonpatch.txt b/docs/licenses/jsonpatch.txt deleted file mode 100644 index 91404e3cea..0000000000 --- a/docs/licenses/jsonpatch.txt +++ /dev/null @@ -1,25 +0,0 @@ -Copyright (c) 2011 Stefan Kögl -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -3. The name of the author may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/jsonpointer.txt b/docs/licenses/jsonpointer.txt deleted file mode 100644 index 91404e3cea..0000000000 --- a/docs/licenses/jsonpointer.txt +++ /dev/null @@ -1,25 +0,0 @@ -Copyright (c) 2011 Stefan Kögl -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -3. The name of the author may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/keystoneauth1.txt b/docs/licenses/keystoneauth1.txt deleted file mode 100644 index af613b1332..0000000000 --- a/docs/licenses/keystoneauth1.txt +++ /dev/null @@ -1,209 +0,0 @@ -Copyright (c) 2009 Jacob Kaplan-Moss - initial codebase (< v2.1) -Copyright (c) 2011 Rackspace - OpenStack extensions (>= v2.1) -Copyright (c) 2011 Nebula, Inc - Keystone refactor (>= v2.7) -All rights reserved. - - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - ---- License for keystoneauth versions prior to 2.1 --- - -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - 3. Neither the name of this project nor the names of its contributors may - be used to endorse or promote products derived from this software without - specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/knack.txt b/docs/licenses/knack.txt deleted file mode 100644 index d1ca00f20a..0000000000 --- a/docs/licenses/knack.txt +++ /dev/null @@ -1,21 +0,0 @@ - MIT License - - Copyright (c) Microsoft Corporation. All rights reserved. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE \ No newline at end of file diff --git a/docs/licenses/monotonic.txt b/docs/licenses/monotonic.txt deleted file mode 100644 index 5c304d1a4a..0000000000 --- a/docs/licenses/monotonic.txt +++ /dev/null @@ -1,201 +0,0 @@ -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/docs/licenses/munch.txt b/docs/licenses/munch.txt deleted file mode 100644 index 20c5c7dcd0..0000000000 --- a/docs/licenses/munch.txt +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2010 David Schoonover - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/docs/licenses/ncclient.txt b/docs/licenses/ncclient.txt deleted file mode 100644 index d645695673..0000000000 --- a/docs/licenses/ncclient.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/docs/licenses/netifaces.txt b/docs/licenses/netifaces.txt deleted file mode 100644 index 67c7bfce25..0000000000 --- a/docs/licenses/netifaces.txt +++ /dev/null @@ -1,4 +0,0 @@ -Copyright (c) 2007-2014 Alastair Houghton -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/licenses/ntlm-auth.txt b/docs/licenses/ntlm-auth.txt deleted file mode 100644 index 21429cb1fa..0000000000 --- a/docs/licenses/ntlm-auth.txt +++ /dev/null @@ -1,163 +0,0 @@ -GNU Lesser General Public License -================================= - -_Version 3, 29 June 2007_ -_Copyright © 2007 Free Software Foundation, Inc. <>_ - -Everyone is permitted to copy and distribute verbatim copies -of this license document, but changing it is not allowed. - - -This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - -### 0. Additional Definitions - -As used herein, “this License” refers to version 3 of the GNU Lesser -General Public License, and the “GNU GPL” refers to version 3 of the GNU -General Public License. - -“The Library” refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - -An “Application” is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - -A “Combined Work” is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the “Linked -Version”. - -The “Minimal Corresponding Source” for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - -The “Corresponding Application Code” for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - -### 1. Exception to Section 3 of the GNU GPL - -You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - -### 2. Conveying Modified Versions - -If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - -* **a)** under this License, provided that you make a good faith effort to -ensure that, in the event an Application does not supply the -function or data, the facility still operates, and performs -whatever part of its purpose remains meaningful, or - -* **b)** under the GNU GPL, with none of the additional permissions of -this License applicable to that copy. - -### 3. Object Code Incorporating Material from Library Header Files - -The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - -* **a)** Give prominent notice with each copy of the object code that the -Library is used in it and that the Library and its use are -covered by this License. -* **b)** Accompany the object code with a copy of the GNU GPL and this license -document. - -### 4. Combined Works - -You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - -* **a)** Give prominent notice with each copy of the Combined Work that -the Library is used in it and that the Library and its use are -covered by this License. - -* **b)** Accompany the Combined Work with a copy of the GNU GPL and this license -document. - -* **c)** For a Combined Work that displays copyright notices during -execution, include the copyright notice for the Library among -these notices, as well as a reference directing the user to the -copies of the GNU GPL and this license document. - -* **d)** Do one of the following: - - **0)** Convey the Minimal Corresponding Source under the terms of this -License, and the Corresponding Application Code in a form -suitable for, and under terms that permit, the user to -recombine or relink the Application with a modified version of -the Linked Version to produce a modified Combined Work, in the -manner specified by section 6 of the GNU GPL for conveying -Corresponding Source. - - **1)** Use a suitable shared library mechanism for linking with the -Library. A suitable mechanism is one that **(a)** uses at run time -a copy of the Library already present on the user's computer -system, and **(b)** will operate properly with a modified version -of the Library that is interface-compatible with the Linked -Version. - -* **e)** Provide Installation Information, but only if you would otherwise -be required to provide such information under section 6 of the -GNU GPL, and only to the extent that such information is -necessary to install and execute a modified version of the -Combined Work produced by recombining or relinking the -Application with a modified version of the Linked Version. (If -you use option **4d0**, the Installation Information must accompany -the Minimal Corresponding Source and Corresponding Application -Code. If you use option **4d1**, you must provide the Installation -Information in the manner specified by section 6 of the GNU GPL -for conveying Corresponding Source.) - -### 5. Combined Libraries - -You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - -* **a)** Accompany the combined library with a copy of the same work based -on the Library, uncombined with any other library facilities, -conveyed under the terms of this License. -* **b)** Give prominent notice with the combined library that part of it -is a work based on the Library, and explaining where to find the -accompanying uncombined form of the same work. - -### 6. Revised Versions of the GNU Lesser General Public License - -The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License “or any later version” -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - -If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. \ No newline at end of file diff --git a/docs/licenses/openstacksdk.txt b/docs/licenses/openstacksdk.txt deleted file mode 100644 index 67db858821..0000000000 --- a/docs/licenses/openstacksdk.txt +++ /dev/null @@ -1,175 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. diff --git a/docs/licenses/os-service-types.txt b/docs/licenses/os-service-types.txt deleted file mode 100644 index 67db858821..0000000000 --- a/docs/licenses/os-service-types.txt +++ /dev/null @@ -1,175 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. diff --git a/docs/licenses/ovirt-engine-sdk-python.txt b/docs/licenses/ovirt-engine-sdk-python.txt deleted file mode 100644 index 4947287f7b..0000000000 --- a/docs/licenses/ovirt-engine-sdk-python.txt +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/docs/licenses/packaging.txt b/docs/licenses/packaging.txt deleted file mode 100644 index f433b1a53f..0000000000 --- a/docs/licenses/packaging.txt +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/docs/licenses/paramiko.txt b/docs/licenses/paramiko.txt deleted file mode 100644 index 733f96c17a..0000000000 --- a/docs/licenses/paramiko.txt +++ /dev/null @@ -1,503 +0,0 @@ -GNU LESSER GENERAL PUBLIC LICENSE - Version 2.1, February 1999 - -Copyright (C) 1991, 1999 Free Software Foundation, Inc. -51 Franklin Street, Suite 500, Boston, MA 02110-1335 USA -Everyone is permitted to copy and distribute verbatim copies -of this license document, but changing it is not allowed. - -[This is the first released version of the Lesser GPL. It also counts -as the successor of the GNU Library Public License, version 2, hence -the version number 2.1.] - - Preamble - -The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -Licenses are intended to guarantee your freedom to share and change -free software--to make sure the software is free for all its users. - -This license, the Lesser General Public License, applies to some -specially designated software packages--typically libraries--of the -Free Software Foundation and other authors who decide to use it. You -can use it too, but we suggest you first think carefully about whether -this license or the ordinary General Public License is the better -strategy to use in any particular case, based on the explanations below. - -When we speak of free software, we are referring to freedom of use, -not price. Our General Public Licenses are designed to make sure that -you have the freedom to distribute copies of free software (and charge -for this service if you wish); that you receive source code or can get -it if you want it; that you can change the software and use pieces of -it in new free programs; and that you are informed that you can do -these things. - -To protect your rights, we need to make restrictions that forbid -distributors to deny you these rights or to ask you to surrender these -rights. These restrictions translate to certain responsibilities for -you if you distribute copies of the library or if you modify it. - -For example, if you distribute copies of the library, whether gratis -or for a fee, you must give the recipients all the rights that we gave -you. You must make sure that they, too, receive or can get the source -code. If you link other code with the library, you must provide -complete object files to the recipients, so that they can relink them -with the library after making changes to the library and recompiling -it. And you must show them these terms so they know their rights. - -We protect your rights with a two-step method: (1) we copyright the -library, and (2) we offer you this license, which gives you legal -permission to copy, distribute and/or modify the library. - -To protect each distributor, we want to make it very clear that -there is no warranty for the free library. Also, if the library is -modified by someone else and passed on, the recipients should know -that what they have is not the original version, so that the original -author's reputation will not be affected by problems that might be -introduced by others. - -Finally, software patents pose a constant threat to the existence of -any free program. We wish to make sure that a company cannot -effectively restrict the users of a free program by obtaining a -restrictive license from a patent holder. Therefore, we insist that -any patent license obtained for a version of the library must be -consistent with the full freedom of use specified in this license. - -Most GNU software, including some libraries, is covered by the -ordinary GNU General Public License. This license, the GNU Lesser -General Public License, applies to certain designated libraries, and -is quite different from the ordinary General Public License. We use -this license for certain libraries in order to permit linking those -libraries into non-free programs. - -When a program is linked with a library, whether statically or using -a shared library, the combination of the two is legally speaking a -combined work, a derivative of the original library. The ordinary -General Public License therefore permits such linking only if the -entire combination fits its criteria of freedom. The Lesser General -Public License permits more lax criteria for linking other code with -the library. - -We call this license the "Lesser" General Public License because it -does Less to protect the user's freedom than the ordinary General -Public License. It also provides other free software developers Less -of an advantage over competing non-free programs. These disadvantages -are the reason we use the ordinary General Public License for many -libraries. However, the Lesser license provides advantages in certain -special circumstances. - -For example, on rare occasions, there may be a special need to -encourage the widest possible use of a certain library, so that it becomes -a de-facto standard. To achieve this, non-free programs must be -allowed to use the library. A more frequent case is that a free -library does the same job as widely used non-free libraries. In this -case, there is little to gain by limiting the free library to free -software only, so we use the Lesser General Public License. - -In other cases, permission to use a particular library in non-free -programs enables a greater number of people to use a large body of -free software. For example, permission to use the GNU C Library in -non-free programs enables many more people to use the whole GNU -operating system, as well as its variant, the GNU/Linux operating -system. - -Although the Lesser General Public License is Less protective of the -users' freedom, it does ensure that the user of a program that is -linked with the Library has the freedom and the wherewithal to run -that program using a modified version of the Library. - -The precise terms and conditions for copying, distribution and -modification follow. Pay close attention to the difference between a -"work based on the library" and a "work that uses the library". The -former contains code derived from the library, whereas the latter must -be combined with the library in order to run. - -GNU LESSER GENERAL PUBLIC LICENSE -TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - -0. This License Agreement applies to any software library or other -program which contains a notice placed by the copyright holder or -other authorized party saying it may be distributed under the terms of -this Lesser General Public License (also called "this License"). -Each licensee is addressed as "you". - -A "library" means a collection of software functions and/or data -prepared so as to be conveniently linked with application programs -(which use some of those functions and data) to form executables. - -The "Library", below, refers to any such software library or work -which has been distributed under these terms. A "work based on the -Library" means either the Library or any derivative work under -copyright law: that is to say, a work containing the Library or a -portion of it, either verbatim or with modifications and/or translated -straightforwardly into another language. (Hereinafter, translation is -included without limitation in the term "modification".) - -"Source code" for a work means the preferred form of the work for -making modifications to it. For a library, complete source code means -all the source code for all modules it contains, plus any associated -interface definition files, plus the scripts used to control compilation -and installation of the library. - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running a program using the Library is not restricted, and output from -such a program is covered only if its contents constitute a work based -on the Library (independent of the use of the Library in a tool for -writing it). Whether that is true depends on what the Library does -and what the program that uses the Library does. - -1. You may copy and distribute verbatim copies of the Library's -complete source code as you receive it, in any medium, provided that -you conspicuously and appropriately publish on each copy an -appropriate copyright notice and disclaimer of warranty; keep intact -all the notices that refer to this License and to the absence of any -warranty; and distribute a copy of this License along with the -Library. - -You may charge a fee for the physical act of transferring a copy, -and you may at your option offer warranty protection in exchange for a -fee. - -2. You may modify your copy or copies of the Library or any portion -of it, thus forming a work based on the Library, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - -a) The modified work must itself be a software library. - -b) You must cause the files modified to carry prominent notices -stating that you changed the files and the date of any change. - -c) You must cause the whole of the work to be licensed at no -charge to all third parties under the terms of this License. - -d) If a facility in the modified Library refers to a function or a -table of data to be supplied by an application program that uses -the facility, other than as an argument passed when the facility -is invoked, then you must make a good faith effort to ensure that, -in the event an application does not supply such function or -table, the facility still operates, and performs whatever part of -its purpose remains meaningful. - -(For example, a function in a library to compute square roots has -a purpose that is entirely well-defined independent of the -application. Therefore, Subsection 2d requires that any -application-supplied function or table used by this function must -be optional: if the application does not supply it, the square -root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Library, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Library, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote -it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library -with the Library (or with a work based on the Library) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - -3. You may opt to apply the terms of the ordinary GNU General Public -License instead of this License to a given copy of the Library. To do -this, you must alter all the notices that refer to this License, so -that they refer to the ordinary GNU General Public License, version 2, -instead of to this License. (If a newer version than version 2 of the -ordinary GNU General Public License has appeared, then you can specify -that version instead if you wish.) Do not make any other change in -these notices. - -Once this change is made in a given copy, it is irreversible for -that copy, so the ordinary GNU General Public License applies to all -subsequent copies and derivative works made from that copy. - -This option is useful when you wish to copy part of the code of -the Library into a program that is not a library. - -4. You may copy and distribute the Library (or a portion or -derivative of it, under Section 2) in object code or executable form -under the terms of Sections 1 and 2 above provided that you accompany -it with the complete corresponding machine-readable source code, which -must be distributed under the terms of Sections 1 and 2 above on a -medium customarily used for software interchange. - -If distribution of object code is made by offering access to copy -from a designated place, then offering equivalent access to copy the -source code from the same place satisfies the requirement to -distribute the source code, even though third parties are not -compelled to copy the source along with the object code. - -5. A program that contains no derivative of any portion of the -Library, but is designed to work with the Library by being compiled or -linked with it, is called a "work that uses the Library". Such a -work, in isolation, is not a derivative work of the Library, and -therefore falls outside the scope of this License. - -However, linking a "work that uses the Library" with the Library -creates an executable that is a derivative of the Library (because it -contains portions of the Library), rather than a "work that uses the -library". The executable is therefore covered by this License. -Section 6 states terms for distribution of such executables. - -When a "work that uses the Library" uses material from a header file -that is part of the Library, the object code for the work may be a -derivative work of the Library even though the source code is not. -Whether this is true is especially significant if the work can be -linked without the Library, or if the work is itself a library. The -threshold for this to be true is not precisely defined by law. - -If such an object file uses only numerical parameters, data -structure layouts and accessors, and small macros and small inline -functions (ten lines or less in length), then the use of the object -file is unrestricted, regardless of whether it is legally a derivative -work. (Executables containing this object code plus portions of the -Library will still fall under Section 6.) - -Otherwise, if the work is a derivative of the Library, you may -distribute the object code for the work under the terms of Section 6. -Any executables containing that work also fall under Section 6, -whether or not they are linked directly with the Library itself. - -6. As an exception to the Sections above, you may also combine or -link a "work that uses the Library" with the Library to produce a -work containing portions of the Library, and distribute that work -under terms of your choice, provided that the terms permit -modification of the work for the customer's own use and reverse -engineering for debugging such modifications. - -You must give prominent notice with each copy of the work that the -Library is used in it and that the Library and its use are covered by -this License. You must supply a copy of this License. If the work -during execution displays copyright notices, you must include the -copyright notice for the Library among them, as well as a reference -directing the user to the copy of this License. Also, you must do one -of these things: - -a) Accompany the work with the complete corresponding -machine-readable source code for the Library including whatever -changes were used in the work (which must be distributed under -Sections 1 and 2 above); and, if the work is an executable linked -with the Library, with the complete machine-readable "work that -uses the Library", as object code and/or source code, so that the -user can modify the Library and then relink to produce a modified -executable containing the modified Library. (It is understood -that the user who changes the contents of definitions files in the -Library will not necessarily be able to recompile the application -to use the modified definitions.) - -b) Use a suitable shared library mechanism for linking with the -Library. A suitable mechanism is one that (1) uses at run time a -copy of the library already present on the user's computer system, -rather than copying library functions into the executable, and (2) -will operate properly with a modified version of the library, if -the user installs one, as long as the modified version is -interface-compatible with the version that the work was made with. - -c) Accompany the work with a written offer, valid for at -least three years, to give the same user the materials -specified in Subsection 6a, above, for a charge no more -than the cost of performing this distribution. - -d) If distribution of the work is made by offering access to copy -from a designated place, offer equivalent access to copy the above -specified materials from the same place. - -e) Verify that the user has already received a copy of these -materials or that you have already sent this user a copy. - -For an executable, the required form of the "work that uses the -Library" must include any data and utility programs needed for -reproducing the executable from it. However, as a special exception, -the materials to be distributed need not include anything that is -normally distributed (in either source or binary form) with the major -components (compiler, kernel, and so on) of the operating system on -which the executable runs, unless that component itself accompanies -the executable. - -It may happen that this requirement contradicts the license -restrictions of other proprietary libraries that do not normally -accompany the operating system. Such a contradiction means you cannot -use both them and the Library together in an executable that you -distribute. - -7. You may place library facilities that are a work based on the -Library side-by-side in a single library together with other library -facilities not covered by this License, and distribute such a combined -library, provided that the separate distribution of the work based on -the Library and of the other library facilities is otherwise -permitted, and provided that you do these two things: - -a) Accompany the combined library with a copy of the same work -based on the Library, uncombined with any other library -facilities. This must be distributed under the terms of the -Sections above. - -b) Give prominent notice with the combined library of the fact -that part of it is a work based on the Library, and explaining -where to find the accompanying uncombined form of the same work. - -8. You may not copy, modify, sublicense, link with, or distribute -the Library except as expressly provided under this License. Any -attempt otherwise to copy, modify, sublicense, link with, or -distribute the Library is void, and will automatically terminate your -rights under this License. However, parties who have received copies, -or rights, from you under this License will not have their licenses -terminated so long as such parties remain in full compliance. - -9. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Library or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Library (or any work based on the -Library), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Library or works based on it. - -10. Each time you redistribute the Library (or any work based on the -Library), the recipient automatically receives a license from the -original licensor to copy, distribute, link with or modify the Library -subject to these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties with -this License. - -11. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Library at all. For example, if a patent -license would not permit royalty-free redistribution of the Library by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any -particular circumstance, the balance of the section is intended to apply, -and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - -12. If the distribution and/or use of the Library is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Library under this License may add -an explicit geographical distribution limitation excluding those countries, -so that distribution is permitted only in or among countries not thus -excluded. In such case, this License incorporates the limitation as if -written in the body of this License. - -13. The Free Software Foundation may publish revised and/or new -versions of the Lesser General Public License from time to time. -Such new versions will be similar in spirit to the present version, -but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library -specifies a version number of this License which applies to it and -"any later version", you have the option of following the terms and -conditions either of that version or of any later version published by -the Free Software Foundation. If the Library does not specify a -license version number, you may choose any version ever published by -the Free Software Foundation. - -14. If you wish to incorporate parts of the Library into other free -programs whose distribution conditions are incompatible with these, -write to the author to ask for permission. For software which is -copyrighted by the Free Software Foundation, write to the Free -Software Foundation; we sometimes make exceptions for this. Our -decision will be guided by the two goals of preserving the free status -of all derivatives of our free software and of promoting the sharing -and reuse of software generally. - - NO WARRANTY - -15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO -WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. -EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR -OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY -KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE -LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME -THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN -WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY -AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU -FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR -CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE -LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING -RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A -FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF -SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Libraries - -If you develop a new library, and you want it to be of the greatest -possible use to the public, we recommend making it free software that -everyone can redistribute and change. You can do so by permitting -redistribution under these terms (or, alternatively, under the terms of the -ordinary General Public License). - -To apply these terms, attach the following notices to the library. It is -safest to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least the -"copyright" line and a pointer to where the full notice is found. - - -Copyright (C) - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 2.1 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335 USA - -Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the library, if -necessary. Here is a sample; alter the names: - -Yoyodyne, Inc., hereby disclaims all copyright interest in the -library `Frob' (a library for tweaking knobs) written by James Random Hacker. - -, 1 April 1990 -Ty Coon, President of Vice - -That's all there is to it! - diff --git a/docs/licenses/pbr.txt b/docs/licenses/pbr.txt deleted file mode 100644 index 68c771a099..0000000000 --- a/docs/licenses/pbr.txt +++ /dev/null @@ -1,176 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - diff --git a/docs/licenses/pycurl.txt b/docs/licenses/pycurl.txt deleted file mode 100644 index aea8911b01..0000000000 --- a/docs/licenses/pycurl.txt +++ /dev/null @@ -1,11 +0,0 @@ -Copyright (C) 2001-2008 by Kjetil Jacobsen -Copyright (C) 2001-2008 by Markus F.X.J. Oberhumer -Copyright (C) 2013-2017 by Oleg Pudeyev - -All rights reserved. - -PycURL is dual licensed under the LGPL and an MIT/X derivative license -based on the cURL license. A full copy of the LGPL license is included -in the file COPYING-LGPL. A full copy of the MIT/X derivative license is -included in the file COPYING-MIT. You can redistribute and/or modify PycURL -according to the terms of either license. \ No newline at end of file diff --git a/docs/licenses/pygments.txt b/docs/licenses/pygments.txt deleted file mode 100644 index 721db633a4..0000000000 --- a/docs/licenses/pygments.txt +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 Pavan Kumar Sunkara - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/docs/licenses/pykerberos.txt b/docs/licenses/pykerberos.txt deleted file mode 100644 index d645695673..0000000000 --- a/docs/licenses/pykerberos.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/docs/licenses/pynacl.txt b/docs/licenses/pynacl.txt deleted file mode 100644 index f29e18a269..0000000000 --- a/docs/licenses/pynacl.txt +++ /dev/null @@ -1,174 +0,0 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, -and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by -the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all -other entities that control, are controlled by, or are under common -control with that entity. For the purposes of this definition, -"control" means (i) the power, direct or indirect, to cause the -direction or management of such entity, whether by contract or -otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity -exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, -including but not limited to software source code, documentation -source, and configuration files. - -"Object" form shall mean any form resulting from mechanical -transformation or translation of a Source form, including but -not limited to compiled object code, generated documentation, -and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or -Object form, made available under the License, as indicated by a -copyright notice that is included in or attached to the work -(an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object -form, that is based on (or derived from) the Work and for which the -editorial revisions, annotations, elaborations, or other modifications -represent, as a whole, an original work of authorship. For the purposes -of this License, Derivative Works shall not include works that remain -separable from, or merely link (or bind by name) to the interfaces of, -the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including -the original version of the Work and any modifications or additions -to that Work or Derivative Works thereof, that is intentionally -submitted to Licensor for inclusion in the Work by the copyright owner -or by an individual or Legal Entity authorized to submit on behalf of -the copyright owner. For the purposes of this definition, "submitted" -means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, -and issue tracking systems that are managed by, or on behalf of, the -Licensor for the purpose of discussing and improving the Work, but -excluding communication that is conspicuously marked or otherwise -designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity -on behalf of whom a Contribution has been received by Licensor and -subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the -Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -(except as stated in this section) patent license to make, have made, -use, offer to sell, sell, import, and otherwise transfer the Work, -where such license applies only to those patent claims licensable -by such Contributor that are necessarily infringed by their -Contribution(s) alone or by combination of their Contribution(s) -with the Work to which such Contribution(s) was submitted. If You -institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work -or a Contribution incorporated within the Work constitutes direct -or contributory patent infringement, then any patent licenses -granted to You under this License for that Work shall terminate -as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the -Work or Derivative Works thereof in any medium, with or without -modifications, and in Source or Object form, provided that You -meet the following conditions: - -(a) You must give any other recipients of the Work or -Derivative Works a copy of this License; and - -(b) You must cause any modified files to carry prominent notices -stating that You changed the files; and - -(c) You must retain, in the Source form of any Derivative Works -that You distribute, all copyright, patent, trademark, and -attribution notices from the Source form of the Work, -excluding those notices that do not pertain to any part of -the Derivative Works; and - -(d) If the Work includes a "NOTICE" text file as part of its -distribution, then any Derivative Works that You distribute must -include a readable copy of the attribution notices contained -within such NOTICE file, excluding those notices that do not -pertain to any part of the Derivative Works, in at least one -of the following places: within a NOTICE text file distributed -as part of the Derivative Works; within the Source form or -documentation, if provided along with the Derivative Works; or, -within a display generated by the Derivative Works, if and -wherever such third-party notices normally appear. The contents -of the NOTICE file are for informational purposes only and -do not modify the License. You may add Your own attribution -notices within Derivative Works that You distribute, alongside -or as an addendum to the NOTICE text from the Work, provided -that such additional attribution notices cannot be construed -as modifying the License. - -You may add Your own copyright statement to Your modifications and -may provide additional or different license terms and conditions -for use, reproduction, or distribution of Your modifications, or -for any such Derivative Works as a whole, provided Your use, -reproduction, and distribution of the Work otherwise complies with -the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, -any Contribution intentionally submitted for inclusion in the Work -by You to the Licensor shall be under the terms and conditions of -this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify -the terms of any separate license agreement you may have executed -with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade -names, trademarks, service marks, or product names of the Licensor, -except as required for reasonable and customary use in describing the -origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or -agreed to in writing, Licensor provides the Work (and each -Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -implied, including, without limitation, any warranties or conditions -of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A -PARTICULAR PURPOSE. You are solely responsible for determining the -appropriateness of using or redistributing the Work and assume any -risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, -whether in tort (including negligence), contract, or otherwise, -unless required by applicable law (such as deliberate and grossly -negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, -incidental, or consequential damages of any character arising as a -result of this License or out of the use or inability to use the -Work (including but not limited to damages for loss of goodwill, -work stoppage, computer failure or malfunction, or any and all -other commercial damages or losses), even if such Contributor -has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing -the Work or Derivative Works thereof, You may choose to offer, -and charge a fee for, acceptance of support, warranty, indemnity, -or other liability obligations and/or rights consistent with this -License. However, in accepting such obligations, You may act only -on Your own behalf and on Your sole responsibility, not on behalf -of any other Contributor, and only if You agree to indemnify, -defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason -of your accepting any such warranty or additional liability. \ No newline at end of file diff --git a/docs/licenses/pyvmomi.txt b/docs/licenses/pyvmomi.txt deleted file mode 100644 index f02b4ae7ab..0000000000 --- a/docs/licenses/pyvmomi.txt +++ /dev/null @@ -1,203 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - diff --git a/docs/licenses/pywinrm.txt b/docs/licenses/pywinrm.txt deleted file mode 100644 index 3e21facc07..0000000000 --- a/docs/licenses/pywinrm.txt +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2013 Alexey Diyan - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file diff --git a/docs/licenses/requests-credssp.txt b/docs/licenses/requests-credssp.txt deleted file mode 100644 index 6f494085eb..0000000000 --- a/docs/licenses/requests-credssp.txt +++ /dev/null @@ -1,15 +0,0 @@ -ISC License - -Copyright (c) 2016 Jordan Borean - -Permission to use, copy, modify and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS-IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file diff --git a/docs/licenses/requests-kerberos.txt b/docs/licenses/requests-kerberos.txt deleted file mode 100644 index 581f115e78..0000000000 --- a/docs/licenses/requests-kerberos.txt +++ /dev/null @@ -1,15 +0,0 @@ -ISC License - -Copyright (c) 2012 Kenneth Reitz - -Permission to use, copy, modify and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS-IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/docs/licenses/requests-ntlm.txt b/docs/licenses/requests-ntlm.txt deleted file mode 100644 index 5f3e18de12..0000000000 --- a/docs/licenses/requests-ntlm.txt +++ /dev/null @@ -1,15 +0,0 @@ -ISC License - -Copyright (c) 2013 Ben Toews - -Permission to use, copy, modify and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS-IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/docs/licenses/requestsexceptions.txt b/docs/licenses/requestsexceptions.txt deleted file mode 100644 index d645695673..0000000000 --- a/docs/licenses/requestsexceptions.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/docs/licenses/ruamel.ordereddict.txt b/docs/licenses/ruamel.ordereddict.txt deleted file mode 100644 index 0c12e55403..0000000000 --- a/docs/licenses/ruamel.ordereddict.txt +++ /dev/null @@ -1,23 +0,0 @@ - - The MIT License (MIT) - - Copyright (c) 2007-2017 Anthon van der Neut/Ruamel BVBA - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - diff --git a/docs/licenses/s3transfer.txt b/docs/licenses/s3transfer.txt deleted file mode 100644 index 6b0b1270ff..0000000000 --- a/docs/licenses/s3transfer.txt +++ /dev/null @@ -1,203 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - diff --git a/docs/licenses/selectors2.txt b/docs/licenses/selectors2.txt deleted file mode 100644 index a2f665b427..0000000000 --- a/docs/licenses/selectors2.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2017 Seth Michael Larson - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/licenses/stevedore.txt b/docs/licenses/stevedore.txt deleted file mode 100644 index d645695673..0000000000 --- a/docs/licenses/stevedore.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/docs/licenses/tabulate.txt b/docs/licenses/tabulate.txt deleted file mode 100644 index 7d6e69259c..0000000000 --- a/docs/licenses/tabulate.txt +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2011-2017 Sergey Astanin - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/docs/licenses/typing.txt b/docs/licenses/typing.txt deleted file mode 100644 index 583f9f6e61..0000000000 --- a/docs/licenses/typing.txt +++ /dev/null @@ -1,254 +0,0 @@ -A. HISTORY OF THE SOFTWARE -========================== - -Python was created in the early 1990s by Guido van Rossum at Stichting -Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands -as a successor of a language called ABC. Guido remains Python's -principal author, although it includes many contributions from others. - -In 1995, Guido continued his work on Python at the Corporation for -National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) -in Reston, Virginia where he released several versions of the -software. - -In May 2000, Guido and the Python core development team moved to -BeOpen.com to form the BeOpen PythonLabs team. In October of the same -year, the PythonLabs team moved to Digital Creations (now Zope -Corporation, see http://www.zope.com). In 2001, the Python Software -Foundation (PSF, see http://www.python.org/psf/) was formed, a -non-profit organization created specifically to own Python-related -Intellectual Property. Zope Corporation is a sponsoring member of -the PSF. - -All Python releases are Open Source (see http://www.opensource.org for -the Open Source Definition). Historically, most, but not all, Python -releases have also been GPL-compatible; the table below summarizes -the various releases. - - Release Derived Year Owner GPL- - from compatible? (1) - - 0.9.0 thru 1.2 1991-1995 CWI yes - 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes - 1.6 1.5.2 2000 CNRI no - 2.0 1.6 2000 BeOpen.com no - 1.6.1 1.6 2001 CNRI yes (2) - 2.1 2.0+1.6.1 2001 PSF no - 2.0.1 2.0+1.6.1 2001 PSF yes - 2.1.1 2.1+2.0.1 2001 PSF yes - 2.1.2 2.1.1 2002 PSF yes - 2.1.3 2.1.2 2002 PSF yes - 2.2 and above 2.1.1 2001-now PSF yes - -Footnotes: - -(1) GPL-compatible doesn't mean that we're distributing Python under - the GPL. All Python licenses, unlike the GPL, let you distribute - a modified version without making your changes open source. The - GPL-compatible licenses make it possible to combine Python with - other software that is released under the GPL; the others don't. - -(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, - because its license has a choice of law clause. According to - CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 - is "not incompatible" with the GPL. - -Thanks to the many outside volunteers who have worked under Guido's -direction to make these releases possible. - - -B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON -=============================================================== - -PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 --------------------------------------------- - -1. This LICENSE AGREEMENT is between the Python Software Foundation -("PSF"), and the Individual or Organization ("Licensee") accessing and -otherwise using this software ("Python") in source or binary form and -its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, PSF hereby -grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, -analyze, test, perform and/or display publicly, prepare derivative works, -distribute, and otherwise use Python alone or in any derivative version, -provided, however, that PSF's License Agreement and PSF's notice of copyright, -i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -2011, 2012, 2013, 2014 Python Software Foundation; All Rights Reserved" are -retained in Python alone or in any derivative version prepared by Licensee. - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python. - -4. PSF is making Python available to Licensee on an "AS IS" -basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. Nothing in this License Agreement shall be deemed to create any -relationship of agency, partnership, or joint venture between PSF and -Licensee. This License Agreement does not grant permission to use PSF -trademarks or trade name in a trademark sense to endorse or promote -products or services of Licensee, or any third party. - -8. By copying, installing or otherwise using Python, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. - - -BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 -------------------------------------------- - -BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 - -1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an -office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the -Individual or Organization ("Licensee") accessing and otherwise using -this software in source or binary form and its associated -documentation ("the Software"). - -2. Subject to the terms and conditions of this BeOpen Python License -Agreement, BeOpen hereby grants Licensee a non-exclusive, -royalty-free, world-wide license to reproduce, analyze, test, perform -and/or display publicly, prepare derivative works, distribute, and -otherwise use the Software alone or in any derivative version, -provided, however, that the BeOpen Python License is retained in the -Software, alone or in any derivative version prepared by Licensee. - -3. BeOpen is making the Software available to Licensee on an "AS IS" -basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE -SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS -AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY -DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -5. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -6. This License Agreement shall be governed by and interpreted in all -respects by the law of the State of California, excluding conflict of -law provisions. Nothing in this License Agreement shall be deemed to -create any relationship of agency, partnership, or joint venture -between BeOpen and Licensee. This License Agreement does not grant -permission to use BeOpen trademarks or trade names in a trademark -sense to endorse or promote products or services of Licensee, or any -third party. As an exception, the "BeOpen Python" logos available at -http://www.pythonlabs.com/logos.html may be used according to the -permissions granted on that web page. - -7. By copying, installing or otherwise using the software, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. - - -CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 ---------------------------------------- - -1. This LICENSE AGREEMENT is between the Corporation for National -Research Initiatives, having an office at 1895 Preston White Drive, -Reston, VA 20191 ("CNRI"), and the Individual or Organization -("Licensee") accessing and otherwise using Python 1.6.1 software in -source or binary form and its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, CNRI -hereby grants Licensee a nonexclusive, royalty-free, world-wide -license to reproduce, analyze, test, perform and/or display publicly, -prepare derivative works, distribute, and otherwise use Python 1.6.1 -alone or in any derivative version, provided, however, that CNRI's -License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) -1995-2001 Corporation for National Research Initiatives; All Rights -Reserved" are retained in Python 1.6.1 alone or in any derivative -version prepared by Licensee. Alternately, in lieu of CNRI's License -Agreement, Licensee may substitute the following text (omitting the -quotes): "Python 1.6.1 is made available subject to the terms and -conditions in CNRI's License Agreement. This Agreement together with -Python 1.6.1 may be located on the Internet using the following -unique, persistent identifier (known as a handle): 1895.22/1013. This -Agreement may also be obtained from a proxy server on the Internet -using the following URL: http://hdl.handle.net/1895.22/1013". - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python 1.6.1 or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python 1.6.1. - -4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" -basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. This License Agreement shall be governed by the federal -intellectual property law of the United States, including without -limitation the federal copyright law, and, to the extent such -U.S. federal law does not apply, by the law of the Commonwealth of -Virginia, excluding Virginia's conflict of law provisions. -Notwithstanding the foregoing, with regard to derivative works based -on Python 1.6.1 that incorporate non-separable material that was -previously distributed under the GNU General Public License (GPL), the -law of the Commonwealth of Virginia shall govern this License -Agreement only as to issues arising under or with respect to -Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this -License Agreement shall be deemed to create any relationship of -agency, partnership, or joint venture between CNRI and Licensee. This -License Agreement does not grant permission to use CNRI trademarks or -trade name in a trademark sense to endorse or promote products or -services of Licensee, or any third party. - -8. By clicking on the "ACCEPT" button where indicated, or by copying, -installing or otherwise using Python 1.6.1, Licensee agrees to be -bound by the terms and conditions of this License Agreement. - - ACCEPT - - -CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 --------------------------------------------------- - -Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, -The Netherlands. All rights reserved. - -Permission to use, copy, modify, and distribute this software and its -documentation for any purpose and without fee is hereby granted, -provided that the above copyright notice appear in all copies and that -both that copyright notice and this permission notice appear in -supporting documentation, and that the name of Stichting Mathematisch -Centrum or CWI not be used in advertising or publicity pertaining to -distribution of the software without specific, written prior -permission. - -STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO -THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE -FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT -OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/docs/licenses/wheel.txt b/docs/licenses/wheel.txt deleted file mode 100644 index c3441e6cc8..0000000000 --- a/docs/licenses/wheel.txt +++ /dev/null @@ -1,22 +0,0 @@ -"wheel" copyright (c) 2012-2014 Daniel Holth and -contributors. - -The MIT License - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/licenses/xmltodict.txt b/docs/licenses/xmltodict.txt deleted file mode 100644 index a462778cff..0000000000 --- a/docs/licenses/xmltodict.txt +++ /dev/null @@ -1,7 +0,0 @@ -Copyright (C) 2012 Martin Blech and individual contributors. - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/requirements/collections_requirements.yml b/requirements/collections_requirements.yml deleted file mode 100644 index f750aafce4..0000000000 --- a/requirements/collections_requirements.yml +++ /dev/null @@ -1,21 +0,0 @@ ---- -# from https://github.com/ansible/awx/blob/devel/ -# requirements/collections_requirements.yml -collections: - - name: awx.awx - - name: azure.azcollection # PR 220 is in 1.1.0 - - name: amazon.aws # PR 125 is in 1.1.0 - - name: theforeman.foreman # has requirements.txt (which -r to another file) - - name: google.cloud # has requirements.txt, mainly for google-auth - # forked from opendev.org - - name: https://github.com/AlanCoding/ansible-collections-openstack.git - version: ee_req_install - type: git - - name: community.vmware # has requirements.txt, but may add pyvcloud - - name: ovirt.ovirt - - name: https://github.com/ansible-collections/community.kubernetes.git - type: git - # adds openshift python lib - # needs kubectl for yum / dnf / apt-get - # needs to install snap, then use snap to install helm - - name: ansible.posix diff --git a/requirements/requirements_ansible.in b/requirements/requirements_ansible.in deleted file mode 100644 index ec3c2984f5..0000000000 --- a/requirements/requirements_ansible.in +++ /dev/null @@ -1,68 +0,0 @@ -# GCE -apache-libcloud==2.5.0 -google-auth==1.6.2 # needed for gce inventory imports -# Azure -# azure deps from https://github.com/ansible/ansible/blob/stable-2.9/packaging/requirements/requirements-azure.txt -packaging -azure-cli-core==2.0.35 -azure-cli-nspkg==3.0.2 -azure-common==1.1.11 -azure-mgmt-authorization==0.51.1 -azure-mgmt-batch==5.0.1 -azure-mgmt-cdn==3.0.0 -azure-mgmt-compute==4.4.0 -azure-mgmt-containerinstance==1.4.0 -azure-mgmt-containerregistry==2.0.0 -azure-mgmt-containerservice==4.4.0 -azure-mgmt-dns==2.1.0 -azure-mgmt-keyvault==1.1.0 -azure-mgmt-marketplaceordering==0.1.0 -azure-mgmt-monitor==0.5.2 -azure-mgmt-network==2.3.0 -azure-mgmt-nspkg==2.0.0 -azure-mgmt-redis==5.0.0 -azure-mgmt-resource==2.1.0 -azure-mgmt-rdbms==1.4.1 -azure-mgmt-servicebus==0.5.3 -azure-mgmt-sql==0.10.0 -azure-mgmt-storage==3.1.0 -azure-mgmt-trafficmanager==0.50.0 -azure-mgmt-web==0.41.0 -azure-nspkg==2.0.0 -azure-storage==0.35.1 -msrest==0.6.1 -msrestazure==0.5.0 -azure-keyvault==1.0.0a1 -azure-graphrbac==0.40.0 -azure-mgmt-cosmosdb==0.5.2 -azure-mgmt-hdinsight==0.1.0 -azure-mgmt-devtestlabs==3.0.0 -azure-mgmt-loganalytics==0.2.0 -azure-mgmt-automation==0.1.1 -azure-mgmt-iothub==0.7.0 -# AWS -boto==2.47.0 # last which does not break ec2 scripts -boto3==1.9.223 -jinja2==2.10.1 # required for native jinja2 types for inventory compat mode -# netconf for network modules -ncclient==0.6.3 -# netaddr filter -netaddr -# oVirt/RHV -ovirt-engine-sdk-python==4.3.0 # minimum set inside Ansible facts module requirements -pycurl==7.43.0.1 # higher versions will not install without SSL backend specified -# AWX usage -psutil==5.7.0 # same as AWX requirement -# VMware -pyvmomi==6.7.3 -# WinRM -backports.ssl-match-hostname==3.5.0.1 -pywinrm[kerberos]==0.3.0 -requests -requests-credssp==1.0.2 # For windows authentication awx/issues/1144 -# OpenStack -openstacksdk==0.37.0 -# Openshift/k8s -openshift>=0.11.0 # minimum version to pull in new pyyaml for CVE-2017-18342 -pip==19.3.1 # see upgrade blockers -setuptools==41.6.0 # see upgrade blockers diff --git a/requirements/requirements_ansible.txt b/requirements/requirements_ansible.txt deleted file mode 100644 index 05355fb8b8..0000000000 --- a/requirements/requirements_ansible.txt +++ /dev/null @@ -1,125 +0,0 @@ -adal==1.2.2 # via msrestazure -apache-libcloud==2.5.0 # via -r /awx_devel/requirements/requirements_ansible.in -appdirs==1.4.3 # via openstacksdk -applicationinsights==0.11.9 # via azure-cli-core -argcomplete==1.10.3 # via azure-cli-core, knack -azure-cli-core==2.0.35 # via -r /awx_devel/requirements/requirements_ansible.in -azure-cli-nspkg==3.0.2 # via -r /awx_devel/requirements/requirements_ansible.in, azure-cli-core -azure-common==1.1.11 # via -r /awx_devel/requirements/requirements_ansible.in, azure-graphrbac, azure-mgmt-authorization, azure-mgmt-automation, azure-mgmt-batch, azure-mgmt-cdn, azure-mgmt-compute, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-cosmosdb, azure-mgmt-devtestlabs, azure-mgmt-dns, azure-mgmt-hdinsight, azure-mgmt-iothub, azure-mgmt-keyvault, azure-mgmt-loganalytics, azure-mgmt-marketplaceordering, azure-mgmt-monitor, azure-mgmt-network, azure-mgmt-rdbms, azure-mgmt-redis, azure-mgmt-resource, azure-mgmt-servicebus, azure-mgmt-sql, azure-mgmt-storage, azure-mgmt-trafficmanager, azure-mgmt-web, azure-storage -azure-graphrbac==0.40.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-keyvault==1.0.0a1 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-authorization==0.51.1 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-automation==0.1.1 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-batch==5.0.1 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-cdn==3.0.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-compute==4.4.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-containerinstance==1.4.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-containerregistry==2.0.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-containerservice==4.4.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-cosmosdb==0.5.2 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-devtestlabs==3.0.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-dns==2.1.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-hdinsight==0.1.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-iothub==0.7.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-keyvault==1.1.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-loganalytics==0.2.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-marketplaceordering==0.1.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-monitor==0.5.2 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-network==2.3.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-nspkg==2.0.0 # via -r /awx_devel/requirements/requirements_ansible.in, azure-mgmt-authorization, azure-mgmt-automation, azure-mgmt-batch, azure-mgmt-cdn, azure-mgmt-compute, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-cosmosdb, azure-mgmt-devtestlabs, azure-mgmt-dns, azure-mgmt-hdinsight, azure-mgmt-iothub, azure-mgmt-keyvault, azure-mgmt-loganalytics, azure-mgmt-marketplaceordering, azure-mgmt-monitor, azure-mgmt-network, azure-mgmt-rdbms, azure-mgmt-redis, azure-mgmt-resource, azure-mgmt-servicebus, azure-mgmt-sql, azure-mgmt-storage, azure-mgmt-trafficmanager, azure-mgmt-web -azure-mgmt-rdbms==1.4.1 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-redis==5.0.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-resource==2.1.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-servicebus==0.5.3 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-sql==0.10.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-storage==3.1.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-trafficmanager==0.50.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-web==0.41.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-nspkg==2.0.0 # via -r /awx_devel/requirements/requirements_ansible.in, azure-cli-nspkg, azure-common, azure-graphrbac, azure-keyvault, azure-mgmt-nspkg, azure-storage -azure-storage==0.35.1 # via -r /awx_devel/requirements/requirements_ansible.in -backports.ssl-match-hostname==3.5.0.1 # via -r /awx_devel/requirements/requirements_ansible.in -bcrypt==3.1.7 # via paramiko -boto3==1.9.223 # via -r /awx_devel/requirements/requirements_ansible.in -boto==2.47.0 # via -r /awx_devel/requirements/requirements_ansible.in -botocore==1.12.253 # via boto3, s3transfer -cachetools==3.1.1 # via google-auth -cffi==1.13.2 # via bcrypt, cryptography, pynacl -chardet==3.0.4 # via requests -colorama==0.4.3 # via azure-cli-core, knack -cryptography==2.8 # via adal, azure-keyvault, azure-storage, openstacksdk, paramiko, pyopenssl, requests-credssp, requests-kerberos, requests-ntlm -decorator==4.4.1 # via dogpile.cache, openstacksdk -docutils==0.15.2 # via botocore -dogpile.cache==0.9.0 # via openstacksdk -enum34==1.1.6; python_version < "3" # via cryptography, knack, msrest, ovirt-engine-sdk-python -futures==3.3.0; python_version < "3" # via openstacksdk, s3transfer -google-auth==1.6.2 # via -r /awx_devel/requirements/requirements_ansible.in, kubernetes -humanfriendly==4.18 # via azure-cli-core -idna==2.8 # via requests -ipaddress==1.0.23; python_version < "3" # via cryptography, kubernetes, openstacksdk -iso8601==0.1.12 # via keystoneauth1, openstacksdk -isodate==0.6.0 # via msrest -jinja2==2.10.1 # via -r /awx_devel/requirements/requirements_ansible.in, openshift -jmespath==0.9.4 # via azure-cli-core, boto3, botocore, knack, openstacksdk -jsonpatch==1.24 # via openstacksdk -jsonpointer==2.0 # via jsonpatch -keystoneauth1==3.18.0 # via openstacksdk -knack==0.3.3 # via azure-cli-core -kubernetes==11.0.0 # via openshift -lxml==4.4.2 # via ncclient -markupsafe==1.1.1 # via jinja2 -monotonic==1.5; python_version < "3" # via humanfriendly -msrest==0.6.1 # via -r /awx_devel/requirements/requirements_ansible.in, azure-cli-core, azure-keyvault, azure-mgmt-authorization, azure-mgmt-automation, azure-mgmt-compute, azure-mgmt-containerinstance, azure-mgmt-containerservice, azure-mgmt-cosmosdb, azure-mgmt-devtestlabs, azure-mgmt-dns, azure-mgmt-hdinsight, azure-mgmt-iothub, azure-mgmt-keyvault, azure-mgmt-network, azure-mgmt-rdbms, azure-mgmt-resource, azure-mgmt-servicebus, azure-mgmt-sql, azure-mgmt-storage, azure-mgmt-web, msrestazure -msrestazure==0.5.0 # via -r /awx_devel/requirements/requirements_ansible.in, azure-cli-core, azure-graphrbac, azure-keyvault, azure-mgmt-authorization, azure-mgmt-automation, azure-mgmt-batch, azure-mgmt-cdn, azure-mgmt-compute, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-cosmosdb, azure-mgmt-devtestlabs, azure-mgmt-dns, azure-mgmt-hdinsight, azure-mgmt-iothub, azure-mgmt-keyvault, azure-mgmt-loganalytics, azure-mgmt-marketplaceordering, azure-mgmt-monitor, azure-mgmt-network, azure-mgmt-rdbms, azure-mgmt-redis, azure-mgmt-resource, azure-mgmt-servicebus, azure-mgmt-sql, azure-mgmt-storage, azure-mgmt-trafficmanager, azure-mgmt-web -munch==2.5.0 # via openstacksdk -ncclient==0.6.3 # via -r /awx_devel/requirements/requirements_ansible.in -netaddr==0.7.19 # via -r /awx_devel/requirements/requirements_ansible.in -netifaces==0.10.9 # via openstacksdk -ntlm-auth==1.4.0 # via requests-credssp, requests-ntlm -oauthlib==3.1.0 # via requests-oauthlib -openshift==0.11.2 # via -r /awx_devel/requirements/requirements_ansible.in -openstacksdk==0.37.0 # via -r /awx_devel/requirements/requirements_ansible.in -os-service-types==1.7.0 # via keystoneauth1, openstacksdk -ovirt-engine-sdk-python==4.3.0 # via -r /awx_devel/requirements/requirements_ansible.in -packaging==19.2 # via -r /awx_devel/requirements/requirements_ansible.in -paramiko==2.7.1 # via azure-cli-core, ncclient -pbr==5.4.4 # via keystoneauth1, openstacksdk, os-service-types, stevedore -psutil==5.7.0 # via -r /awx_devel/requirements/requirements_ansible.in -pyasn1-modules==0.2.7 # via google-auth -pyasn1==0.4.8 # via pyasn1-modules, requests-credssp, rsa -pycparser==2.19 # via cffi -pycurl==7.43.0.1 # via -r /awx_devel/requirements/requirements_ansible.in, ovirt-engine-sdk-python -pygments==2.5.2 # via azure-cli-core, knack -pyjwt==1.7.1 # via adal, azure-cli-core -pykerberos==1.2.1 # via requests-kerberos -pynacl==1.4.0 # via paramiko -pyopenssl==19.1.0 # via azure-cli-core, requests-credssp -pyparsing==2.4.5 # via packaging -python-dateutil==2.8.1 # via adal, azure-storage, botocore, kubernetes -python-string-utils==0.6.0; python_version < "3" # via openshift -pyvmomi==6.7.3 # via -r /awx_devel/requirements/requirements_ansible.in -pywinrm[kerberos]==0.3.0 # via -r /awx_devel/requirements/requirements_ansible.in -pyyaml==5.2 # via azure-cli-core, knack, kubernetes, openstacksdk -requests-credssp==1.0.2 # via -r /awx_devel/requirements/requirements_ansible.in -requests-kerberos==0.12.0 # via pywinrm -requests-ntlm==1.1.0 # via pywinrm -requests-oauthlib==1.3.0 # via kubernetes, msrest -requests==2.22.0 # via -r /awx_devel/requirements/requirements_ansible.in, adal, apache-libcloud, azure-cli-core, azure-keyvault, azure-storage, keystoneauth1, kubernetes, msrest, pyvmomi, pywinrm, requests-credssp, requests-kerberos, requests-ntlm, requests-oauthlib -requestsexceptions==1.4.0 # via openstacksdk -rsa==4.0 # via google-auth -ruamel.ordereddict==0.4.14; python_version < "3" # via ruamel.yaml -ruamel.yaml.clib==0.2.0 # via ruamel.yaml -ruamel.yaml==0.16.10 # via openshift -s3transfer==0.2.1 # via boto3 -selectors2==2.0.1 # via ncclient -six==1.13.0 # via azure-cli-core, bcrypt, cryptography, google-auth, isodate, keystoneauth1, knack, kubernetes, munch, ncclient, openshift, openstacksdk, ovirt-engine-sdk-python, packaging, pynacl, pyopenssl, python-dateutil, pyvmomi, pywinrm, requests-credssp, stevedore, websocket-client -stevedore==1.31.0 # via keystoneauth1 -tabulate==0.8.2 # via azure-cli-core, knack -typing==3.7.4.1; python_version < "3" # via msrest -urllib3==1.25.7 # via botocore, kubernetes, requests -websocket-client==0.57.0 # via kubernetes -wheel==0.33.6 # via azure-cli-core (overriden, see upgrade blockers) -xmltodict==0.12.0 # via pywinrm - -# The following packages are considered to be unsafe in a requirements file: -pip==19.3.1 # via -r /awx_devel/requirements/requirements_ansible.in, azure-cli-core -setuptools==41.6.0 # via -r /awx_devel/requirements/requirements_ansible.in, kubernetes, ncclient diff --git a/requirements/requirements_ansible_git.txt b/requirements/requirements_ansible_git.txt deleted file mode 100644 index 340cbfdcc7..0000000000 --- a/requirements/requirements_ansible_git.txt +++ /dev/null @@ -1 +0,0 @@ -git+https://github.com/ansible/system-certifi.git@devel#egg=certifi diff --git a/requirements/requirements_ansible_uninstall.txt b/requirements/requirements_ansible_uninstall.txt deleted file mode 100644 index 56cbaa5f19..0000000000 --- a/requirements/requirements_ansible_uninstall.txt +++ /dev/null @@ -1 +0,0 @@ -rsa # stop adding new crypto libs diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index 66a0eeebbd..203f64b64e 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -48,19 +48,16 @@ RUN python3 -m ensurepip && pip3 install "virtualenv < 20" # Install & build requirements ADD Makefile /tmp/Makefile RUN mkdir /tmp/requirements -ADD requirements/requirements_ansible.txt \ - requirements/requirements_ansible_uninstall.txt \ - requirements/requirements_ansible_git.txt \ - requirements/requirements.txt \ +ADD requirements/requirements.txt \ requirements/requirements_tower_uninstall.txt \ requirements/requirements_git.txt \ /tmp/requirements/ -RUN cd /tmp && make requirements_awx requirements_ansible_py3 +RUN cd /tmp && make requirements_awx {% if (build_dev|bool) or (kube_dev|bool) %} ADD requirements/requirements_dev.txt /tmp/requirements -RUN cd /tmp && make requirements_awx_dev requirements_ansible_dev +RUN cd /tmp && make requirements_awx_dev {% else %} # Use the distro provided npm to bootstrap our required version of node RUN npm install -g n && n 14.15.1 && dnf remove -y nodejs diff --git a/tools/audit-api-license.sh b/tools/audit-api-license.sh deleted file mode 100755 index 6cf93f4022..0000000000 --- a/tools/audit-api-license.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -echo "---------- requirements.txt --------------" -for each in `cat requirements/requirements.txt| awk -F= '{print $1}' | tr -d "[]"` -do - if [ ! -f docs/licenses/$each.txt ]; then - echo No license for $each - fi -done -echo "---------- end requirements.txt --------------" - - -echo "---------- requirements_ansible.txt --------------" -for each in `cat requirements/requirements_ansible.txt| awk -F= '{print $1}' | tr -d "[]"` -do - if [ ! -f docs/licenses/$each.txt ]; then - echo No license for $each - fi -done -echo "---------- end requirements_ansible.txt --------------" From 34df47ceba3d8d175e53248f2b3f00d53de0c644 Mon Sep 17 00:00:00 2001 From: "Christian M. Adams" Date: Thu, 4 Mar 2021 12:24:01 -0500 Subject: [PATCH 163/178] Remove messages.js files that do not need to be committed to the repo * also clean up old .PHONY entries --- Makefile | 8 ++++---- awx/ui_next/package.json | 1 + awx/ui_next/src/locales/en/messages.js | 1 - awx/ui_next/src/locales/ja/messages.js | 1 - 4 files changed, 5 insertions(+), 6 deletions(-) delete mode 100644 awx/ui_next/src/locales/en/messages.js delete mode 100644 awx/ui_next/src/locales/ja/messages.js diff --git a/Makefile b/Makefile index 858a6f67e4..823017221f 100644 --- a/Makefile +++ b/Makefile @@ -62,11 +62,11 @@ WHEEL_FILE ?= $(WHEEL_NAME)-py2-none-any.whl I18N_FLAG_FILE = .i18n_built .PHONY: awx-link clean clean-tmp clean-venv requirements requirements_dev \ - develop refresh adduser migrate dbchange runserver \ + develop refresh adduser migrate dbchange \ receiver test test_unit test_coverage coverage_html \ - dev_build release_build release_clean sdist \ - ui-docker-machine ui-docker ui-release ui-devel \ - ui-test ui-deps ui-test-ci VERSION docker-compose-sources + dev_build release_build sdist \ + ui-release ui-devel \ + VERSION docker-compose-sources clean-tmp: rm -rf tmp/ diff --git a/awx/ui_next/package.json b/awx/ui_next/package.json index 0d470a17ca..4204fd5769 100644 --- a/awx/ui_next/package.json +++ b/awx/ui_next/package.json @@ -55,6 +55,7 @@ "react-scripts": "^3.4.4" }, "scripts": { + "prestart": "lingui compile", "start": "PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts start", "start-instrumented": "DEBUG=instrument-cra PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts -r @cypress/instrument-cra start", "build": "INLINE_RUNTIME_CHUNK=false react-scripts build", diff --git a/awx/ui_next/src/locales/en/messages.js b/awx/ui_next/src/locales/en/messages.js deleted file mode 100644 index 61cb8198b8..0000000000 --- a/awx/ui_next/src/locales/en/messages.js +++ /dev/null @@ -1 +0,0 @@ -/* eslint-disable */module.exports={languageData:{"plurals":function(n,ord){var s=String(n).split("."),v0=!s[1],t0=Number(s[0])==n,n10=t0&&s[0].slice(-1),n100=t0&&s[0].slice(-2);if(ord)return n10==1&&n100!=11?"one":n10==2&&n100!=12?"two":n10==3&&n100!=13?"few":"other";return n==1&&v0?"one":"other"}},messages:{"404":"404","> add":"> add","> edit":"> edit","AWX Logo":"AWX Logo","About":"About","AboutModal Logo":"AboutModal Logo","Access":"Access","Add":"Add","Add Roles":"Add Roles","Add Team Roles":"Add Team Roles","Add User Roles":"Add User Roles","Administration":"Administration","Admins":"Admins","Ansible Environment":"Ansible Environment","Ansible Version":"Ansible Version","Applications":"Applications","Apply roles":"Apply roles","Are you sure you want to delete:":"Are you sure you want to delete:","Are you sure you want to remove {0} access from {1}? Doing so affects all members of the team.":function(a){return["Are you sure you want to remove ",a("0")," access from ",a("1"),"? Doing so affects all members of the team."]},"Are you sure you want to remove {0} access from {username}?":function(a){return["Are you sure you want to remove ",a("0")," access from ",a("username"),"?"]},"Authentication":"Authentication","Authentication Settings":"Authentication Settings","Brand Image":"Brand Image","Cancel":"Cancel","Cannot find organization with ID":"Cannot find organization with ID","Cannot find resource.":"Cannot find resource.","Cannot find route {0}.":function(a){return["Cannot find route ",a("0"),"."]},"Close":"Close","Collapse":"Collapse","Copyright 2018 Red Hat, Inc.":"Copyright 2018 Red Hat, Inc.","Copyright 2019 Red Hat, Inc.":"Copyright 2019 Red Hat, Inc.","Create New Organization":"Create New Organization","Created":"Created","Credential Types":"Credential Types","Credentials":"Credentials","Current page":"Current page","Dashboard":"Dashboard","Delete":"Delete","Delete {0}":function(a){return["Delete ",a("0")]},"Delete {itemName}":function(a){return["Delete ",a("itemName")]},"Description":"Description","Details":"Details","Edit":"Edit","Edit Details":"Edit Details","Expand":"Expand","Failure":"Failure","First":"First","Go to first page":"Go to first page","Go to last page":"Go to last page","Go to next page":"Go to next page","Go to previous page":"Go to previous page","Help":"Help","If you {0} want to remove access for this particular user, please remove them from the team.":function(a){return["If you ",a("0")," want to remove access for this particular user, please remove them from the team."]},"Info":"Info","Instance Groups":"Instance Groups","Integrations":"Integrations","Invalid username or password. Please try again.":"Invalid username or password. Please try again.","Inventories":"Inventories","Inventory Scripts":"Inventory Scripts","Items Per Page":"Items Per Page","Items per page":"Items per page","Items {itemMin} \u2013 {itemMax} of {count}":function(a){return["Items ",a("itemMin")," \u2013 ",a("itemMax")," of ",a("count")]},"Jobs":"Jobs","Jobs Settings":"Jobs Settings","Last":"Last","Last Modified":"Last Modified","Last Name":"Last Name","License":"License","Loading...":"Loading...","Logout":"Logout","Management Jobs":"Management Jobs","Members":"Members","Modified":"Modified","My View":"My View","Name":"Name","Next":"Next","No {0} Found":function(a){return["No ",a("0")," Found"]},"Notification Templates":"Notification Templates","Notifications":"Notifications","Organization Add":"Organization Add","Organization detail tabs":"Organization detail tabs","Organizations":"Organizations","Organizations List":"Organizations List","Page":"Page","Page <0/> of {pageCount}":function(a){return["Page <0/> of ",a("pageCount")]},"Page Number":"Page Number","Pagination":"Pagination","Password":"Password","Per Page":"Per Page","Please add {0} to populate this list":function(a){return["Please add ",a("0")," to populate this list"]},"Please add {0} {itemName} to populate this list":function(a){return["Please add ",a("0")," ",a("itemName")," to populate this list"]},"Portal Mode":"Portal Mode","Previous":"Previous","Primary Navigation":"Primary Navigation","Projects":"Projects","Remove {0} Access":function(a){return["Remove ",a("0")," Access"]},"Resources":"Resources","Save":"Save","Schedules":"Schedules","Search":"Search","Search text input":"Search text input","Select":"Select","Select Input":"Select Input","Select Users Or Teams":"Select Users Or Teams","Select a row to delete":"Select a row to delete","Select all":"Select all","Select items from list":"Select items from list","Select the Instance Groups for this Organization to run on.":"Select the Instance Groups for this Organization to run on.","Select {header}":function(a){return["Select ",a("header")]},"Selected":"Selected","Settings":"Settings","Sort":"Sort","Successful":"Successful","System":"System","System Settings":"System Settings","Team":"Team","Team Roles":"Team Roles","Teams":"Teams","Templates":"Templates","This field must not be blank":"This field must not be blank","This field must not exceed {max} characters":function(a){return["This field must not exceed ",a("max")," characters"]},"Toggle notification failure":"Toggle notification failure","Toggle notification success":"Toggle notification success","Use Default {label}":function(a){return["Use Default ",a("label")]},"User":"User","User Details":"User Details","User Interface":"User Interface","User Interface Settings":"User Interface Settings","User Roles":"User Roles","Username":"Username","Users":"Users","Views":"Views","Welcome to Ansible {brandName}! Please Sign In.":function(a){return["Welcome to Ansible ",a("brandName"),"! Please Sign In."]},"You do not have permission to delete the following {0}: {itemsUnableToDelete}":function(a){return["You do not have permission to delete the following ",a("0"),": ",a("itemsUnableToDelete")]},"You have been logged out.":"You have been logged out.","add {currentTab}":function(a){return["add ",a("currentTab")]},"adding {currentTab}":function(a){return["adding ",a("currentTab")]},"cancel delete":"cancel delete","confirm delete":"confirm delete","confirm removal of {currentTab}/cancel and go back to {currentTab} view.":function(a){return["confirm removal of ",a("currentTab"),"/cancel and go back to ",a("currentTab")," view."]},"delete {currentTab}":function(a){return["delete ",a("currentTab")]},"deleting {currentTab} association with orgs":function(a){return["deleting ",a("currentTab")," association with orgs"]},"edit view":"edit view","items":"items","of {pageCount}":function(a){return["of ",a("pageCount")]},"pages":"pages","per page":"per page","save/cancel and go back to view":"save/cancel and go back to view","save/cancel and go back to {currentTab} view":function(a){return["save/cancel and go back to ",a("currentTab")," view"]},"select organization {itemId}":function(a){return["select organization ",a("itemId")]},"{0}":function(a){return[a("0")]},"{0} List":function(a){return[a("0")," List"]},"{currentTab} detail view":function(a){return[a("currentTab")," detail view"]},"{itemMin} - {itemMax} of {count}":function(a){return[a("itemMin")," - ",a("itemMax")," of ",a("count")]}}}; \ No newline at end of file diff --git a/awx/ui_next/src/locales/ja/messages.js b/awx/ui_next/src/locales/ja/messages.js deleted file mode 100644 index b3b6c304be..0000000000 --- a/awx/ui_next/src/locales/ja/messages.js +++ /dev/null @@ -1 +0,0 @@ -/* eslint-disable */module.exports={languageData:{"plurals":function(n,ord){if(ord)return"other";return"other"}},messages:{"404":"404","> add":"> add","> edit":"> edit","AWX Logo":"AWX Logo","About":"About","AboutModal Logo":"AboutModal Logo","Access":"Access","Add":"Add","Add Roles":"Add Roles","Add Team Roles":"Add Team Roles","Add User Roles":"Add User Roles","Administration":"Administration","Admins":"Admins","Ansible Environment":"Ansible Environment","Ansible Version":"Ansible Version","Applications":"Applications","Apply roles":"Apply roles","Are you sure you want to delete:":"Are you sure you want to delete:","Are you sure you want to remove {0} access from {1}? Doing so affects all members of the team.":function(a){return["Are you sure you want to remove ",a("0")," access from ",a("1"),"? Doing so affects all members of the team."]},"Are you sure you want to remove {0} access from {username}?":function(a){return["Are you sure you want to remove ",a("0")," access from ",a("username"),"?"]},"Authentication":"Authentication","Authentication Settings":"Authentication Settings","Brand Image":"Brand Image","Cancel":"Cancel","Cannot find organization with ID":"Cannot find organization with ID","Cannot find resource.":"Cannot find resource.","Cannot find route {0}.":function(a){return["Cannot find route ",a("0"),"."]},"Close":"Close","Collapse":"Collapse","Copyright 2018 Red Hat, Inc.":"Copyright 2018 Red Hat, Inc.","Copyright 2019 Red Hat, Inc.":"Copyright 2019 Red Hat, Inc.","Create New Organization":"Create New Organization","Created":"Created","Credential Types":"Credential Types","Credentials":"Credentials","Current page":"Current page","Dashboard":"Dashboard","Delete":"Delete","Delete {0}":function(a){return["Delete ",a("0")]},"Delete {itemName}":function(a){return["Delete ",a("itemName")]},"Description":"Description","Details":"Details","Edit":"Edit","Edit Details":"Edit Details","Expand":"Expand","Failure":"Failure","First":"First","Go to first page":"Go to first page","Go to last page":"Go to last page","Go to next page":"Go to next page","Go to previous page":"Go to previous page","Help":"Help","If you {0} want to remove access for this particular user, please remove them from the team.":function(a){return["If you ",a("0")," want to remove access for this particular user, please remove them from the team."]},"Info":"Info","Instance Groups":"Instance Groups","Integrations":"Integrations","Invalid username or password. Please try again.":"Invalid username or password. Please try again.","Inventories":"Inventories","Inventory Scripts":"Inventory Scripts","Items Per Page":"Items Per Page","Items per page":"Items per page","Items {itemMin} \u2013 {itemMax} of {count}":function(a){return["Items ",a("itemMin")," \u2013 ",a("itemMax")," of ",a("count")]},"Jobs":"Jobs","Jobs Settings":"Jobs Settings","Last":"Last","Last Modified":"Last Modified","Last Name":"Last Name","License":"License","Loading...":"Loading...","Logout":"Logout","Management Jobs":"Management Jobs","Members":"Members","Modified":"Modified","My View":"My View","Name":"Name","Next":"Next","No {0} Found":function(a){return["No ",a("0")," Found"]},"Notification Templates":"Notification Templates","Notifications":"Notifications","Organization Add":"Organization Add","Organization detail tabs":"Organization detail tabs","Organizations":"Organizations","Organizations List":"Organizations List","Page":"Page","Page <0/> of {pageCount}":function(a){return["Page <0/> of ",a("pageCount")]},"Page Number":"Page Number","Pagination":"Pagination","Password":"Password","Per Page":"Per Page","Please add {0} to populate this list":function(a){return["Please add ",a("0")," to populate this list"]},"Please add {0} {itemName} to populate this list":function(a){return["Please add ",a("0")," ",a("itemName")," to populate this list"]},"Portal Mode":"Portal Mode","Previous":"Previous","Primary Navigation":"Primary Navigation","Projects":"Projects","Remove {0} Access":function(a){return["Remove ",a("0")," Access"]},"Resources":"Resources","Save":"Save","Schedules":"Schedules","Search":"Search","Search text input":"Search text input","Select":"Select","Select Input":"Select Input","Select Users Or Teams":"Select Users Or Teams","Select a row to delete":"Select a row to delete","Select all":"Select all","Select items from list":"Select items from list","Select the Instance Groups for this Organization to run on.":"Select the Instance Groups for this Organization to run on.","Select {header}":function(a){return["Select ",a("header")]},"Selected":"Selected","Settings":"Settings","Sort":"Sort","Successful":"Successful","System":"System","System Settings":"System Settings","Team":"Team","Team Roles":"Team Roles","Teams":"Teams","Templates":"Templates","This field must not be blank":"This field must not be blank","This field must not exceed {max} characters":function(a){return["This field must not exceed ",a("max")," characters"]},"Toggle notification failure":"Toggle notification failure","Toggle notification success":"Toggle notification success","Use Default {label}":function(a){return["Use Default ",a("label")]},"User":"User","User Details":"User Details","User Interface":"User Interface","User Interface Settings":"User Interface Settings","User Roles":"User Roles","Username":"Username","Users":"Users","Views":"Views","Welcome to Ansible {brandName}! Please Sign In.":function(a){return["Welcome to Ansible ",a("brandName"),"! Please Sign In."]},"You do not have permission to delete the following {0}: {itemsUnableToDelete}":function(a){return["You do not have permission to delete the following ",a("0"),": ",a("itemsUnableToDelete")]},"You have been logged out.":"You have been logged out.","add {currentTab}":function(a){return["add ",a("currentTab")]},"adding {currentTab}":function(a){return["adding ",a("currentTab")]},"cancel delete":"cancel delete","confirm delete":"confirm delete","confirm removal of {currentTab}/cancel and go back to {currentTab} view.":function(a){return["confirm removal of ",a("currentTab"),"/cancel and go back to ",a("currentTab")," view."]},"delete {currentTab}":function(a){return["delete ",a("currentTab")]},"deleting {currentTab} association with orgs":function(a){return["deleting ",a("currentTab")," association with orgs"]},"edit view":"edit view","items":"items","of {pageCount}":function(a){return["of ",a("pageCount")]},"pages":"pages","per page":"per page","save/cancel and go back to view":"save/cancel and go back to view","save/cancel and go back to {currentTab} view":function(a){return["save/cancel and go back to ",a("currentTab")," view"]},"select organization {itemId}":function(a){return["select organization ",a("itemId")]},"{0}":function(a){return[a("0")]},"{0} List":function(a){return[a("0")," List"]},"{currentTab} detail view":function(a){return[a("currentTab")," detail view"]},"{itemMin} - {itemMax} of {count}":function(a){return[a("itemMin")," - ",a("itemMax")," of ",a("count")]}}}; \ No newline at end of file From 55b948bf395048ec0c8df1ca8357a7c07e2fe7b0 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 4 Mar 2021 12:57:06 -0500 Subject: [PATCH 164/178] Remove checkout from Makefile --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index 823017221f..815124660e 100644 --- a/Makefile +++ b/Makefile @@ -458,7 +458,6 @@ clean-ui: rm -rf awx/ui_next/build rm -rf awx/ui_next/src/locales/_build rm -rf $(UI_BUILD_FLAG_FILE) - git checkout awx/ui_next/src/locales awx/ui_next/node_modules: $(NPM_BIN) --prefix awx/ui_next --loglevel warn --ignore-scripts install From 95ec0097582c75ff2592a95125371e4427e0c39f Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 4 Mar 2021 13:12:12 -0500 Subject: [PATCH 165/178] Add language catalog compile step before test commands --- awx/ui_next/package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/awx/ui_next/package.json b/awx/ui_next/package.json index 4204fd5769..24371abfc7 100644 --- a/awx/ui_next/package.json +++ b/awx/ui_next/package.json @@ -55,7 +55,11 @@ "react-scripts": "^3.4.4" }, "scripts": { + "prelint": "lingui compile", "prestart": "lingui compile", + "prestart-instrumented": "lingui compile", + "pretest": "lingui compile", + "pretest-watch": "lingui compile", "start": "PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts start", "start-instrumented": "DEBUG=instrument-cra PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts -r @cypress/instrument-cra start", "build": "INLINE_RUNTIME_CHUNK=false react-scripts build", From 6f1f64118b6fcb69b9dabd6173b075d158465109 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 3 Mar 2021 15:38:27 -0500 Subject: [PATCH 166/178] wip --- Makefile | 12 ++-- tools/docker-compose/Dockerfile-haproxy | 2 - .../ansible/roles/sources/defaults/main.yml | 1 + .../sources/templates/docker-compose.yml.j2 | 65 +++++++++++++------ 4 files changed, 50 insertions(+), 30 deletions(-) delete mode 100644 tools/docker-compose/Dockerfile-haproxy diff --git a/Makefile b/Makefile index 858a6f67e4..971baae552 100644 --- a/Makefile +++ b/Makefile @@ -539,25 +539,23 @@ docker-compose-sources: ansible-playbook -i tools/docker-compose/inventory tools/docker-compose/ansible/sources.yml \ -e awx_image=$(DEV_DOCKER_TAG_BASE)/awx_devel \ -e awx_image_tag=$(COMPOSE_TAG) + -e cluster_node_count=$(CLUSER_NODE_COUNT) docker-compose: docker-auth awx/projects docker-compose-sources - docker-compose -f tools/docker-compose/_sources/docker-compose.yml $(COMPOSE_UP_OPTS) up --no-recreate awx - -docker-compose-cluster: docker-auth awx/projects - docker-compose -f tools/docker-compose-cluster.yml up + docker-compose -f tools/docker-compose/_sources/docker-compose.yml $(COMPOSE_UP_OPTS) up --no-recreate awx_1 docker-compose-credential-plugins: docker-auth awx/projects docker-compose-sources echo -e "\033[0;31mTo generate a CyberArk Conjur API key: docker exec -it tools_conjur_1 conjurctl account create quick-start\033[0m" docker-compose -f tools/docker-compose/_sources/docker-compose.yml -f tools/docker-credential-plugins-override.yml up --no-recreate awx docker-compose-test: docker-auth awx/projects docker-compose-sources - docker-compose -f tools/docker-compose/_sources/docker-compose.yml run --rm --service-ports awx /bin/bash + docker-compose -f tools/docker-compose/_sources/docker-compose.yml run --rm --service-ports awx_1 /bin/bash docker-compose-runtest: awx/projects docker-compose-sources - docker-compose -f tools/docker-compose/_sources/docker-compose.yml run --rm --service-ports awx /start_tests.sh + docker-compose -f tools/docker-compose/_sources/docker-compose.yml run --rm --service-ports awx_1 /start_tests.sh docker-compose-build-swagger: awx/projects docker-compose-sources - docker-compose -f tools/docker-compose/_sources/docker-compose.yml run --rm --service-ports --no-deps awx /start_tests.sh swagger + docker-compose -f tools/docker-compose/_sources/docker-compose.yml run --rm --service-ports --no-deps awx_1 /start_tests.sh swagger detect-schema-change: genschema curl https://s3.amazonaws.com/awx-public-ci-files/schema.json -o reference-schema.json diff --git a/tools/docker-compose/Dockerfile-haproxy b/tools/docker-compose/Dockerfile-haproxy deleted file mode 100644 index 9d38924939..0000000000 --- a/tools/docker-compose/Dockerfile-haproxy +++ /dev/null @@ -1,2 +0,0 @@ -FROM haproxy:1.6-alpine -COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg diff --git a/tools/docker-compose/ansible/roles/sources/defaults/main.yml b/tools/docker-compose/ansible/roles/sources/defaults/main.yml index f867d7ef2a..cff661deb8 100644 --- a/tools/docker-compose/ansible/roles/sources/defaults/main.yml +++ b/tools/docker-compose/ansible/roles/sources/defaults/main.yml @@ -5,3 +5,4 @@ awx_image: 'quay.io/ansible/awx_devel' pg_port: 5432 pg_username: 'awx' pg_database: 'awx' +cluster_node_count: "{{ lookup('env', 'CLUSTER_COUNT') | default(1, True) }}" diff --git a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 index 1d0799c7b9..0f043cf215 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 @@ -1,30 +1,34 @@ --- version: '2' services: +{% for i in range(cluster_node_count|int) %} + {% set container_postfix = loop.index %} + {% set awx_sdb_port_start = 7899 + (loop.index0*100) | int %} + {% set awx_sdb_port_end = 7999 + (loop.index0*100) | int %} # Primary AWX Development Container - awx: + awx_{{ container_postfix }}: user: "{{ ansible_user_uid }}" image: "{{ awx_image }}:{{ awx_image_tag }}" - container_name: tools_awx_1 - hostname: awx + container_name: tools_awx_{{ container_postfix }} + hostname: awx_{{ container_postfix }} command: launch_awx.sh environment: OS: "{{ os_info.stdout }}" SDB_HOST: 0.0.0.0 - SDB_PORT: 7899 + SDB_PORT: {{ awx_sdb_port_start }} AWX_GROUP_QUEUES: tower RECEPTORCTL_SOCKET: /var/run/receptor/receptor.sock ports: - - "2222:2222" - - "8888:8888" - - "8080:8080" - - "8013:8013" - - "8043:8043" - - "6899:6899" # default port range for sdb-listen - - "7899-7999:7899-7999" # default port range for sdb-listen +{% if cluster_node_count|int == 1 %} + - "8080:8080" # unused but mapped for debugging + - "8888:8888" # jupyter notebook + - "8013:8013" # http + - "8043:8043" # https +{% endif %} + - "{{ awx_sdb_port_start }}:{{ awx_sdb_port_end }}" # sdb-listen links: - postgres - - redis + - redis_{{ container_postfix }} working_dir: "/awx_devel" volumes: - "../../../:/awx_devel" @@ -34,12 +38,21 @@ services: - "../../docker-compose/_sources/local_settings.py:/etc/tower/conf.d/local_settings.py" - "../../docker-compose/_sources/SECRET_KEY:/etc/tower/SECRET_KEY" - "../../docker-compose/receptor.conf:/etc/receptor/receptor.conf" - - "redis_socket:/var/run/redis/:rw" - "receptor:/var/run/receptor/" - "/sys/fs/cgroup:/sys/fs/cgroup" - "~/.kube/config:/var/lib/awx/.kube/config" + - "redis_socket_{{ container_postfix }}:/var/run/redis/:rw" privileged: true tty: true + redis_{{ container_postfix }}: + image: redis:latest + container_name: tools_redis_{{ container_postfix }} + volumes: + - "../../redis/redis.conf:/usr/local/etc/redis/redis.conf" + - "redis_socket_{{ container_postfix }}:/var/run/redis/:rw" + entrypoint: ["redis-server"] + command: ["/usr/local/etc/redis/redis.conf"] +{% endfor %} # A useful container that simply passes through log messages to the console # helpful for testing awx/tower logging # logstash: @@ -56,16 +69,26 @@ services: POSTGRES_PASSWORD: {{ pg_password }} volumes: - "awx_db:/var/lib/postgresql/data" - redis: - image: redis:latest - container_name: tools_redis_1 +{% if cluster_node_count|int > 1 %} + haproxy: + image: haproxy volumes: - - "../../redis/redis.conf:/usr/local/etc/redis/redis.conf" - - "redis_socket:/var/run/redis/:rw" - entrypoint: ["redis-server"] - command: ["/usr/local/etc/redis/redis.conf"] + - "../ha.conf:/usr/local/etc/haproxy/haproxy.cfg" + depends_on: +{% for i in range(cluster_node_count|int) -%} + {% set container_postfix = loop.index %} + - "awx_{{ container_postfix }}" +{% endfor %} + ports: + - "8013:8013" + - "8043:8043" + - "1936:1936" +{% endif -%} {# haproxy #} volumes: awx_db: - redis_socket: receptor: +{% for i in range(cluster_node_count|int) -%} + {% set container_postfix = loop.index %} + redis_socket_{{ container_postfix }}: +{% endfor -%} From 69edef430c7ce5fa59c6dfe1195e39edf4a54efb Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Wed, 3 Mar 2021 19:41:50 -0500 Subject: [PATCH 167/178] Get clustered dev env working --- Makefile | 5 ++- .../ansible/roles/sources/tasks/main.yml | 1 + .../sources/templates/docker-compose.yml.j2 | 38 +++++++++++-------- .../roles/sources/templates/haproxy.cfg.j2} | 15 ++++---- 4 files changed, 34 insertions(+), 25 deletions(-) rename tools/docker-compose/{haproxy.cfg => ansible/roles/sources/templates/haproxy.cfg.j2} (67%) diff --git a/Makefile b/Makefile index 971baae552..a68ab74c46 100644 --- a/Makefile +++ b/Makefile @@ -534,15 +534,16 @@ awx/projects: @mkdir -p $@ COMPOSE_UP_OPTS ?= +CLUSER_NODE_COUNT ?= 1 docker-compose-sources: ansible-playbook -i tools/docker-compose/inventory tools/docker-compose/ansible/sources.yml \ -e awx_image=$(DEV_DOCKER_TAG_BASE)/awx_devel \ - -e awx_image_tag=$(COMPOSE_TAG) + -e awx_image_tag=$(COMPOSE_TAG) \ -e cluster_node_count=$(CLUSER_NODE_COUNT) docker-compose: docker-auth awx/projects docker-compose-sources - docker-compose -f tools/docker-compose/_sources/docker-compose.yml $(COMPOSE_UP_OPTS) up --no-recreate awx_1 + docker-compose -f tools/docker-compose/_sources/docker-compose.yml $(COMPOSE_UP_OPTS) up docker-compose-credential-plugins: docker-auth awx/projects docker-compose-sources echo -e "\033[0;31mTo generate a CyberArk Conjur API key: docker exec -it tools_conjur_1 conjurctl account create quick-start\033[0m" diff --git a/tools/docker-compose/ansible/roles/sources/tasks/main.yml b/tools/docker-compose/ansible/roles/sources/tasks/main.yml index 33ace141f1..55937b4051 100644 --- a/tools/docker-compose/ansible/roles/sources/tasks/main.yml +++ b/tools/docker-compose/ansible/roles/sources/tasks/main.yml @@ -44,6 +44,7 @@ with_items: - "database.py" - "websocket_secret.py" + - "haproxy.cfg" - name: Delete old local_settings.py file: diff --git a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 index 0f043cf215..7d724c82f4 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 @@ -44,6 +44,28 @@ services: - "redis_socket_{{ container_postfix }}:/var/run/redis/:rw" privileged: true tty: true + ports: + - "{{ awx_sdb_port_start }}:{{ awx_sdb_port_end }}" # sdb-listen +{% if cluster_node_count|int == 1 %} + - "8080:8080" # unused but mapped for debugging + - "8888:8888" # jupyter notebook + - "8013:8013" # http + - "8043:8043" # https +{% else %} + haproxy: + image: haproxy + volumes: + - "./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg" + depends_on: +{% for i in range(cluster_node_count|int) -%} + {% set container_postfix = loop.index %} + - "awx_{{ container_postfix }}" +{% endfor %} + ports: + - "8013:8013" + - "8043:8043" + - "1936:1936" +{% endif %} redis_{{ container_postfix }}: image: redis:latest container_name: tools_redis_{{ container_postfix }} @@ -69,22 +91,6 @@ services: POSTGRES_PASSWORD: {{ pg_password }} volumes: - "awx_db:/var/lib/postgresql/data" -{% if cluster_node_count|int > 1 %} - haproxy: - image: haproxy - volumes: - - "../ha.conf:/usr/local/etc/haproxy/haproxy.cfg" - depends_on: -{% for i in range(cluster_node_count|int) -%} - {% set container_postfix = loop.index %} - - "awx_{{ container_postfix }}" -{% endfor %} - ports: - - "8013:8013" - - "8043:8043" - - "1936:1936" -{% endif -%} {# haproxy #} - volumes: awx_db: receptor: diff --git a/tools/docker-compose/haproxy.cfg b/tools/docker-compose/ansible/roles/sources/templates/haproxy.cfg.j2 similarity index 67% rename from tools/docker-compose/haproxy.cfg rename to tools/docker-compose/ansible/roles/sources/templates/haproxy.cfg.j2 index d37cbf691a..33837ded1f 100644 --- a/tools/docker-compose/haproxy.cfg +++ b/tools/docker-compose/ansible/roles/sources/templates/haproxy.cfg.j2 @@ -1,5 +1,4 @@ global - debug stats socket /tmp/admin.sock stats timeout 30s @@ -30,16 +29,18 @@ backend nodes http-request set-header X-Forwarded-Port %[dst_port] http-request add-header X-Forwarded-Proto https if { ssl_fc } option httpchk HEAD / HTTP/1.1\r\nHost:localhost - server awx-1 awx-1:8013 check - server awx-2 awx-2:8013 check - server awx-3 awx-3:8013 check +{% for i in range(cluster_node_count|int) %} + {% set container_postfix = loop.index %} + server tools_awx_{{ container_postfix }} tools_awx_{{ container_postfix }}:8013 check +{% endfor %} backend nodes_ssl mode tcp balance roundrobin - server awx-1 awx-1:8043 - server awx-2 awx-2:8043 - server awx-3 awx-3:8043 +{% for i in range(cluster_node_count|int) %} + {% set container_postfix = loop.index %} + server tools_awx_{{ container_postfix }} tools_awx_{{ container_postfix }}:8043 check +{% endfor %} listen stats bind *:1936 From 7b1edda368f82eeaca13bf75f22eada0b65b0a36 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 4 Mar 2021 15:04:36 -0500 Subject: [PATCH 168/178] support receptor in multi cluster nodes --- Makefile | 2 +- .../ansible/roles/sources/templates/docker-compose.yml.j2 | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index a68ab74c46..a06c32c2de 100644 --- a/Makefile +++ b/Makefile @@ -540,7 +540,7 @@ docker-compose-sources: ansible-playbook -i tools/docker-compose/inventory tools/docker-compose/ansible/sources.yml \ -e awx_image=$(DEV_DOCKER_TAG_BASE)/awx_devel \ -e awx_image_tag=$(COMPOSE_TAG) \ - -e cluster_node_count=$(CLUSER_NODE_COUNT) + -e cluster_node_count=$(CLUSTER_NODE_COUNT) docker-compose: docker-auth awx/projects docker-compose-sources docker-compose -f tools/docker-compose/_sources/docker-compose.yml $(COMPOSE_UP_OPTS) up diff --git a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 index 7d724c82f4..ccc42d2b0e 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 @@ -38,10 +38,10 @@ services: - "../../docker-compose/_sources/local_settings.py:/etc/tower/conf.d/local_settings.py" - "../../docker-compose/_sources/SECRET_KEY:/etc/tower/SECRET_KEY" - "../../docker-compose/receptor.conf:/etc/receptor/receptor.conf" - - "receptor:/var/run/receptor/" - "/sys/fs/cgroup:/sys/fs/cgroup" - "~/.kube/config:/var/lib/awx/.kube/config" - "redis_socket_{{ container_postfix }}:/var/run/redis/:rw" + - "receptor_{{ container_postfix }}:/var/run/receptor/" privileged: true tty: true ports: @@ -93,8 +93,8 @@ services: - "awx_db:/var/lib/postgresql/data" volumes: awx_db: - receptor: {% for i in range(cluster_node_count|int) -%} {% set container_postfix = loop.index %} + receptor_{{ container_postfix }}: redis_socket_{{ container_postfix }}: {% endfor -%} From 16a6fb5adc96d991d7e069142b0374b9342abde9 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 4 Mar 2021 15:23:04 -0500 Subject: [PATCH 169/178] add docs for cluster dev mode --- tools/docker-compose/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tools/docker-compose/README.md b/tools/docker-compose/README.md index 88a3ea1883..d381614a75 100644 --- a/tools/docker-compose/README.md +++ b/tools/docker-compose/README.md @@ -170,3 +170,15 @@ In order to launch all developer services: `launch_awx.sh` also calls `bootstrap_development.sh` so if all you are doing is launching the supervisor to start all services, you don't need to call `bootstrap_development.sh` first. + +### Start a cluster + +Certain features or bugs are only applicable when running a cluster of AWX nodes. To bring up a 3 node cluster development environment simply run the below command. + +```bash +(host)$ CLUSTER_NODE_COUNT=3 make docker-compose +``` + +`CLUSTER_NODE_COUNT` is configurable and defaults to 1, effectively a non-clustered AWX. + +Note that you may see multiple messages of the form `2021-03-04 20:11:47,666 WARNING [-] awx.main.wsbroadcast Connection from awx_2 to awx_5 failed: 'Cannot connect to host awx_5:8013 ssl:False [Name or service not known]'.`. This can happen when you bring up a cluster of many nodes, say 10, then you bring up a cluster of less nodes, say 3. In this example, there will be 7 `Instance` records in the database that represent AWX instances. The AWX development environment mimics the VM deployment (vs. kubernetes) and expects the missing nodes to be brought back to healthy by the admin. The warning message you are seeing is all of the AWX nodes trying to connect the websocket backplane. You can manually delete the `Instance` records from the database i.e. `Instance.objects.get(hostname='awx_9').delete()` to stop the warnings. From b7e614beee11990babb1cd1ca69ed27a33870561 Mon Sep 17 00:00:00 2001 From: nixocio Date: Thu, 4 Mar 2021 15:37:59 -0500 Subject: [PATCH 170/178] Fix extra re-render for Job Template Fix extra re-render for Job Template. Also, update a few unit-tests. See: https://github.com/ansible/awx/issues/9479 --- .../Lookup/ExecutionEnvironmentLookup.jsx | 46 +++++++++++++++---- .../ExecutionEnvironmentLookup.test.jsx | 30 ++++++++++-- .../JobTemplateAdd/JobTemplateAdd.test.jsx | 4 +- .../WorkflowJobTemplateAdd.test.jsx | 12 +++++ .../Template/shared/JobTemplateForm.jsx | 38 ++------------- 5 files changed, 83 insertions(+), 47 deletions(-) diff --git a/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.jsx b/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.jsx index 86f659c430..4647d5809e 100644 --- a/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.jsx +++ b/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.jsx @@ -1,11 +1,11 @@ import React, { useCallback, useEffect } from 'react'; -import { string, func, bool } from 'prop-types'; +import { string, func, bool, oneOfType, number } from 'prop-types'; import { useLocation } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { FormGroup, Tooltip } from '@patternfly/react-core'; -import { ExecutionEnvironmentsAPI } from '../../api'; +import { ExecutionEnvironmentsAPI, ProjectsAPI } from '../../api'; import { ExecutionEnvironment } from '../../types'; import { getQSConfig, parseQueryString, mergeParams } from '../../util/qs'; import Popover from '../Popover'; @@ -26,15 +26,38 @@ function ExecutionEnvironmentLookup({ i18n, isDefaultEnvironment, isDisabled, + onBlur, onChange, organizationId, popoverContent, + projectId, tooltip, value, - onBlur, }) { const location = useLocation(); + const { + request: fetchProject, + error: fetchProjectError, + isLoading: fetchProjectLoading, + result: project, + } = useRequest( + useCallback(async () => { + if (!projectId) { + return {}; + } + const { data } = await ProjectsAPI.readDetail(projectId); + return data; + }, [projectId]), + { + project: null, + } + ); + + useEffect(() => { + fetchProject(); + }, [fetchProject]); + const { result: { executionEnvironments, @@ -51,9 +74,10 @@ function ExecutionEnvironmentLookup({ const globallyAvailableParams = globallyAvailable ? { or__organization__isnull: 'True' } : {}; - const organizationIdParams = organizationId - ? { or__organization__id: organizationId } - : {}; + const organizationIdParams = + organizationId || project?.organization + ? { or__organization__id: organizationId } + : {}; const [{ data }, actionsResponse] = await Promise.all([ ExecutionEnvironmentsAPI.read( mergeParams(params, { @@ -73,7 +97,7 @@ function ExecutionEnvironmentLookup({ actionsResponse.data.actions?.GET || {} ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; - }, [location, globallyAvailable, organizationId]), + }, [location, globallyAvailable, organizationId, project]), { executionEnvironments: [], count: 0, @@ -95,7 +119,7 @@ function ExecutionEnvironmentLookup({ onBlur={onBlur} onChange={onChange} qsConfig={QS_CONFIG} - isLoading={isLoading} + isLoading={isLoading || fetchProjectLoading} isDisabled={isDisabled} renderOptionsList={({ state, dispatch, canDelete }) => ( + ); } @@ -156,12 +180,16 @@ ExecutionEnvironmentLookup.propTypes = { popoverContent: string, onChange: func.isRequired, isDefaultEnvironment: bool, + projectId: oneOfType([number, string]), + organizationId: oneOfType([number, string]), }; ExecutionEnvironmentLookup.defaultProps = { popoverContent: '', isDefaultEnvironment: false, value: null, + projectId: null, + organizationId: null, }; export default withI18n()(ExecutionEnvironmentLookup); diff --git a/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.test.jsx b/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.test.jsx index 783d43707b..7093854366 100644 --- a/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.test.jsx +++ b/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.test.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import ExecutionEnvironmentLookup from './ExecutionEnvironmentLookup'; -import { ExecutionEnvironmentsAPI } from '../../api'; +import { ExecutionEnvironmentsAPI, ProjectsAPI } from '../../api'; jest.mock('../../api'); @@ -32,6 +32,17 @@ describe('ExecutionEnvironmentLookup', () => { ExecutionEnvironmentsAPI.read.mockResolvedValue( mockedExecutionEnvironments ); + ProjectsAPI.read.mockResolvedValue({ + data: { + count: 1, + results: [ + { + id: 1, + name: 'Fuz', + }, + ], + }, + }); }); afterEach(() => { @@ -52,14 +63,21 @@ describe('ExecutionEnvironmentLookup', () => { await act(async () => { wrapper = mountWithContexts( {}} /> ); }); wrapper.update(); - expect(ExecutionEnvironmentsAPI.read).toHaveBeenCalledTimes(1); + expect(ExecutionEnvironmentsAPI.read).toHaveBeenCalledTimes(2); expect(wrapper.find('ExecutionEnvironmentLookup')).toHaveLength(1); + expect( + wrapper.find('FormGroup[label="Default Execution Environment"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="Execution Environment"]').length + ).toBe(0); }); test('should fetch execution environments', async () => { @@ -71,6 +89,12 @@ describe('ExecutionEnvironmentLookup', () => { /> ); }); - expect(ExecutionEnvironmentsAPI.read).toHaveBeenCalledTimes(1); + expect(ExecutionEnvironmentsAPI.read).toHaveBeenCalledTimes(2); + expect( + wrapper.find('FormGroup[label="Default Execution Environment"]').length + ).toBe(0); + expect( + wrapper.find('FormGroup[label="Execution Environment"]').length + ).toBe(1); }); }); diff --git a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx index 9860d968c5..b93b56ef53 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx @@ -58,7 +58,7 @@ const jobTemplateData = { timeout: 0, use_fact_cache: false, verbosity: '0', - execution_environment: { id: 1, name: 'Foo' }, + execution_environment: { id: 1, name: 'Foo', image: 'localhost.com' }, }; describe('', () => { @@ -139,7 +139,7 @@ describe('', () => { wrapper = mountWithContexts(); }); await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); - await act(() => { + await act(async () => { wrapper.find('input#template-name').simulate('change', { target: { value: 'Bar', name: 'name' }, }); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.test.jsx index c6ac734bfa..2cfd5e0093 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.test.jsx @@ -6,6 +6,7 @@ import { WorkflowJobTemplatesAPI, OrganizationsAPI, LabelsAPI, + ExecutionEnvironmentsAPI, } from '../../../api'; import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; @@ -15,6 +16,7 @@ jest.mock('../../../api/models/WorkflowJobTemplates'); jest.mock('../../../api/models/Organizations'); jest.mock('../../../api/models/Labels'); jest.mock('../../../api/models/Inventories'); +jest.mock('../../../api/models/ExecutionEnvironments'); describe('', () => { let wrapper; @@ -34,6 +36,10 @@ describe('', () => { }, }); + ExecutionEnvironmentsAPI.read.mockResolvedValue({ + data: { results: [{ id: 1, name: 'Foo', image: 'localhost.com' }] }, + }); + await act(async () => { history = createMemoryHistory({ initialEntries: ['/templates/workflow_job_template/add'], @@ -82,6 +88,11 @@ describe('', () => { .find('LabelSelect') .find('SelectToggle') .simulate('click'); + + wrapper.find('ExecutionEnvironmentLookup').invoke('onChange')({ + id: 1, + name: 'Foo', + }); }); wrapper.update(); @@ -113,6 +124,7 @@ describe('', () => { webhook_credential: undefined, webhook_service: '', webhook_url: '', + execution_environment: 1, }); expect(WorkflowJobTemplatesAPI.associateLabel).toHaveBeenCalledTimes(1); diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index c267e157bb..b1ba6aac3d 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -40,7 +40,7 @@ import { ExecutionEnvironmentLookup, } from '../../../components/Lookup'; import Popover from '../../../components/Popover'; -import { JobTemplatesAPI, ProjectsAPI } from '../../../api'; +import { JobTemplatesAPI } from '../../../api'; import LabelSelect from './LabelSelect'; import PlaybookSelect from './PlaybookSelect'; import WebhookSubForm from './WebhookSubForm'; @@ -108,30 +108,6 @@ function JobTemplateForm({ executionEnvironmentHelpers, ] = useField({ name: 'execution_environment' }); - const projectId = projectField.value?.id; - - const { - request: fetchProject, - error: fetchProjectError, - isLoading: fetchProjectLoading, - result: projectData, - } = useRequest( - useCallback(async () => { - if (!projectId) { - return {}; - } - const { data } = await ProjectsAPI.readDetail(projectId); - return data; - }, [projectId]), - { - projectData: null, - } - ); - - useEffect(() => { - fetchProject(); - }, [fetchProject]); - const { request: loadRelatedInstanceGroups, error: instanceGroupError, @@ -213,16 +189,12 @@ function JobTemplateForm({ callbackUrl = `${origin}${path}`; } - if (instanceGroupLoading || fetchProjectLoading) { + if (instanceGroupLoading) { return ; } - if (contentError || instanceGroupError || fetchProjectError) { - return ( - - ); + if (contentError || instanceGroupError) { + return ; } return ( @@ -323,7 +295,7 @@ function JobTemplateForm({ )} globallyAvailable isDisabled={!projectField.value} - organizationId={projectData?.organization} + projectId={projectField.value?.id} /> {projectField.value?.allow_override && ( From 119e80c717a683cfb61b8457fa15f524c1a4b030 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Thu, 4 Mar 2021 18:28:49 -0500 Subject: [PATCH 171/178] Delete the old installer directory --- installer/dockerfile.yml | 6 - installer/install.yml | 6 - installer/inventory | 173 ------ .../check_vars/tasks/check_openshift.yml | 48 -- installer/roles/check_vars/tasks/main.yml | 10 - installer/roles/kubernetes/defaults/main.yml | 62 -- installer/roles/kubernetes/handlers/main.yml | 5 - installer/roles/kubernetes/tasks/backup.yml | 82 --- .../roles/kubernetes/tasks/kubernetes.yml | 23 - .../kubernetes/tasks/kubernetes_auth.yml | 3 - installer/roles/kubernetes/tasks/main.yml | 320 ---------- .../roles/kubernetes/tasks/openshift.yml | 76 --- .../roles/kubernetes/tasks/openshift_auth.yml | 56 -- installer/roles/kubernetes/tasks/rekey.yml | 72 --- installer/roles/kubernetes/tasks/restore.yml | 145 ----- .../kubernetes/templates/configmap.yml.j2 | 206 ------- .../kubernetes/templates/credentials.py.j2 | 16 - .../kubernetes/templates/deployment.yml.j2 | 556 ------------------ .../kubernetes/templates/environment.sh.j2 | 5 - .../templates/management-pod.yml.j2 | 106 ---- .../templates/postgres_root_ca.yml.j2 | 8 - .../templates/postgresql-persistent.yml.j2 | 176 ------ .../templates/postgresql-values.yml.j2 | 64 -- .../roles/kubernetes/templates/secret.yml.j2 | 11 - .../kubernetes/templates/supervisor.yml.j2 | 149 ----- installer/roles/kubernetes/vars/openshift.yml | 3 - 26 files changed, 2387 deletions(-) delete mode 100644 installer/dockerfile.yml delete mode 100644 installer/install.yml delete mode 100644 installer/inventory delete mode 100644 installer/roles/check_vars/tasks/check_openshift.yml delete mode 100644 installer/roles/check_vars/tasks/main.yml delete mode 100644 installer/roles/kubernetes/defaults/main.yml delete mode 100644 installer/roles/kubernetes/handlers/main.yml delete mode 100644 installer/roles/kubernetes/tasks/backup.yml delete mode 100644 installer/roles/kubernetes/tasks/kubernetes.yml delete mode 100644 installer/roles/kubernetes/tasks/kubernetes_auth.yml delete mode 100644 installer/roles/kubernetes/tasks/main.yml delete mode 100644 installer/roles/kubernetes/tasks/openshift.yml delete mode 100644 installer/roles/kubernetes/tasks/openshift_auth.yml delete mode 100644 installer/roles/kubernetes/tasks/rekey.yml delete mode 100644 installer/roles/kubernetes/tasks/restore.yml delete mode 100644 installer/roles/kubernetes/templates/configmap.yml.j2 delete mode 100644 installer/roles/kubernetes/templates/credentials.py.j2 delete mode 100644 installer/roles/kubernetes/templates/deployment.yml.j2 delete mode 100644 installer/roles/kubernetes/templates/environment.sh.j2 delete mode 100644 installer/roles/kubernetes/templates/management-pod.yml.j2 delete mode 100644 installer/roles/kubernetes/templates/postgres_root_ca.yml.j2 delete mode 100644 installer/roles/kubernetes/templates/postgresql-persistent.yml.j2 delete mode 100644 installer/roles/kubernetes/templates/postgresql-values.yml.j2 delete mode 100644 installer/roles/kubernetes/templates/secret.yml.j2 delete mode 100644 installer/roles/kubernetes/templates/supervisor.yml.j2 delete mode 100644 installer/roles/kubernetes/vars/openshift.yml diff --git a/installer/dockerfile.yml b/installer/dockerfile.yml deleted file mode 100644 index 9b6bfdf974..0000000000 --- a/installer/dockerfile.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- name: Render AWX Dockerfile and sources - hosts: localhost - gather_facts: true - roles: - - {role: dockerfile} diff --git a/installer/install.yml b/installer/install.yml deleted file mode 100644 index a0c6d71f90..0000000000 --- a/installer/install.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- name: Build and deploy AWX - hosts: all - roles: - - {role: check_vars} - - {role: kubernetes, when: "openshift_host is defined or kubernetes_context is defined"} diff --git a/installer/inventory b/installer/inventory deleted file mode 100644 index b9e7d22cfa..0000000000 --- a/installer/inventory +++ /dev/null @@ -1,173 +0,0 @@ -localhost ansible_connection=local ansible_python_interpreter="/usr/bin/env python3" - -[all:vars] - -# Remove these lines if you want to run a local image build -# Otherwise the setup playbook will install the official Ansible images. Versions may -# be selected based on: latest, 1, 1.0, 1.0.0, 1.0.0.123 -# by default the base will be used to search for ansible/awx -dockerhub_base=ansible - -# Openshift Install -# Will need to set -e openshift_password=developer -e docker_registry_password=$(oc whoami -t) -# or set -e openshift_token=TOKEN -# openshift_host=127.0.0.1:8443 -# openshift_project=awx -# openshift_user=developer -# openshift_skip_tls_verify=False -# openshift_pg_emptydir=True - -# Kubernetes Install -# kubernetes_context=test-cluster -# kubernetes_namespace=awx -# kubernetes_web_svc_type=NodePort -# Optional Kubernetes Variables -# pg_image_registry=docker.io -# pg_serviceaccount=awx -# pg_volume_capacity=5 -# pg_persistence_storageClass=StorageClassName -# pg_persistence_existingclaim=postgres_pvc -# pg_cpu_limit=1000 -# pg_mem_limit=2 - -# Kubernetes Ingress Configuration -# You can use the variables below to configure Kubernetes Ingress -# Set hostname -# kubernetes_ingress_hostname=awx.example.org -# Add annotations. The example below shows an annotation to be used with Traefik but other Ingress controllers are also supported -# kubernetes_ingress_annotations={'kubernetes.io/ingress.class': 'traefik', 'traefik.ingress.kubernetes.io/redirect-entry-point': 'https'} -# Specify a secret for TLS termination -# kubernetes_ingress_tls_secret=awx-cert - -# Kubernetes and Openshift Install Resource Requests -# These are the request and limit values for a pod's container for task/web/redis/management. -# The total amount of requested resources for a pod is the sum of all -# resources requested by all containers in the pod -# A cpu_request of 1500 is 1.5 cores for the container to start out with. -# A cpu_limit defines the maximum cores that that container can reserve. -# A mem_request of 2 is for 2 gigabytes of memory for the container -# A mem_limit defines the maximum memory that that container can reserve. -# Default values for these entries can be found in ./roles/kubernetes/defaults/main.yml -# task_cpu_request=1500 -# task_mem_request=2 -# task_cpu_limit=2000 -# task_mem_limit=4 -# web_cpu_limit=1000 -# web_mem_limit=2 -# redis_cpu_limit=1000 -# redis_mem_limit=3 -# management_cpu_limit=2000 -# management_mem_limit=2 - -# Common Docker parameters -awx_task_hostname=awx -awx_web_hostname=awxweb -# Local directory that is mounted in the awx_postgres docker container to place the db in -postgres_data_dir="~/.awx/pgdocker" -host_port=80 -host_port_ssl=443 -#ssl_certificate= -# Optional key file -#ssl_certificate_key= -docker_compose_dir="~/.awx/awxcompose" - -# Required for Openshift when building the image on your own -# Optional for Openshift if using Dockerhub or another prebuilt registry -# Required for Docker Compose Install if building the image on your own -# Optional for Docker Compose Install if using Dockerhub or another prebuilt registry -# Define if you want the image pushed to a registry. The container definition will also use these images -# docker_registry=172.30.1.1:5000 -# docker_registry_repository=awx -# docker_registry_username=developer - - -# Set pg_hostname if you have an external postgres server, otherwise -# a new postgres service will be created -# pg_hostname=postgresql -pg_username=awx -# pg_password should be random 10 character alphanumeric string, when postgresql is running on kubernetes -# NB: it's a limitation of the "official" postgres helm chart -pg_password=awxpass -pg_database=awx -pg_port=5432 -#pg_sslmode=require - -# If requiring SSL communication (e.g. pg_sslmode='verify-full') with Postgres -# and using a self-signed certificate or a certificate signed by a custom CA -# set pg_root_ca_file to a file containing the self-signed certificate or the -# root CA certificate chain. -# pg_root_ca_file='example_root_ca.crt' - -# The following variable is only required when using the provided -# containerized postgres deployment on OpenShift -# pg_admin_password=postgrespass - -# This will create or update a default admin (superuser) account in AWX, if not provided -# then these default values are used -admin_user=admin -# admin_password=password - -# Whether or not to create preload data for demonstration purposes -create_preload_data=True - -# AWX Secret key -# It's *very* important that this stay the same between upgrades or you will lose the ability to decrypt -# your credentials -secret_key=awxsecret - -# By default a broadcast websocket secret will be generated. -# If you would like to *rerun the playbook*, you need to set a unique password. -# Otherwise it would generate a new one every playbook run. -# broadcast_websocket_secret= - -# Build AWX with official logos -# Requires cloning awx-logos repo as a sibling of this project. -# Review the trademark guidelines at https://github.com/ansible/awx-logos/blob/master/TRADEMARKS.md -# awx_official=false - -# Proxy -#http_proxy=http://proxy:3128 -#https_proxy=http://proxy:3128 -#no_proxy=mycorp.org - -# Container networking configuration -# Set the awx_task and awx_web containers' search domain(s) -#awx_container_search_domains=example.com,ansible.com -# Alternate DNS servers -#awx_alternate_dns_servers="10.1.2.3,10.2.3.4" - -# AWX project data folder. If you need access to the location where AWX stores the projects -# it manages from the docker host, you can set this to turn it into a volume for the container. -#project_data_dir=/var/lib/awx/projects - -# AWX custom virtual environment folder. Only usable for local install. -#custom_venv_dir=/opt/my-envs/ - -# CA Trust directory. If you need to provide custom CA certificates, supplying -# this variable causes this directory on the host to be bind mounted over -# /etc/pki/ca-trust in the awx_task and awx_web containers. -# If you are deploying on openshift or kubernetes, set the variable to /etc/pki/ca-trust instead, -# as the awx_web and awx_task containers will not run the `update-ca-trust` command. -#ca_trust_dir=/etc/pki/ca-trust/source/anchors - -# Include /etc/nginx/awx_extra.conf -# Note the use of glob pattern for nginx -# which makes include "optional" - i.e. not fail -# if file is absent -#extra_nginx_include="/etc/nginx/awx_extra[.]conf" - -# Docker compose explicit subnet. Set to avoid overlapping your existing LAN networks. -#docker_compose_subnet="172.17.0.1/16" -# -# Allow for different docker logging drivers -# By Default; the logger will be json-file, however you can override -# that by uncommenting the docker_logger below. -# Be aware that journald may rate limit your log messages if you choose it. -# See: https://docs.docker.com/config/containers/logging/configure/ -# docker_logger=journald -# - -# Add extra hosts to docker compose file. This might be necessary to -# sneak in servernames. For example for DMZ self-signed CA certificates. -# Equivialent to using the --add-host parameter with "docker run". -#docker_compose_extra_hosts="otherserver.local:192.168.0.1,ldap-server.local:192.168.0.2" diff --git a/installer/roles/check_vars/tasks/check_openshift.yml b/installer/roles/check_vars/tasks/check_openshift.yml deleted file mode 100644 index a2bebf6b16..0000000000 --- a/installer/roles/check_vars/tasks/check_openshift.yml +++ /dev/null @@ -1,48 +0,0 @@ -# check_openshift.yml ---- -- name: openshift_project should be defined - assert: - that: - - openshift_project is defined and openshift_project != '' - msg: "Set the value of 'openshift_project' in the inventory file." - -- name: openshift_user should be defined - assert: - that: - - openshift_user is defined and openshift_user != '' - msg: "Set the value of 'openshift_user' in the inventory file." - -- name: openshift_password or openshift_token should be defined - assert: - that: - - (openshift_password is defined and openshift_password != '') or - (openshift_token is defined and openshift_token != '') - msg: "Set the value of 'openshift_password' or 'openshift_token' in the inventory file." - -- name: docker_registry should be defined if not using dockerhub - assert: - that: - - docker_registry is defined and docker_registry != '' - msg: "Set the value of 'docker_registry' in the inventory file." - when: dockerhub_base is not defined - -- name: docker_registry_repository should be defined if not using dockerhub - assert: - that: - - docker_registry_repository is defined and docker_registry_repository != '' - msg: "Set the value of 'docker_registry_repository' in the inventory file." - when: dockerhub_base is not defined - -- name: docker_registry_username should be defined if not using dockerhub - assert: - that: - - docker_registry_username is defined and docker_registry_username != '' - msg: "Set the value of 'docker_registry_username' in the inventory file." - when: dockerhub_base is not defined - -- name: docker_registry_password should be defined - assert: - that: - - docker_registry_password is defined and docker_registry_password != '' - msg: "Set the value of 'docker_registry_password' in the inventory file." - when: dockerhub_base is not defined diff --git a/installer/roles/check_vars/tasks/main.yml b/installer/roles/check_vars/tasks/main.yml deleted file mode 100644 index 9ac079e560..0000000000 --- a/installer/roles/check_vars/tasks/main.yml +++ /dev/null @@ -1,10 +0,0 @@ -# main.yml ---- -- name: admin_password should be defined - assert: - that: - - admin_password is defined and admin_password != '' - msg: "Set the value of 'admin_password' in the inventory file." - -- include_tasks: check_openshift.yml - when: openshift_host is defined and openshift_host != '' diff --git a/installer/roles/kubernetes/defaults/main.yml b/installer/roles/kubernetes/defaults/main.yml deleted file mode 100644 index 754ff86fe0..0000000000 --- a/installer/roles/kubernetes/defaults/main.yml +++ /dev/null @@ -1,62 +0,0 @@ ---- -dockerhub_version: "{{ lookup('file', playbook_dir + '/../VERSION') }}" -create_preload_data: true - -admin_user: 'admin' -admin_email: 'root@localhost' -admin_password: '' - -kubernetes_base_path: "{{ local_base_config_path|default('/tmp') }}/{{ kubernetes_deployment_name }}-config" - -kubernetes_awx_version: "{{ dockerhub_version }}" -kubernetes_awx_image: "ansible/awx" -kubernetes_web_svc_type: "NodePort" - -awx_psp_create: false -awx_psp_name: 'awx' -awx_psp_privileged: true - -web_mem_request: 1 -web_cpu_request: 500 -web_security_context_enabled: true -web_security_context_privileged: false - -task_mem_request: 2 -task_cpu_request: 1500 -task_security_context_enabled: true -task_security_context_privileged: true - -redis_mem_request: 2 -redis_cpu_request: 500 -redis_security_context_enabled: true -redis_security_context_privileged: false -redis_security_context_user: 1001 - -kubernetes_redis_image: "redis" -kubernetes_redis_image_tag: "latest" -kubernetes_redis_config_mount_path: "/usr/local/etc/redis/redis.conf" - -openshift_pg_emptydir: false -openshift_pg_pvc_name: postgresql - -kubernetes_deployment_name: awx -kubernetes_serviceaccount_name: awx - -kubernetes_deployment_replica_size: 1 - -postgress_activate_wait: 60 - -restore_backup_file: "./tower-openshift-backup-latest.tar.gz" - -insights_url_base: "https://example.org" -automation_analytics_url: "https://example.org" -insights_agent_mime: "application/example" - -custom_venvs_path: "/opt/custom-venvs" -custom_venvs_python: "python2" - -ca_trust_bundle: "/etc/pki/tls/certs/ca-bundle.crt" - -container_groups_image: "ansible/ansible-runner" - -uwsgi_bash: "bash -c" diff --git a/installer/roles/kubernetes/handlers/main.yml b/installer/roles/kubernetes/handlers/main.yml deleted file mode 100644 index 89b3fa701c..0000000000 --- a/installer/roles/kubernetes/handlers/main.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -- name: remove-rmq_cert_tempdir - file: - state: absent - path: "{{ rmq_cert_tempdir.path }}" diff --git a/installer/roles/kubernetes/tasks/backup.yml b/installer/roles/kubernetes/tasks/backup.yml deleted file mode 100644 index d2d41bbbc1..0000000000 --- a/installer/roles/kubernetes/tasks/backup.yml +++ /dev/null @@ -1,82 +0,0 @@ ---- -- name: Determine the timestamp for the backup. - set_fact: - now: '{{ lookup("pipe", "date +%F-%T") }}' - -- include_tasks: openshift_auth.yml - when: openshift_host is defined - -- include_tasks: kubernetes_auth.yml - when: kubernetes_context is defined - -- name: Use kubectl or oc - set_fact: - kubectl_or_oc: "{{ openshift_oc_bin if openshift_oc_bin is defined else 'kubectl' }}" - -- name: Delete any existing management pod - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - delete pod ansible-tower-management --grace-period=0 --ignore-not-found - -- name: Template management pod - set_fact: - management_pod: "{{ lookup('template', 'management-pod.yml.j2') }}" - -- name: Create management pod - shell: | - echo {{ management_pod | quote }} | {{ kubectl_or_oc }} apply -f - - -- name: Wait for management pod to start - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - get pod ansible-tower-management -o jsonpath="{.status.phase}" - register: result - until: result.stdout == "Running" - retries: 60 - delay: 10 - -- name: Create directory for backup - file: - state: directory - path: "{{ playbook_dir }}/tower-openshift-backup-{{ now }}" - -- name: Precreate file for database dump - file: - path: "{{ playbook_dir }}/tower-openshift-backup-{{ now }}/tower.db" - state: touch - mode: 0600 - -- name: Dump database - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} exec ansible-tower-management -- \ - bash -c "PGPASSWORD={{ pg_password | quote }} \ - pg_dump --clean --create \ - --host='{{ pg_hostname | default('postgresql') }}' \ - --port={{ pg_port | default('5432') }} \ - --username='{{ pg_username }}' \ - --dbname='{{ pg_database }}'" > {{ playbook_dir }}/tower-openshift-backup-{{ now }}/tower.db - no_log: true - -- name: Copy inventory into backup directory - copy: - src: "{{ inventory_file }}" - dest: "{{ playbook_dir }}/tower-openshift-backup-{{ now }}/" - mode: 0600 - -- name: Delete management pod - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - delete pod ansible-tower-management --grace-period=0 --ignore-not-found - -- name: Create backup archive - archive: - path: "{{ playbook_dir }}/tower-openshift-backup-{{ now }}" - dest: "{{ item }}" - with_items: - - "{{ playbook_dir }}/tower-openshift-backup-{{ now }}.tar.gz" - - "{{ playbook_dir }}/tower-openshift-backup-latest.tar.gz" - -- name: Remove temporary backup directory - file: - path: "{{ playbook_dir }}/tower-openshift-backup-{{ now }}" - state: absent diff --git a/installer/roles/kubernetes/tasks/kubernetes.yml b/installer/roles/kubernetes/tasks/kubernetes.yml deleted file mode 100644 index 89370bed1f..0000000000 --- a/installer/roles/kubernetes/tasks/kubernetes.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- -- name: Get Namespace Detail - shell: "kubectl get namespace {{ kubernetes_namespace }}" - register: namespace_details - ignore_errors: true - -- name: Create AWX Kubernetes Project - shell: "kubectl create namespace {{ kubernetes_namespace }}" - when: namespace_details.rc != 0 - -- name: Set postgresql service name - set_fact: - postgresql_service_name: "{{ kubernetes_deployment_name }}-postgresql" - when: "pg_hostname is not defined or pg_hostname == ''" - -- name: Get Kubernetes API version - command: | - kubectl version -o json - register: kube_version - -- name: Extract server version from command output - set_fact: - kube_api_version: "{{ (kube_version.stdout | from_json).serverVersion.gitVersion[1:] }}" diff --git a/installer/roles/kubernetes/tasks/kubernetes_auth.yml b/installer/roles/kubernetes/tasks/kubernetes_auth.yml deleted file mode 100644 index 7f9d29456b..0000000000 --- a/installer/roles/kubernetes/tasks/kubernetes_auth.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -- name: Set the Kubernetes Context - shell: "kubectl config use-context {{ kubernetes_context }}" diff --git a/installer/roles/kubernetes/tasks/main.yml b/installer/roles/kubernetes/tasks/main.yml deleted file mode 100644 index dc6639b56b..0000000000 --- a/installer/roles/kubernetes/tasks/main.yml +++ /dev/null @@ -1,320 +0,0 @@ ---- -- name: Generate broadcast websocket secret - set_fact: - broadcast_websocket_secret: "{{ lookup('password', '/dev/null length=128') }}" - run_once: true - no_log: true - when: broadcast_websocket_secret is not defined - -- fail: - msg: "Only set one of kubernetes_context or openshift_host" - when: openshift_host is defined and kubernetes_context is defined - -- include_tasks: "{{ tasks }}" - with_items: - - openshift_auth.yml - - openshift.yml - loop_control: - loop_var: tasks - when: openshift_host is defined - -- include_tasks: "{{ tasks }}" - with_items: - - kubernetes_auth.yml - - kubernetes.yml - loop_control: - loop_var: tasks - when: kubernetes_context is defined - -- name: Use kubectl or oc - set_fact: - kubectl_or_oc: "{{ openshift_oc_bin if openshift_oc_bin is defined else 'kubectl' }}" - -- set_fact: - deployment_object: "deployment" - -- name: Record deployment size - shell: | - {{ kubectl_or_oc }} get {{ deployment_object }} \ - {{ kubernetes_deployment_name }} \ - -n {{ kubernetes_namespace }} -o=jsonpath='{.status.replicas}' - register: deployment_details - ignore_errors: true - -- name: Set expected post-deployment Replicas value - set_fact: - kubernetes_deployment_replica_size: "{{ deployment_details.stdout | int }}" - when: deployment_details.rc == 0 - -- name: Delete existing Deployment (or StatefulSet) - shell: | - {{ kubectl_or_oc }} delete sts \ - {{ kubernetes_deployment_name }} -n {{ kubernetes_namespace }} --ignore-not-found - {{ kubectl_or_oc }} delete {{ deployment_object }} \ - {{ kubernetes_deployment_name }} -n {{ kubernetes_namespace }} --ignore-not-found - -- name: Get Postgres Service Detail - shell: "{{ kubectl_or_oc }} describe svc {{ postgresql_service_name }} -n {{ kubernetes_namespace }}" - register: postgres_svc_details - ignore_errors: true - when: "pg_hostname is not defined or pg_hostname == ''" - -- name: Deploy PostgreSQL (OpenShift) - block: - - name: Template PostgreSQL Deployment (OpenShift) - template: - src: postgresql-persistent.yml.j2 - dest: "{{ kubernetes_base_path }}/postgresql-persistent.yml" - mode: '0600' - - - name: Deploy and Activate Postgres (OpenShift) - shell: | - {{ openshift_oc_bin }} new-app --file={{ kubernetes_base_path }}/postgresql-persistent.yml \ - -e MEMORY_LIMIT={{ pg_memory_limit|default('512') }}Mi \ - -e DATABASE_SERVICE_NAME=postgresql \ - -e POSTGRESQL_MAX_CONNECTIONS={{ pg_max_connections|default(1024) }} \ - -e POSTGRESQL_USER={{ pg_username }} \ - -e POSTGRESQL_PASSWORD={{ pg_password | quote }} \ - -e POSTGRESQL_DATABASE={{ pg_database | quote }} \ - -e POSTGRESQL_VERSION=12 \ - -n {{ kubernetes_namespace }} - register: openshift_pg_activate - no_log: true - when: - - pg_hostname is not defined or pg_hostname == '' - - postgres_svc_details is defined and postgres_svc_details.rc != 0 - - openshift_host is defined - -- name: Deploy PostgreSQL (Kubernetes) - block: - - name: Create Temporary Values File (Kubernetes) - tempfile: - state: file - suffix: .yml - register: values_file - - - name: Populate Temporary Values File (Kubernetes) - template: - src: postgresql-values.yml.j2 - dest: "{{ values_file.path }}" - no_log: true - - - name: Deploy and Activate Postgres (Kubernetes) - shell: | - helm repo add stable https://charts.helm.sh/stable - helm repo update - helm upgrade {{ postgresql_service_name }} \ - --install \ - --namespace {{ kubernetes_namespace }} \ - --version="8.3.0" \ - --values {{ values_file.path }} \ - stable/postgresql - register: kubernetes_pg_activate - no_log: true - - - name: Remove tempfile - file: - path: "{{ values_file.path }}" - state: absent - when: - - pg_hostname is not defined or pg_hostname == '' - - postgres_svc_details is defined and postgres_svc_details.rc != 0 - - kubernetes_context is defined - -- name: Set postgresql hostname to helm package service (Kubernetes) - set_fact: - pg_hostname: "{{ postgresql_service_name }}" - when: - - pg_hostname is not defined or pg_hostname == '' - - kubernetes_context is defined - -- name: Wait for Postgres to activate - pause: - seconds: "{{ postgress_activate_wait }}" - when: openshift_pg_activate.changed or kubernetes_pg_activate.changed - -- name: Check postgres version and upgrade Postgres if necessary (Openshift) - block: - - name: Check if Postgres 10 is being used - shell: | - POD=$({{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - get pods -l=name=postgresql --field-selector status.phase=Running -o jsonpath="{.items[0].metadata.name}") - {{ kubectl_or_oc }} exec $POD -n {{ kubernetes_namespace }} -- bash -c "psql -tAc 'select version()'" - register: pg_version - - name: Upgrade postgres if necessary - block: - - name: Set new pg image - shell: | - IMAGE=registry.redhat.io/rhel-8/postgresql-12 - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} set image dc/postgresql postgresql=$IMAGE - - - name: Wait for change to take affect - pause: - seconds: 5 - - - name: Set env var for pg upgrade - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} set env dc/postgresql POSTGRESQL_UPGRADE=copy - - - name: Wait for change to take affect - pause: - seconds: 5 - - - name: Set env var for new pg version - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} set env dc/postgresql POSTGRESQL_VERSION=12 - - - name: Wait for Postgres to redeploy - pause: - seconds: "{{ postgress_activate_wait }}" - - - name: Wait for Postgres to finish upgrading - shell: | - POD=$({{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - get pods -l=name=postgresql -o jsonpath="{.items[0].metadata.name}") - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} logs $POD | grep 'Upgrade DONE' - register: pg_upgrade_logs - retries: 360 - delay: 10 - until: pg_upgrade_logs is success - - - name: Unset upgrade env var - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} set env dc/postgresql POSTGRESQL_UPGRADE- - - - name: Wait for Postgres to redeploy - pause: - seconds: "{{ postgress_activate_wait }}" - when: "pg_version is success and '10' in pg_version.stdout" - when: - - pg_hostname is not defined or pg_hostname == '' - - postgres_svc_details is defined and postgres_svc_details.rc != 0 - - openshift_host is defined - -- name: Set image names if using custom registry - block: - - name: Set awx image name - set_fact: - kubernetes_awx_image: "{{ docker_registry }}/{{ docker_registry_repository }}/{{ awx_image }}" - when: kubernetes_awx_image is not defined - when: docker_registry is defined - -- name: Determine Deployment api version - set_fact: - kubernetes_deployment_api_version: "{{ 'apps/v1' if kube_api_version is version('1.9', '>=') else 'apps/v1beta1' }}" - -- name: Use Custom Root CA file for PosgtreSQL SSL communication - block: - - name: Get Root CA file contents - set_fact: - postgres_root_ca_cert: "{{ lookup('file', pg_root_ca_file) }}" - no_log: true - - - name: Render Root CA template - set_fact: - postgres_root_ca: "{{ lookup('template', 'postgres_root_ca.yml.j2') }}" - no_log: true - - - name: Apply Root CA template - shell: | - echo {{ postgres_root_ca | quote }} | {{ kubectl_or_oc }} apply -f - - no_log: true - - - name: Set Root CA file name - set_fact: - postgres_root_ca_filename: 'postgres_root_ca.crt' - - - name: Set Root CA file location - set_fact: - ca_trust_bundle: '/etc/tower/{{ postgres_root_ca_filename }}' - when: - - pg_root_ca_file is defined - - pg_root_ca_file != '' - -- name: Render deployment templates - set_fact: - "{{ item }}": "{{ lookup('template', item + '.yml.j2') }}" - with_items: - - 'configmap' - - 'secret' - - 'deployment' - - 'supervisor' - no_log: true - -- name: Apply Deployment - shell: | - echo {{ item | quote }} | {{ kubectl_or_oc }} apply -f - - with_items: - - "{{ configmap }}" - - "{{ secret }}" - - "{{ deployment }}" - - "{{ supervisor }}" - no_log: true - -- name: Delete any existing management pod - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - delete pod ansible-tower-management --grace-period=0 --ignore-not-found - -- name: Template management pod - set_fact: - management_pod: "{{ lookup('template', 'management-pod.yml.j2') }}" - -- name: Create management pod - shell: | - echo {{ management_pod | quote }} | {{ kubectl_or_oc }} apply -f - - -- name: Wait for management pod to start - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - get pod ansible-tower-management -o jsonpath="{.status.phase}" - register: result - until: result.stdout == "Running" - retries: 60 - delay: 10 - -- name: Migrate database - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} exec ansible-tower-management -- \ - bash -c "awx-manage migrate --noinput" - -- name: Check for Tower Super users - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} exec ansible-tower-management -- \ - bash -c "echo 'from django.contrib.auth.models import User; nsu = User.objects.filter(is_superuser=True).count(); exit(0 if nsu > 0 else 1)' | awx-manage shell" - register: super_check - ignore_errors: true - changed_when: super_check.rc > 0 - -- name: create django super user if it does not exist - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} exec ansible-tower-management -- \ - bash -c "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('{{ admin_user }}', '{{ admin_email }}', '{{ admin_password }}')\" | awx-manage shell" - no_log: true - when: super_check.rc > 0 - -- name: update django super user password - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} exec ansible-tower-management -- \ - bash -c "awx-manage update_password --username='{{ admin_user }}' --password='{{ admin_password }}'" - no_log: true - register: result - changed_when: "'Password updated' in result.stdout" - -- name: Create the default organization if it is needed. - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} exec ansible-tower-management -- \ - bash -c "awx-manage create_preload_data" - register: cdo - changed_when: "'added' in cdo.stdout" - when: create_preload_data | bool - -- name: Delete management pod - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - delete pod ansible-tower-management --grace-period=0 --ignore-not-found - -- name: Scale up deployment - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - scale {{ deployment_object }} {{ kubernetes_deployment_name }} --replicas={{ replicas | default(kubernetes_deployment_replica_size) }} diff --git a/installer/roles/kubernetes/tasks/openshift.yml b/installer/roles/kubernetes/tasks/openshift.yml deleted file mode 100644 index c85ab961ba..0000000000 --- a/installer/roles/kubernetes/tasks/openshift.yml +++ /dev/null @@ -1,76 +0,0 @@ ---- -- name: Get Project Detail - shell: "{{ openshift_oc_bin }} get project {{ openshift_project }}" - register: project_details - ignore_errors: true - -- name: Create AWX Openshift Project - shell: "{{ openshift_oc_bin }} new-project {{ openshift_project }}" - when: project_details.rc != 0 - -- name: Ensure PostgreSQL PVC is available - block: - - name: Check PVC status - command: "{{ openshift_oc_bin }} get pvc {{ openshift_pg_pvc_name }} -n {{ openshift_project }} -o=jsonpath='{.status.phase}'" - register: pg_pvc_status - ignore_errors: true - - - name: Ensure PostgreSQL PVC is available - assert: - that: - - pg_pvc_status.stdout in ["Bound", "Pending"] - msg: "Ensure a PVC named '{{ openshift_pg_pvc_name }}' is available in the namespace '{{ openshift_project }}'." - when: - - pg_hostname is not defined or pg_hostname == '' - - openshift_pg_emptydir is defined and (openshift_pg_emptydir | bool) != true - -- name: Set postgresql service name - set_fact: - postgresql_service_name: "postgresql" - when: "pg_hostname is not defined or pg_hostname == ''" - -- name: Add privileged SCC to service account - shell: | - {{ openshift_oc_bin }} adm policy add-scc-to-user privileged system:serviceaccount:{{ openshift_project }}:awx - -# https://github.com/openshift/origin/issues/19182#issuecomment-378233606 -# If oc version ever grows a -o json option, remove the following tasks -# and go with the approach in kubernetes.yml. -- name: Get Kubernetes Config - command: | - {{ openshift_oc_bin }} config view -o json - register: kube_config_cmd - no_log: true - -- name: Convert kube config to dictionary - set_fact: - kube_config: "{{ kube_config_cmd.stdout | from_json }}" - no_log: true - -- name: Extract current context from kube config - set_fact: - current_kube_context: "{{ kube_config['current-context'] }}" - -- name: Find cluster for current context - set_fact: - kube_cluster: | - {{ (kube_config.contexts | - selectattr("name", "match", current_kube_context) | - list)[0].context.cluster }} - -- name: Find server for current context - set_fact: - kube_server: | - {{ (kube_config.clusters | - selectattr("name", "match", kube_cluster|trim) | - list)[0].cluster.server }} - -- name: Get kube version from api server - uri: - url: "{{ kube_server | trim }}/version" - validate_certs: false - register: kube_version - -- name: Extract server version from command output - set_fact: - kube_api_version: "{{ kube_version.json.gitVersion[1:] }}" diff --git a/installer/roles/kubernetes/tasks/openshift_auth.yml b/installer/roles/kubernetes/tasks/openshift_auth.yml deleted file mode 100644 index 1b53cda59a..0000000000 --- a/installer/roles/kubernetes/tasks/openshift_auth.yml +++ /dev/null @@ -1,56 +0,0 @@ ---- -- include_vars: openshift.yml - -- name: Set kubernetes_namespace - set_fact: - kubernetes_namespace: "{{ openshift_project }}" - -- name: Ensure workspace directories exist - file: - path: "{{ item }}" - state: directory - with_items: - - "{{ kubernetes_base_path }}" - - "{{ openshift_oc_config_file | dirname }}" - -- name: Authenticate with OpenShift via user and password - shell: | - {{ openshift_oc_bin }} login {{ openshift_host }} \ - -u {{ openshift_user }} \ - -p {{ openshift_password | quote }} \ - --insecure-skip-tls-verify={{ openshift_skip_tls_verify | default(false) | bool }} - when: - - openshift_user is defined - - openshift_password is defined - - openshift_token is not defined - register: openshift_auth_result - ignore_errors: true - no_log: true - -- name: OpenShift authentication failed on TLS verification - fail: - msg: "Failed to verify TLS, consider settings openshift_skip_tls_verify=True {{ openshift_auth_result.stderr | default('certificate does not match hostname') }}" - when: - - openshift_skip_tls_verify is not defined or not openshift_skip_tls_verify - - openshift_auth_result.rc is defined and openshift_auth_result.rc != 0 - - openshift_auth_result.stderr is defined and (openshift_auth_result.stderr | search("certificate that does not match its hostname")) - -- name: OpenShift authentication failed - fail: - msg: "{{ openshift_auth_result.stderr | default('Invalid credentials') }}" - when: openshift_auth_result.rc is defined and openshift_auth_result.rc != 0 - -- name: Authenticate with OpenShift via token - shell: | - {{ openshift_oc_bin }} login {{ openshift_host }} \ - --token {{ openshift_token }} \ - --insecure-skip-tls-verify={{ openshift_skip_tls_verify | default(false) | bool }} - when: openshift_token is defined - register: openshift_auth_result - ignore_errors: true - no_log: true - -- name: OpenShift authentication failed - fail: - msg: "{{ openshift_auth_result.stderr | default('Invalid token') }}" - when: openshift_auth_result.rc is defined and openshift_auth_result.rc != 0 diff --git a/installer/roles/kubernetes/tasks/rekey.yml b/installer/roles/kubernetes/tasks/rekey.yml deleted file mode 100644 index 91ed2828ed..0000000000 --- a/installer/roles/kubernetes/tasks/rekey.yml +++ /dev/null @@ -1,72 +0,0 @@ ---- -- include_tasks: openshift_auth.yml - when: openshift_host is defined - -- include_tasks: kubernetes_auth.yml - when: kubernetes_context is defined - -- name: Use kubectl or oc - set_fact: - kubectl_or_oc: "{{ openshift_oc_bin if openshift_oc_bin is defined else 'kubectl' }}" - -- set_fact: - deployment_object: "deployment" - -- name: Record deployment size - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - get {{ deployment_object }} {{ kubernetes_deployment_name }} -o jsonpath="{.status.replicas}" - register: deployment_size - -- name: Scale deployment down - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - scale {{ deployment_object }} {{ kubernetes_deployment_name }} --replicas=0 - -- name: Wait for scale down - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} get pods \ - -o jsonpath='{.items[*].metadata.name}' \ - | tr -s '[[:space:]]' '\n' \ - | grep {{ kubernetes_deployment_name }} \ - | grep -v postgres | wc -l - register: tower_pods - until: (tower_pods.stdout | trim) == '0' - retries: 30 - -- name: Delete any existing management pod - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - delete pod ansible-tower-management --grace-period=0 --ignore-not-found - -- name: Template management pod - set_fact: - management_pod: "{{ lookup('template', 'management-pod.yml.j2') }}" - -- name: Create management pod - shell: | - echo {{ management_pod | quote }} | {{ kubectl_or_oc }} apply -f - - -- name: Wait for management pod to start - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - get pod ansible-tower-management -o jsonpath="{.status.phase}" - register: result - until: result.stdout == "Running" - retries: 60 - delay: 10 - -- name: generate a new SECRET_KEY - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - exec -i ansible-tower-management -- bash -c "awx-manage regenerate_secret_key" - register: new_key - -- name: print the new SECRET_KEY - debug: - msg: "{{ new_key.stdout }}" - -- name: Delete management pod - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - delete pod ansible-tower-management --grace-period=0 --ignore-not-found diff --git a/installer/roles/kubernetes/tasks/restore.yml b/installer/roles/kubernetes/tasks/restore.yml deleted file mode 100644 index 9d0358e751..0000000000 --- a/installer/roles/kubernetes/tasks/restore.yml +++ /dev/null @@ -1,145 +0,0 @@ ---- -- include_tasks: openshift_auth.yml - when: openshift_host is defined - -- include_tasks: kubernetes_auth.yml - when: kubernetes_context is defined - -- name: Use kubectl or oc - set_fact: - kubectl_or_oc: "{{ openshift_oc_bin if openshift_oc_bin is defined else 'kubectl' }}" - -- name: Remove any present restore directories - file: - state: absent - path: "{{ playbook_dir }}/tower-openshift-restore" - -- name: Create directory for restore data - file: - state: directory - path: "{{ playbook_dir }}/tower-openshift-restore" - -- name: Unarchive Tower backup - unarchive: - src: "{{ restore_backup_file }}" - dest: "{{ playbook_dir }}/tower-openshift-restore" - extra_opts: [--strip-components=1] - -- name: Verify if common.tar.gz exists - stat: - path: "{{ playbook_dir }}/tower-openshift-restore/common.tar.gz" - register: common_tarball - -- name: Unarchive Tower backup from common.tar.gz - unarchive: - src: "{{ playbook_dir }}/tower-openshift-restore/common.tar.gz" - dest: "{{ playbook_dir }}/tower-openshift-restore" - extra_opts: [--strip-components=1] - when: common_tarball.stat.exists - -- set_fact: - deployment_object: "deployment" - -- name: Record deployment size - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - get {{ deployment_object }} {{ kubernetes_deployment_name }} -o jsonpath="{.status.replicas}" - register: deployment_size - -- name: Scale deployment down - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - scale {{ deployment_object }} {{ kubernetes_deployment_name }} --replicas=0 - -- name: Delete management pod - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - delete pod ansible-tower-management --grace-period=0 --ignore-not-found - -- name: Wait for scale down - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} get pods \ - -o jsonpath='{.items[*].metadata.name}' \ - | tr -s '[[:space:]]' '\n' \ - | grep {{ kubernetes_deployment_name }} \ - | grep -v postgres | wc -l - register: tower_pods - until: (tower_pods.stdout | trim) == '0' - retries: 30 - -- name: Setup Management Pod & Restore (External DB) - block: - - name: Delete any existing management pod - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - delete pod ansible-tower-management --grace-period=0 --ignore-not-found - - - name: Template management pod - set_fact: - management_pod: "{{ lookup('template', 'management-pod.yml.j2') }}" - - - name: Create management pod - shell: | - echo {{ management_pod | quote }} | {{ kubectl_or_oc }} apply -f - - - - name: Wait for management pod to start - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - get pod ansible-tower-management -o jsonpath="{.status.phase}" - register: result - until: result.stdout == "Running" - retries: 60 - delay: 10 - - - name: Perform a PostgreSQL restore (for External Postgres) - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - exec -i ansible-tower-management -- bash -c "PGPASSWORD={{ pg_password | quote }} \ - psql \ - --host={{ pg_hostname | default('postgresql') }} \ - --port={{ pg_port | default('5432') }} \ - --username={{ pg_username }} \ - --dbname=template1" < {{ playbook_dir }}/tower-openshift-restore/tower.db - no_log: true - - - name: Delete management pod - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - delete pod ansible-tower-management --grace-period=0 --ignore-not-found - when: pg_hostname is defined or pg_hostname != '' - -- name: Restore (Containerized DB) - block: - - name: Temporarily grant createdb role - shell: | - POD=$({{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - get pods -l=name=postgresql --field-selector status.phase=Running -o jsonpath="{.items[0].metadata.name}") - {{ kubectl_or_oc }} exec $POD -n {{ kubernetes_namespace }} -- bash -c "\ - psql --dbname=template1 -c 'ALTER USER \"{{ pg_username }}\" CREATEDB;'" - - - name: Perform a PostgreSQL restore - shell: | - POD=$({{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - get pods -l=name=postgresql --field-selector status.phase=Running -o jsonpath="{.items[0].metadata.name}") - {{ kubectl_or_oc }} exec -i $POD -n {{ kubernetes_namespace }} -- bash -c "\ - psql --dbname=template1" < {{ playbook_dir }}/tower-openshift-restore/tower.db - no_log: true - - - name: Revoke createdb role - shell: | - POD=$({{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - get pods -l=name=postgresql --field-selector status.phase=Running -o jsonpath="{.items[0].metadata.name}") - {{ kubectl_or_oc }} exec $POD -n {{ kubernetes_namespace }} -- bash -c "\ - psql --dbname=template1 -c 'ALTER USER \"{{ pg_username }}\" NOCREATEDB;'" - when: pg_hostname is not defined or pg_hostname == '' - -- name: Remove restore directory - file: - state: absent - path: "{{ playbook_dir }}/tower-openshift-restore" - -- name: Scale deployment back up - shell: | - {{ kubectl_or_oc }} -n {{ kubernetes_namespace }} \ - scale {{ deployment_object }} {{ kubernetes_deployment_name }} --replicas={{ deployment_size.stdout }} - when: deployment_size.stdout != '' diff --git a/installer/roles/kubernetes/templates/configmap.yml.j2 b/installer/roles/kubernetes/templates/configmap.yml.j2 deleted file mode 100644 index 2d7fd50aac..0000000000 --- a/installer/roles/kubernetes/templates/configmap.yml.j2 +++ /dev/null @@ -1,206 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ kubernetes_deployment_name }}-config - namespace: {{ kubernetes_namespace }} -data: - {{ kubernetes_deployment_name }}_nginx_conf: | - #user awx; - - worker_processes 1; - - pid /tmp/nginx.pid; - - events { - worker_connections 1024; - } - - http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - server_tokens off; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /dev/stdout main; - - map $http_upgrade $connection_upgrade { - default upgrade; - '' close; - } - - sendfile on; - #tcp_nopush on; - #gzip on; - - upstream uwsgi { - server 127.0.0.1:8050; - } - - upstream daphne { - server 127.0.0.1:8051; - } - - {% if ssl_certificate is defined %} - server { - listen 8052 default_server; - server_name _; - - # Redirect all HTTP links to the matching HTTPS page - return 301 https://$host$request_uri; - } - {%endif %} - - server { - {% if ssl_certificate is defined %} - listen 8053 ssl; - - ssl_certificate /etc/nginx/awxweb.pem; - ssl_certificate_key /etc/nginx/awxweb.pem; - {% else %} - listen 8052 default_server; - {% endif %} - - # If you have a domain name, this is where to add it - server_name _; - keepalive_timeout 65; - - # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months) - add_header Strict-Transport-Security max-age=15768000; - - # Protect against click-jacking https://www.owasp.org/index.php/Testing_for_Clickjacking_(OTG-CLIENT-009) - add_header X-Frame-Options "DENY"; - - location /nginx_status { - stub_status on; - access_log off; - allow 127.0.0.1; - deny all; - } - - location /static/ { - alias /var/lib/awx/public/static/; - } - - location /favicon.ico { alias /var/lib/awx/public/static/favicon.ico; } - - location /websocket { - # Pass request to the upstream alias - proxy_pass http://daphne; - # Require http version 1.1 to allow for upgrade requests - proxy_http_version 1.1; - # We want proxy_buffering off for proxying to websockets. - proxy_buffering off; - # http://en.wikipedia.org/wiki/X-Forwarded-For - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - # enable this if you use HTTPS: - proxy_set_header X-Forwarded-Proto https; - # pass the Host: header from the client for the sake of redirects - proxy_set_header Host $http_host; - # We've set the Host header, so we don't need Nginx to muddle - # about with redirects - proxy_redirect off; - # Depending on the request value, set the Upgrade and - # connection headers - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - } - - location / { - # Add trailing / if missing - rewrite ^(.*)$http_host(.*[^/])$ $1$http_host$2/ permanent; - uwsgi_read_timeout 120s; - uwsgi_pass uwsgi; - include /etc/nginx/uwsgi_params; - {%- if extra_nginx_include is defined %} - include {{ extra_nginx_include }}; - {%- endif %} - proxy_set_header X-Forwarded-Port 443; - uwsgi_param HTTP_X_FORWARDED_PORT 443; - } - } - } - - {{ kubernetes_deployment_name }}_settings: | - import os - import socket - ADMINS = () - - AWX_PROOT_ENABLED = True - - # Automatically deprovision pods that go offline - AWX_AUTO_DEPROVISION_INSTANCES = True - - SYSTEM_TASK_ABS_CPU = {{ ((task_cpu_request|int / 1000) * 4)|int }} - SYSTEM_TASK_ABS_MEM = {{ ((task_mem_request|int * 1024) / 100)|int }} - - INSIGHTS_URL_BASE = "{{ insights_url_base }}" - INSIGHTS_AGENT_MIME = "{{ insights_agent_mime }}" - AUTOMATION_ANALYTICS_URL = "{{ automation_analytics_url }}" - - #Autoprovisioning should replace this - CLUSTER_HOST_ID = socket.gethostname() - SYSTEM_UUID = os.environ.get('MY_POD_UID', '00000000-0000-0000-0000-000000000000') - - SESSION_COOKIE_SECURE = False - CSRF_COOKIE_SECURE = False - - REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR'] - - STATIC_ROOT = '/var/lib/awx/public/static' - PROJECTS_ROOT = '/var/lib/awx/projects' - AWX_ANSIBLE_COLLECTIONS_PATHS = '/var/lib/awx/vendor/awx_ansible_collections' - JOBOUTPUT_ROOT = '/var/lib/awx/job_status' - SECRET_KEY = open('/etc/tower/SECRET_KEY', 'rb').read().strip() - ALLOWED_HOSTS = ['*'] - SERVER_EMAIL = 'root@localhost' - DEFAULT_FROM_EMAIL = 'webmaster@localhost' - EMAIL_SUBJECT_PREFIX = '[AWX] ' - EMAIL_HOST = 'localhost' - EMAIL_PORT = 25 - EMAIL_HOST_USER = '' - EMAIL_HOST_PASSWORD = '' - EMAIL_USE_TLS = False - - LOGGING['handlers']['console'] = { - '()': 'logging.StreamHandler', - 'level': 'DEBUG', - 'formatter': 'simple', - 'filters': ['guid'], - } - - LOGGING['loggers']['django.request']['handlers'] = ['console'] - LOGGING['loggers']['rest_framework.request']['handlers'] = ['console'] - LOGGING['loggers']['awx']['handlers'] = ['console', 'external_logger'] - LOGGING['loggers']['awx.main.commands.run_callback_receiver']['handlers'] = ['console'] - LOGGING['loggers']['awx.main.commands.inventory_import']['handlers'] = ['console'] - LOGGING['loggers']['awx.main.tasks']['handlers'] = ['console', 'external_logger'] - LOGGING['loggers']['awx.main.scheduler']['handlers'] = ['console', 'external_logger'] - LOGGING['loggers']['django_auth_ldap']['handlers'] = ['console'] - LOGGING['loggers']['social']['handlers'] = ['console'] - LOGGING['loggers']['system_tracking_migrations']['handlers'] = ['console'] - LOGGING['loggers']['rbac_migrations']['handlers'] = ['console'] - LOGGING['loggers']['awx.isolated.manager.playbooks']['handlers'] = ['console'] - LOGGING['handlers']['callback_receiver'] = {'class': 'logging.NullHandler'} - LOGGING['handlers']['fact_receiver'] = {'class': 'logging.NullHandler'} - LOGGING['handlers']['task_system'] = {'class': 'logging.NullHandler'} - LOGGING['handlers']['tower_warnings'] = {'class': 'logging.NullHandler'} - LOGGING['handlers']['rbac_migrations'] = {'class': 'logging.NullHandler'} - LOGGING['handlers']['system_tracking_migrations'] = {'class': 'logging.NullHandler'} - LOGGING['handlers']['management_playbooks'] = {'class': 'logging.NullHandler'} - - USE_X_FORWARDED_PORT = True - - AWX_CONTAINER_GROUP_DEFAULT_IMAGE = "{{ container_groups_image }}" - REDHAT_CANDLEPIN_HOST = "{{ candlepin_host | default(omit) }}" - REDHAT_CANDLEPIN_VERIFY = "{{ candlepin_verify | default(omit) }}" - BROADCAST_WEBSOCKET_PORT = 8052 - BROADCAST_WEBSOCKET_PROTOCOL = 'http' - - {{ kubernetes_deployment_name }}_redis_conf: | - unixsocket /var/run/redis/redis.sock - unixsocketperm 660 - port 0 - bind 127.0.0.1 diff --git a/installer/roles/kubernetes/templates/credentials.py.j2 b/installer/roles/kubernetes/templates/credentials.py.j2 deleted file mode 100644 index 74995988d8..0000000000 --- a/installer/roles/kubernetes/templates/credentials.py.j2 +++ /dev/null @@ -1,16 +0,0 @@ -DATABASES = { - 'default': { - 'ATOMIC_REQUESTS': True, - 'ENGINE': 'awx.main.db.profiled_pg', - 'NAME': "{{ pg_database }}", - 'USER': "{{ pg_username }}", - 'PASSWORD': "{{ pg_password }}", - 'HOST': "{{ pg_hostname|default('postgresql') }}", - 'PORT': "{{ pg_port }}", - 'OPTIONS': { 'sslmode': '{{ pg_sslmode|default("prefer") }}', - 'sslrootcert': '{{ ca_trust_bundle }}', - }, - } -} - -BROADCAST_WEBSOCKET_SECRET = "{{ broadcast_websocket_secret | b64encode }}" diff --git a/installer/roles/kubernetes/templates/deployment.yml.j2 b/installer/roles/kubernetes/templates/deployment.yml.j2 deleted file mode 100644 index da329d23de..0000000000 --- a/installer/roles/kubernetes/templates/deployment.yml.j2 +++ /dev/null @@ -1,556 +0,0 @@ ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ kubernetes_serviceaccount_name }} - namespace: {{ kubernetes_namespace }} -{% if kubernetes_service_account_annotations is defined %} - annotations: -{% for key, value in kubernetes_service_account_annotations.items() %} - {{ key }}: "{{ value }}" -{% endfor %} -{% endif %} -{% if kubernetes_image_pull_secrets is defined %} -imagePullSecrets: - - name: "{{ kubernetes_image_pull_secrets }}" -{% endif %} - -{% if awx_psp_create is defined and awx_psp_create | bool %} ---- -apiVersion: policy/v1beta1 -kind: PodSecurityPolicy -metadata: - name: {{ awx_psp_name }}-psp -spec: -{% if awx_psp_privileged is defined %} - privileged: {{ awx_psp_privileged }} - allowPrivilegeEscalation: {{ awx_psp_privileged }} -{% endif %} - requiredDropCapabilities: - - ALL - volumes: - - 'configMap' - - 'emptyDir' - - 'projected' - - 'secret' - - 'downwardAPI' - - 'persistentVolumeClaim' - hostNetwork: false - hostIPC: false - hostPID: false - runAsUser: - rule: 'MustRunAsNonRoot' - seLinux: - rule: 'RunAsAny' - supplementalGroups: - rule: 'RunAsAny' - fsGroup: - rule: 'RunAsAny' - ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - namespace: {{ kubernetes_namespace }} - name: {{ awx_psp_name }}-role -rules: -- apiGroups: - - policy - resources: - - podsecuritypolicies - resourceNames: - - {{ awx_psp_name }}-psp - verbs: - - use - ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: {{ awx_psp_name }}-role-binding - namespace: {{ kubernetes_namespace }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: {{ awx_psp_name }}-role -subjects: -- kind: ServiceAccount - name: {{ kubernetes_serviceaccount_name }} - namespace: {{ kubernetes_namespace }} -{% endif %} - ---- -apiVersion: {{ kubernetes_deployment_api_version }} -kind: Deployment -metadata: - name: {{ kubernetes_deployment_name }} - namespace: {{ kubernetes_namespace }} -{% if kubernetes_deployment_annotations is defined %} - annotations: -{% for key, value in kubernetes_deployment_annotations.items() %} - {{ key }}: "{{ value }}" -{% endfor %} -{% endif %} -{% if openshift_host is defined %} - labels: - app: {{ kubernetes_deployment_name }} -{% endif %} -spec: - replicas: 1 -{% if kubernetes_deployment_api_version == "apps/v1" %} - selector: - matchLabels: - app: {{ kubernetes_deployment_name }} -{% endif %} - template: - metadata: -{% if kubernetes_pod_annotations is defined %} - annotations: -{% for key, value in kubernetes_pod_annotations.items() %} - {{ key }}: "{{ value }}" -{% endfor %} -{% endif %} - labels: - name: {{ kubernetes_deployment_name }}-web-deploy - service: django - app: {{ kubernetes_deployment_name }} - spec: - serviceAccountName: {{ kubernetes_serviceaccount_name }} - terminationGracePeriodSeconds: 10 -{% if custom_venvs is defined %} -{% set trusted_hosts = "" %} - initContainers: - - image: 'centos:7' - name: init-custom-venvs -{% if http_proxy is defined or https_proxy is defined %} -{% set trusted_hosts = "--trusted-host pypi.org --trusted-host files.pythonhosted.org --trusted-host pypi.python.org" %} - env: -{% if http_proxy is defined %} - - name: http_proxy - value: {{ http_proxy }} -{% endif %} -{% if https_proxy is defined %} - - name: https_proxy - value: {{ https_proxy }} -{% endif %} -{% if no_proxy is defined %} - - name: no_proxy - value: {{ no_proxy }} -{% endif %} -{% endif %} - command: - - sh - - '-c' - - >- - yum install -y ansible curl python-setuptools epel-release \ - openssl openssl-devel gcc python-devel && - yum install -y python-virtualenv python36 python36-devel && - mkdir -p {{ custom_venvs_path }} && -{% for custom_venv in custom_venvs %} - virtualenv -p {{ custom_venv.python | default(custom_venvs_python) }} \ - {{ custom_venvs_path }}/{{ custom_venv.name }} && - source {{ custom_venvs_path }}/{{ custom_venv.name }}/bin/activate && - {{ custom_venvs_path }}/{{ custom_venv.name }}/bin/pip install {{ trusted_hosts }} -U pip && - {{ custom_venvs_path }}/{{ custom_venv.name }}/bin/pip install {{ trusted_hosts }} -U psutil \ - "ansible=={{ custom_venv.python_ansible_version }}" && -{% if custom_venv.python_modules is defined %} - {{ custom_venvs_path }}/{{ custom_venv.name }}/bin/pip install {{ trusted_hosts }} -U \ - {% for module in custom_venv.python_modules %}{{ module }} {% endfor %} && -{% endif %} - deactivate && -{% endfor %} - : - volumeMounts: - - name: custom-venvs - mountPath: {{ custom_venvs_path }} -{% endif %} - containers: - - name: {{ kubernetes_deployment_name }}-web -{% if web_security_context_enabled is defined and web_security_context_enabled | bool %} - securityContext: -{% if web_security_context_privileged is defined %} - privileged: {{ web_security_context_privileged }} -{% endif %} -{% endif %} - image: "{{ kubernetes_awx_image }}:{{ kubernetes_awx_version }}" - imagePullPolicy: Always - ports: - - containerPort: 8052 -{% if ca_trust_dir is defined %} - env: - - name: REQUESTS_CA_BUNDLE - value: /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem -{% endif %} - volumeMounts: -{% if postgres_root_ca_cert is defined %} - - name: {{ kubernetes_deployment_name }}-postgres-root-ca-cert - mountPath: {{ ca_trust_bundle }} - subPath: {{ postgres_root_ca_filename }} - readOnly: true -{% endif %} - - name: supervisor-socket - mountPath: "/var/run/supervisor" - - name: rsyslog-socket - mountPath: "/var/run/awx-rsyslog" - - name: rsyslog-dir - mountPath: "/var/lib/awx/rsyslog" -{% if ca_trust_dir is defined %} - - name: {{ kubernetes_deployment_name }}-ca-trust-dir - mountPath: "{{ ca_trust_dir }}" - readOnly: true -{% endif %} -{% if project_data_dir is defined %} - - name: {{ kubernetes_deployment_name }}-project-data-dir - mountPath: "/var/lib/awx/projects" - readOnly: false -{% endif %} -{% if custom_venvs is defined %} - - name: custom-venvs - mountPath: {{ custom_venvs_path }} -{% endif %} - - name: {{ kubernetes_deployment_name }}-application-config - mountPath: "/etc/tower/settings.py" - subPath: settings.py - readOnly: true - - - name: {{ kubernetes_deployment_name }}-nginx-config - mountPath: /etc/nginx/nginx.conf - subPath: nginx.conf - readOnly: true - - - name: "{{ kubernetes_deployment_name }}-application-credentials" - mountPath: "/etc/tower/conf.d/" - readOnly: true - - - name: {{ kubernetes_deployment_name }}-supervisor-web-config - mountPath: "/etc/supervisord.conf" - subPath: supervisor.conf - readOnly: true - - - name: {{ kubernetes_deployment_name }}-supervisor-task-config - mountPath: "/etc/supervisord_task.conf" - subPath: supervisor_task.conf - readOnly: true - - - name: {{ kubernetes_deployment_name }}-secret-key - mountPath: "/etc/tower/SECRET_KEY" - subPath: SECRET_KEY - readOnly: true - - - name: {{ kubernetes_deployment_name }}-redis-socket - mountPath: "/var/run/redis" - - resources: - requests: - memory: "{{ web_mem_request }}Gi" - cpu: "{{ web_cpu_request }}m" -{% if web_mem_limit is defined or web_cpu_limit is defined %} - limits: -{% endif %} -{% if web_mem_limit is defined %} - memory: "{{ web_mem_limit }}Gi" -{% endif %} -{% if web_cpu_limit is defined %} - cpu: "{{ web_cpu_limit }}m" -{% endif %} - - name: {{ kubernetes_deployment_name }}-task -{% if task_security_context_enabled is defined and task_security_context_enabled | bool %} - securityContext: -{% if task_security_context_privileged is defined %} - privileged: {{ task_security_context_privileged }} -{% endif %} -{% endif %} - image: "{{ kubernetes_awx_image }}:{{ kubernetes_awx_version }}" - command: - - /usr/bin/launch_awx_task.sh - imagePullPolicy: Always - volumeMounts: -{% if postgres_root_ca_cert is defined %} - - name: {{ kubernetes_deployment_name }}-postgres-root-ca-cert - mountPath: {{ ca_trust_bundle }} - subPath: {{ postgres_root_ca_filename }} - readOnly: true -{% endif %} - - name: supervisor-socket - mountPath: "/var/run/supervisor" - - name: rsyslog-socket - mountPath: "/var/run/awx-rsyslog" - - name: rsyslog-dir - mountPath: "/var/lib/awx/rsyslog" -{% if ca_trust_dir is defined %} - - name: {{ kubernetes_deployment_name }}-ca-trust-dir - mountPath: "{{ ca_trust_dir }}" - readOnly: true -{% endif %} -{% if custom_venvs is defined %} - - name: custom-venvs - mountPath: {{ custom_venvs_path }} -{% endif %} - - name: {{ kubernetes_deployment_name }}-application-config - mountPath: "/etc/tower/settings.py" - subPath: settings.py - readOnly: true - - - name: "{{ kubernetes_deployment_name }}-application-credentials" - mountPath: "/etc/tower/conf.d/" - readOnly: true - - - name: {{ kubernetes_deployment_name }}-supervisor-web-config - mountPath: "/etc/supervisord.conf" - subPath: supervisor.conf - readOnly: true - - - name: {{ kubernetes_deployment_name }}-supervisor-task-config - mountPath: "/etc/supervisord_task.conf" - subPath: supervisor_task.conf - readOnly: true - - - name: {{ kubernetes_deployment_name }}-secret-key - mountPath: "/etc/tower/SECRET_KEY" - subPath: SECRET_KEY - readOnly: true - - - name: {{ kubernetes_deployment_name }}-redis-socket - mountPath: "/var/run/redis" - env: - - name: SUPERVISOR_WEB_CONFIG_PATH - value: "/etc/supervisord.conf" - - name: AWX_SKIP_MIGRATIONS - value: "1" - - name: MY_POD_UID - valueFrom: - fieldRef: - fieldPath: metadata.uid - - name: MY_POD_IP - valueFrom: - fieldRef: - fieldPath: status.podIP -{% if ca_trust_dir is defined %} - - name: REQUESTS_CA_BUNDLE - value: /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem -{% endif %} - resources: - requests: - memory: "{{ task_mem_request }}Gi" - cpu: "{{ task_cpu_request }}m" -{% if task_mem_limit is defined or task_cpu_limit is defined %} - limits: -{% endif %} -{% if task_mem_limit is defined %} - memory: "{{ task_mem_limit }}Gi" -{% endif %} -{% if task_cpu_limit is defined %} - cpu: "{{ task_cpu_limit }}m" -{% endif %} - - name: {{ kubernetes_deployment_name }}-redis -{% if redis_security_context_enabled is defined and redis_security_context_enabled | bool %} - securityContext: -{% if redis_security_context_privileged is defined %} - privileged: {{ redis_security_context_privileged }} -{% endif %} -{% if redis_security_context_user is defined %} - runAsUser: {{ redis_security_context_user }} -{% endif %} -{% endif %} - image: {{ kubernetes_redis_image }}:{{ kubernetes_redis_image_tag }} - imagePullPolicy: Always - args: ["redis-server", "{{ kubernetes_redis_config_mount_path }}"] - volumeMounts: - - name: {{ kubernetes_deployment_name }}-redis-config - mountPath: "{{ kubernetes_redis_config_mount_path }}" - subPath: redis.conf - readOnly: true - - - name: {{ kubernetes_deployment_name }}-redis-socket - mountPath: "/var/run/redis" - resources: - requests: - memory: "{{ redis_mem_request }}Gi" - cpu: "{{ redis_cpu_request }}m" -{% if redis_mem_limit is defined or redis_cpu_limit is defined %} - limits: -{% endif %} -{% if redis_mem_limit is defined %} - memory: "{{ redis_mem_limit }}Gi" -{% endif %} -{% if redis_cpu_limit is defined %} - cpu: "{{ redis_cpu_limit }}m" -{% endif %} -{% if tolerations is defined %} - tolerations: -{{ tolerations | to_nice_yaml(indent=2) | indent(width=8, indentfirst=True) }} -{% endif %} -{% if node_selector is defined %} - nodeSelector: -{{ node_selector | to_nice_yaml(indent=2) | indent(width=8, indentfirst=True) }} -{% endif %} -{% if affinity is defined %} - affinity: -{{ affinity | to_nice_yaml(indent=2) | indent(width=8, indentfirst=True) }} -{% endif %} - volumes: -{% if postgres_root_ca_cert is defined %} - - name: {{ kubernetes_deployment_name }}-postgres-root-ca-cert - configMap: - name: {{ kubernetes_deployment_name }}-postgres-root-ca-cert - items: - - key: postgres_root_ca.crt - path: postgres_root_ca.crt -{% endif %} - - name: supervisor-socket - emptyDir: {} - - name: rsyslog-socket - emptyDir: {} - - name: rsyslog-dir - emptyDir: {} -{% if ca_trust_dir is defined %} - - name: {{ kubernetes_deployment_name }}-ca-trust-dir - hostPath: - path: "{{ ca_trust_dir }}" - type: Directory -{% endif %} -{% if project_data_dir is defined %} - - name: {{ kubernetes_deployment_name }}-project-data-dir - hostPath: - path: "{{ project_data_dir }}" - type: Directory -{% endif %} -{% if custom_venvs is defined %} - - name: custom-venvs - emptyDir: {} -{% endif %} - - name: {{ kubernetes_deployment_name }}-application-config - configMap: - name: {{ kubernetes_deployment_name }}-config - items: - - key: {{ kubernetes_deployment_name }}_settings - path: settings.py - - - name: {{ kubernetes_deployment_name }}-nginx-config - configMap: - name: {{ kubernetes_deployment_name }}-config - items: - - key: {{ kubernetes_deployment_name }}_nginx_conf - path: nginx.conf - - - name: {{ kubernetes_deployment_name }}-redis-config - configMap: - name: {{ kubernetes_deployment_name }}-config - items: - - key: {{ kubernetes_deployment_name }}_redis_conf - path: redis.conf - - - name: "{{ kubernetes_deployment_name }}-application-credentials" - secret: - secretName: "{{ kubernetes_deployment_name }}-secrets" - items: - - key: credentials_py - path: 'credentials.py' - - key: environment_sh - path: 'environment.sh' - - - name: {{ kubernetes_deployment_name }}-supervisor-web-config - configMap: - name: {{ kubernetes_deployment_name }}-supervisor-config - items: - - key: supervisor-web-config - path: 'supervisor.conf' - - - name: {{ kubernetes_deployment_name }}-supervisor-task-config - configMap: - name: {{ kubernetes_deployment_name }}-supervisor-config - items: - - key: supervisor-task-config - path: 'supervisor_task.conf' - - - name: {{ kubernetes_deployment_name }}-secret-key - secret: - secretName: "{{ kubernetes_deployment_name }}-secrets" - items: - - key: secret_key - path: SECRET_KEY - - - name: {{ kubernetes_deployment_name }}-redis-socket - emptyDir: {} - ---- -apiVersion: v1 -kind: Service -metadata: - name: {{ kubernetes_deployment_name }}-web-svc - namespace: {{ kubernetes_namespace }} - labels: - name: {{ kubernetes_deployment_name }}-web-svc -{% if kubernetes_service_annotations is defined %} - annotations: -{% for key, value in kubernetes_service_annotations.items() %} - {{ key }}: "{{ value }}" -{% endfor %} -{% endif %} -spec: - type: {{ kubernetes_web_svc_type }} - ports: - - name: http - port: 80 -{% if kubernetes_web_svc_type == "ClusterIP" %} - nodePort: null -{% endif %} - targetPort: 8052 - selector: - name: {{ kubernetes_deployment_name }}-web-deploy - -{% if kubernetes_context is defined %} ---- -apiVersion: extensions/v1beta1 -kind: Ingress -metadata: - name: {{ kubernetes_deployment_name }}-web-svc - namespace: {{ kubernetes_namespace }} -{% if kubernetes_ingress_annotations is defined %} - annotations: -{% for key, value in kubernetes_ingress_annotations.items() %} - {{ key }}: "{{ value }}" -{% endfor %} -{% endif %} - -spec: -{% if kubernetes_ingress_hostname is defined %} - rules: - - host: {{ kubernetes_ingress_hostname }} - http: - paths: - - path: / - backend: - serviceName: {{ kubernetes_deployment_name }}-web-svc - servicePort: 80 -{% else %} - backend: - serviceName: {{ kubernetes_deployment_name }}-web-svc - servicePort: 80 -{% endif %} -{% if kubernetes_ingress_tls_secret is defined %} - tls: - - hosts: - - {{ kubernetes_ingress_hostname }} - secretName: {{ kubernetes_ingress_tls_secret }} -{% endif %} -{% endif %} -{% if openshift_host is defined %} ---- -apiVersion: v1 -kind: Route -metadata: - name: {{ kubernetes_deployment_name }}-web-svc - namespace: {{ kubernetes_namespace }} -spec: - port: - targetPort: http - tls: - insecureEdgeTerminationPolicy: Redirect - termination: edge - to: - kind: Service - name: {{ kubernetes_deployment_name }}-web-svc - weight: 100 - wildcardPolicy: None -{% endif %} diff --git a/installer/roles/kubernetes/templates/environment.sh.j2 b/installer/roles/kubernetes/templates/environment.sh.j2 deleted file mode 100644 index 45fd3fba8e..0000000000 --- a/installer/roles/kubernetes/templates/environment.sh.j2 +++ /dev/null @@ -1,5 +0,0 @@ -DATABASE_USER={{ pg_username }} -DATABASE_NAME={{ pg_database }} -DATABASE_HOST={{ pg_hostname|default('postgresql') }} -DATABASE_PORT={{ pg_port|default('5432') }} -DATABASE_PASSWORD={{ pg_password | quote }} diff --git a/installer/roles/kubernetes/templates/management-pod.yml.j2 b/installer/roles/kubernetes/templates/management-pod.yml.j2 deleted file mode 100644 index 2a41853fa8..0000000000 --- a/installer/roles/kubernetes/templates/management-pod.yml.j2 +++ /dev/null @@ -1,106 +0,0 @@ ---- -apiVersion: v1 -kind: Pod -metadata: - name: ansible-tower-management - namespace: {{ kubernetes_namespace }} -{% if kubernetes_pod_annotations is defined %} - annotations: -{% for key, value in kubernetes_pod_annotations.items() %} - {{ key }}: {{ value | quote }} -{% endfor %} -{% endif %} -spec: -{% if kubernetes_image_pull_secrets is defined %} - imagePullSecrets: - - name: "{{ kubernetes_image_pull_secrets }}" -{% endif %} - containers: - - name: ansible-tower-management - image: "{{ kubernetes_awx_image }}:{{ kubernetes_awx_version }}" - imagePullPolicy: Always - command: ["sleep", "infinity"] - volumeMounts: -{% if ca_trust_dir is defined %} - - name: {{ kubernetes_deployment_name }}-ca-trust-dir - mountPath: "/etc/pki/ca-trust/source/anchors/" - readOnly: true - -{% endif %} - - name: {{ kubernetes_deployment_name }}-application-config - mountPath: "/etc/tower/settings.py" - subPath: settings.py - readOnly: true -{% if postgres_root_ca_cert is defined %} - - name: {{ kubernetes_deployment_name }}-postgres-root-ca-cert - mountPath: {{ ca_trust_bundle }} - subPath: {{ postgres_root_ca_filename }} - readOnly: true -{% endif %} - - name: "{{ kubernetes_deployment_name }}-application-credentials" - mountPath: "/etc/tower/conf.d/" - readOnly: true - - - name: {{ kubernetes_deployment_name }}-secret-key - mountPath: "/etc/tower/SECRET_KEY" - subPath: SECRET_KEY - readOnly: true - resources: -{% if management_mem_limit is defined or management_cpu_limit is defined %} - limits: -{% endif %} -{% if management_mem_limit is defined %} - memory: "{{ management_mem_limit }}Gi" -{% endif %} -{% if management_cpu_limit is defined %} - cpu: "{{ management_cpu_limit }}m" -{% endif %} -{% if tolerations is defined %} - tolerations: -{{ tolerations | to_nice_yaml(indent=2) | indent(width=4, indentfirst=True) }} -{% endif %} -{% if node_selector is defined %} - nodeSelector: -{{ node_selector | to_nice_yaml(indent=2) | indent(width=4, indentfirst=True) }} -{% endif %} -{% if affinity is defined %} - affinity: -{{ affinity | to_nice_yaml(indent=2) | indent(width=4, indentfirst=True) }} -{% endif %} - volumes: -{% if ca_trust_dir is defined %} - - name: {{ kubernetes_deployment_name }}-ca-trust-dir - hostPath: - path: "{{ ca_trust_dir }}" - type: Directory - -{% endif %} - - name: {{ kubernetes_deployment_name }}-application-config - configMap: - name: {{ kubernetes_deployment_name }}-config - items: - - key: {{ kubernetes_deployment_name }}_settings - path: settings.py -{% if postgres_root_ca_cert is defined %} - - name: {{ kubernetes_deployment_name }}-postgres-root-ca-cert - configMap: - name: {{ kubernetes_deployment_name }}-postgres-root-ca-cert - items: - - key: postgres_root_ca.crt - path: postgres_root_ca.crt -{% endif %} - - name: {{ kubernetes_deployment_name }}-secret-key - secret: - secretName: "{{ kubernetes_deployment_name }}-secrets" - items: - - key: secret_key - path: SECRET_KEY - - - name: "{{ kubernetes_deployment_name }}-application-credentials" - secret: - secretName: "{{ kubernetes_deployment_name }}-secrets" - items: - - key: credentials_py - path: 'credentials.py' - - restartPolicy: Never diff --git a/installer/roles/kubernetes/templates/postgres_root_ca.yml.j2 b/installer/roles/kubernetes/templates/postgres_root_ca.yml.j2 deleted file mode 100644 index 091bfff196..0000000000 --- a/installer/roles/kubernetes/templates/postgres_root_ca.yml.j2 +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ kubernetes_deployment_name }}-postgres-root-ca-cert - namespace: {{ kubernetes_namespace }} -data: - postgres_root_ca.crt: | - {{ postgres_root_ca_cert | indent(width=4) }} diff --git a/installer/roles/kubernetes/templates/postgresql-persistent.yml.j2 b/installer/roles/kubernetes/templates/postgresql-persistent.yml.j2 deleted file mode 100644 index c688083cbb..0000000000 --- a/installer/roles/kubernetes/templates/postgresql-persistent.yml.j2 +++ /dev/null @@ -1,176 +0,0 @@ -apiVersion: v1 -kind: Template -labels: - template: postgresql-persistent-template -message: |- - The following service(s) have been created in your project: ${DATABASE_SERVICE_NAME}. - - Username: ${POSTGRESQL_USER} - Password: ${POSTGRESQL_PASSWORD} - Database Name: ${POSTGRESQL_DATABASE} - Connection URL: postgresql://${DATABASE_SERVICE_NAME}:5432/ - - For more information about using this template, including OpenShift considerations, see https://github.com/sclorg/postgresql-container/. -metadata: - annotations: - description: |- - PostgreSQL database service, with persistent storage. For more information about using this template, including OpenShift considerations, see https://github.com/sclorg/postgresql-container/. - - NOTE: Scaling to more than one replica is not supported. You must have persistent volumes available in your cluster to use this template. - iconClass: icon-postgresql - openshift.io/display-name: PostgreSQL (Persistent) - tags: database,postgresql - template.openshift.io/documentation-url: https://docs.openshift.org/latest/using_images/db_images/postgresql.html - template.openshift.io/long-description: This template provides a standalone - PostgreSQL server with a database created. The database is stored on persistent - storage. The database name, username, and password are chosen via parameters - when provisioning this service. - template.openshift.io/provider-display-name: Red Hat, Inc. - template.openshift.io/support-url: https://access.redhat.com - name: postgresql-persistent -objects: -- apiVersion: v1 - kind: Secret - metadata: - annotations: - template.openshift.io/expose-database_name: '{.data[''database-name'']}' - template.openshift.io/expose-password: '{.data[''database-password'']}' - template.openshift.io/expose-admin_password: '{.data[''database-admin-password'']}' - template.openshift.io/expose-username: '{.data[''database-user'']}' - name: ${DATABASE_SERVICE_NAME} - stringData: - database-name: ${POSTGRESQL_DATABASE} - database-password: ${POSTGRESQL_PASSWORD} - database-admin-password: ${POSTGRESQL_PASSWORD} - database-user: ${POSTGRESQL_USER} -- apiVersion: v1 - kind: Service - metadata: - annotations: - template.openshift.io/expose-uri: postgres://{.spec.clusterIP}:{.spec.ports[?(.name=="postgresql")].port} - name: ${DATABASE_SERVICE_NAME} - spec: - ports: - - name: postgresql - nodePort: 0 - port: 5432 - protocol: TCP - targetPort: 5432 - selector: - name: ${DATABASE_SERVICE_NAME} - sessionAffinity: None - type: ClusterIP - status: - loadBalancer: {} -- apiVersion: v1 - kind: DeploymentConfig - metadata: - annotations: - template.alpha.openshift.io/wait-for-ready: "true" - name: ${DATABASE_SERVICE_NAME} - spec: - replicas: 1 - selector: - name: ${DATABASE_SERVICE_NAME} - strategy: - type: Recreate - template: - metadata: - labels: - name: ${DATABASE_SERVICE_NAME} - spec: - containers: - - capabilities: {} - env: - - name: POSTGRESQL_USER - valueFrom: - secretKeyRef: - key: database-user - name: ${DATABASE_SERVICE_NAME} - - name: POSTGRESQL_PASSWORD - valueFrom: - secretKeyRef: - key: database-password - name: ${DATABASE_SERVICE_NAME} - - name: POSTGRESQL_DATABASE - valueFrom: - secretKeyRef: - key: database-name - name: ${DATABASE_SERVICE_NAME} - - name: POSTGRESQL_MAX_CONNECTIONS - value: ${POSTGRESQL_MAX_CONNECTIONS} - image: registry.redhat.io/rhel8/postgresql-12 - imagePullPolicy: IfNotPresent - livenessProbe: - exec: - command: - - /usr/libexec/check-container - - --live - initialDelaySeconds: 120 - timeoutSeconds: 10 - name: postgresql - ports: - - containerPort: 5432 - protocol: TCP - readinessProbe: - exec: - command: - - /usr/libexec/check-container - initialDelaySeconds: 5 - timeoutSeconds: 1 - resources: - limits: - memory: ${MEMORY_LIMIT} - securityContext: - capabilities: {} - privileged: false - terminationMessagePath: /dev/termination-log - volumeMounts: - - mountPath: /var/lib/pgsql/data - name: ${DATABASE_SERVICE_NAME}-data - dnsPolicy: ClusterFirst - restartPolicy: Always - volumes: - - name: ${DATABASE_SERVICE_NAME}-data -{% if openshift_pg_emptydir | bool %} - emptyDir: {} -{% else %} - persistentVolumeClaim: - claimName: {{ openshift_pg_pvc_name }} -{% endif %} - triggers: - - type: ConfigChange - status: {} -parameters: -- description: Maximum amount of memory the container can use. - displayName: Memory Limit - name: MEMORY_LIMIT - required: true - value: 512Mi -- description: The OpenShift Namespace where the ImageStream resides. - displayName: Namespace - name: NAMESPACE - value: openshift -- description: The name of the OpenShift Service exposed for the database. - displayName: Database Service Name - name: DATABASE_SERVICE_NAME - required: true - value: postgresql -- description: Username for PostgreSQL user that will be used for accessing the - database. - displayName: PostgreSQL Connection Username - from: user[A-Z0-9]{3} - generate: expression - name: POSTGRESQL_USER - required: true -- description: Password for the PostgreSQL connection user. - displayName: PostgreSQL Connection Password - from: '[a-zA-Z0-9]{16}' - generate: expression - name: POSTGRESQL_PASSWORD - required: true -- description: Name of the PostgreSQL database accessed. - displayName: PostgreSQL Database Name - name: POSTGRESQL_DATABASE - required: true - value: sampledb diff --git a/installer/roles/kubernetes/templates/postgresql-values.yml.j2 b/installer/roles/kubernetes/templates/postgresql-values.yml.j2 deleted file mode 100644 index 4fca12a7b7..0000000000 --- a/installer/roles/kubernetes/templates/postgresql-values.yml.j2 +++ /dev/null @@ -1,64 +0,0 @@ -postgresqlUsername: {{ pg_username }} -postgresqlPassword: {{ pg_password }} -postgresqlDatabase: {{ pg_database }} -persistence: - size: {{ pg_volume_capacity|default('5') }}Gi -{% if pg_persistence_storageClass is defined %} - storageClass: {{ pg_persistence_storageClass }} -{% endif %} -{% if pg_persistence_existingclaim is defined %} - existingClaim: {{ pg_persistence_existingclaim }} -{% endif %} -{% if pg_cpu_limit is defined or pg_mem_limit is defined %} -resources: - limits: -{% if pg_cpu_limit is defined %} - cpu: {{ pg_cpu_limit | string }}m -{% endif %} -{% if pg_mem_limit is defined %} - memory: {{ pg_mem_limit | string }}Gi -{% endif %} -{% endif %} -{% if tolerations is defined or node_selector is defined or affinity is defined %} -master: -{% if tolerations is defined %} - tolerations: -{{ tolerations | to_nice_yaml(indent=2) | indent(width=4, indentfirst=True) }} -{% endif %} -{% if node_selector is defined %} - nodeSelector: -{{ node_selector | to_nice_yaml(indent=2) | indent(width=4, indentfirst=True) }} -{% endif %} -{% if affinity is defined %} - affinity: -{{ affinity | to_nice_yaml(indent=2) | indent(width=4, indentfirst=True) }} -{% endif %} -{% endif %} -image: -{% if pg_image_registry is defined %} -# The default bitnami image from the chart doesn't work on ARM - registry: {{ pg_image_registry }} -{% endif %} -{% if pg_image_registry is not defined %} - registry: docker.io/bitnami -{% endif %} - repository: postgresql - tag: '12.5.0' -volumePermissions: - image: -{% if pg_image_registry is defined %} - registry: {{ pg_image_registry }} -{% endif %} - # The default bitnami image from the chart doesn't work on ARM - repository: alpine - tag: '3' -{% if pg_image_registry is defined %} -metrics: - image: - registry: {{ pg_image_registry }} -{% endif %} -{% if pg_serviceaccount is defined %} -serviceAccount: - enabled: true - name: {{ pg_serviceaccount }} -{% endif %} diff --git a/installer/roles/kubernetes/templates/secret.yml.j2 b/installer/roles/kubernetes/templates/secret.yml.j2 deleted file mode 100644 index 989cb4485f..0000000000 --- a/installer/roles/kubernetes/templates/secret.yml.j2 +++ /dev/null @@ -1,11 +0,0 @@ ---- -apiVersion: v1 -kind: Secret -metadata: - namespace: {{ kubernetes_namespace }} - name: "{{ kubernetes_deployment_name }}-secrets" -type: Opaque -data: - secret_key: "{{ secret_key | b64encode }}" - credentials_py: "{{ lookup('template', 'credentials.py.j2') | b64encode }}" - environment_sh: "{{ lookup('template', 'environment.sh.j2') | b64encode }}" diff --git a/installer/roles/kubernetes/templates/supervisor.yml.j2 b/installer/roles/kubernetes/templates/supervisor.yml.j2 deleted file mode 100644 index da93f29e5d..0000000000 --- a/installer/roles/kubernetes/templates/supervisor.yml.j2 +++ /dev/null @@ -1,149 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ kubernetes_deployment_name }}-supervisor-config - namespace: {{ kubernetes_namespace }} -data: - supervisor-web-config: | - [supervisord] - nodaemon = True - umask = 022 - logfile = /dev/stdout - logfile_maxbytes = 0 - pidfile = /var/run/supervisor/supervisor.web.pid - - [program:nginx] - command = nginx -g "daemon off;" - autostart = true - autorestart = true - stopwaitsecs = 5 - stdout_logfile=/dev/stdout - stdout_logfile_maxbytes=0 - stderr_logfile=/dev/stderr - stderr_logfile_maxbytes=0 - - [program:uwsgi] - command = {{ uwsgi_bash }} '/var/lib/awx/venv/awx/bin/uwsgi --socket 127.0.0.1:8050 --module=awx.wsgi:application --vacuum --processes=5 --harakiri=120 --no-orphans --master --max-requests=1000 --master-fifo=/var/lib/awx/awxfifo --lazy-apps -b 32768' - directory = /var/lib/awx - autostart = true - autorestart = true - stopwaitsecs = 15 - stopsignal = INT - stdout_logfile=/dev/stdout - stdout_logfile_maxbytes=0 - stderr_logfile=/dev/stderr - stderr_logfile_maxbytes=0 - - [program:daphne] - command = {{ uwsgi_bash }} '/var/lib/awx/venv/awx/bin/daphne -b 127.0.0.1 -p 8051 awx.asgi:channel_layer' - directory = /var/lib/awx - autostart = true - autorestart = true - stopwaitsecs = 5 - stdout_logfile=/dev/stdout - stdout_logfile_maxbytes=0 - stderr_logfile=/dev/stderr - stderr_logfile_maxbytes=0 - - [program:wsbroadcast] - command = awx-manage run_wsbroadcast - directory = /var/lib/awx - autostart = true - autorestart = true - stopwaitsecs = 5 - stdout_logfile=/dev/stdout - stdout_logfile_maxbytes=0 - stderr_logfile=/dev/stderr - stderr_logfile_maxbytes=0 - - [program:awx-rsyslogd] - command = rsyslogd -n -i /var/run/awx-rsyslog/rsyslog.pid -f /var/lib/awx/rsyslog/rsyslog.conf - autostart = true - autorestart = true - stopwaitsecs = 5 - startretries = 10 - stopsignal=TERM - stopasgroup=true - killasgroup=true - redirect_stderr=true - stdout_logfile=/dev/stderr - stdout_logfile_maxbytes=0 - - [group:tower-processes] - programs=nginx,uwsgi,daphne,wsbroadcast,awx-rsyslogd - priority=5 - - # TODO: Exit Handler - - [eventlistener:awx-config-watcher] - command=/usr/bin/config-watcher - stderr_logfile=/dev/stdout - stderr_logfile_maxbytes=0 - stdout_logfile=/dev/stdout - stdout_logfile_maxbytes=0 - events=TICK_60 - priority=0 - - [unix_http_server] - file=/var/run/supervisor/supervisor.web.sock - - [supervisorctl] - serverurl=unix:///var/run/supervisor/supervisor.web.sock ; use a unix:// URL for a unix socket - - [rpcinterface:supervisor] - supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface - - supervisor-task-config: | - [supervisord] - nodaemon = True - umask = 022 - logfile = /dev/stdout - logfile_maxbytes = 0 - pidfile = /var/run/supervisor/supervisor.pid - - [program:dispatcher] - command = awx-manage run_dispatcher - directory = /var/lib/awx - environment = LANGUAGE="en_US.UTF-8",LANG="en_US.UTF-8",LC_ALL="en_US.UTF-8",LC_CTYPE="en_US.UTF-8" - autostart = true - autorestart = true - stopwaitsecs = 5 - stdout_logfile=/dev/stdout - stdout_logfile_maxbytes=0 - stderr_logfile=/dev/stderr - stderr_logfile_maxbytes=0 - - [program:callback-receiver] - command = awx-manage run_callback_receiver - directory = /var/lib/awx - autostart = true - autorestart = true - stopwaitsecs = 5 - stdout_logfile=/dev/stdout - stdout_logfile_maxbytes=0 - stderr_logfile=/dev/stderr - stderr_logfile_maxbytes=0 - - [group:tower-processes] - programs=dispatcher,callback-receiver - priority=5 - - # TODO: Exit Handler - - [eventlistener:awx-config-watcher] - command=/usr/bin/config-watcher - stderr_logfile=/dev/stdout - stderr_logfile_maxbytes=0 - stdout_logfile=/dev/stdout - stdout_logfile_maxbytes=0 - events=TICK_60 - priority=0 - - [unix_http_server] - file=/var/run/supervisor/supervisor.sock - - [supervisorctl] - serverurl=unix:///var/run/supervisor/supervisor.sock ; use a unix:// URL for a unix socket - - [rpcinterface:supervisor] - supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface diff --git a/installer/roles/kubernetes/vars/openshift.yml b/installer/roles/kubernetes/vars/openshift.yml deleted file mode 100644 index 6e1fd30a3c..0000000000 --- a/installer/roles/kubernetes/vars/openshift.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -openshift_oc_config_file: "{{ kubernetes_base_path }}/.kube/config" -openshift_oc_bin: "oc --kubeconfig={{ openshift_oc_config_file }}" From 6df65c95a7034da160c3f95490d53e7076ed3766 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Thu, 4 Mar 2021 18:34:39 -0500 Subject: [PATCH 172/178] Update INSTALL.md --- INSTALL.md | 433 +++++++++-------------------------------------------- 1 file changed, 69 insertions(+), 364 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 1a63cc9b89..85ab2a5cbb 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,410 +1,115 @@ +Table of Contents +================= + + * [Installing AWX](#installing-awx) + * [The AWX Operator](#the-awx-operator) + * [Quickstart with minikube](#quickstart-with-minikube) + * [Starting minikube](#starting-minikube) + * [Deploying the AWX Operator](#deploying-the-awx-operator) + * [Verifying the Operator Deployment](#verifying-the-operator-deployment) + * [Deploy AWX](#deploy-awx) + * [Accessing AWX](#accessing-awx) + * [Installing the AWX CLI](#installing-the-awx-cli) + * [Building the CLI Documentation](#building-the-cli-documentation) + + # Installing AWX -This document provides a guide for installing AWX. +## The AWX Operator -## Table of contents +Starting in version 18.0, the [AWX Operator](https://github.com/ansible/awx-operator) is the preferred way to install AWX. -- [Installing AWX](#installing-awx) - * [Getting started](#getting-started) - + [Clone the repo](#clone-the-repo) - + [AWX branding](#awx-branding) - + [Prerequisites](#prerequisites) - + [System Requirements](#system-requirements) - + [Choose a deployment platform](#choose-a-deployment-platform) - + [Official vs Building Images](#official-vs-building-images) - * [OpenShift](#openshift) - + [Prerequisites](#prerequisites-1) - + [Pre-install steps](#pre-install-steps) - - [Deploying to Minishift](#deploying-to-minishift) - - [PostgreSQL](#postgresql) - + [Run the installer](#run-the-installer) - + [Post-install](#post-install) - + [Accessing AWX](#accessing-awx) - * [Kubernetes](#kubernetes) - + [Prerequisites](#prerequisites-2) - + [Pre-install steps](#pre-install-steps-1) - + [Configuring Helm](#configuring-helm) - + [Run the installer](#run-the-installer-1) - + [Post-install](#post-install-1) - + [Accessing AWX](#accessing-awx-1) - + [SSL Termination](#ssl-termination) -- [Installing the AWX CLI](#installing-the-awx-cli) - * [Building the CLI Documentation](#building-the-cli-documentation) +### Quickstart with minikube +If you don't have an existing OpenShift or Kubernetes cluster, minikube is a fast and easy way to get up and running. -## Getting started +To install minikube, follow the steps in their [documentation](https://minikube.sigs.k8s.io/docs/start/). -### Clone the repo +#### Starting minikube -If you have not already done so, you will need to clone, or create a local copy, of the [AWX repo](https://github.com/ansible/awx). We generally recommend that you view the releases page: - -https://github.com/ansible/awx/releases - -...and clone the latest stable release, e.g., - -`git clone -b x.y.z https://github.com/ansible/awx.git` - -Please note that deploying from `HEAD` (or the latest commit) is **not** stable, and that if you want to do this, you should proceed at your own risk (also, see the section #official-vs-building-images for building your own image). - -For more on how to clone the repo, view [git clone help](https://git-scm.com/docs/git-clone). - -Once you have a local copy, run the commands in the following sections from the root of the project tree. - -### AWX branding - -You can optionally install the AWX branding assets from the [awx-logos repo](https://github.com/ansible/awx-logos). Prior to installing, please review and agree to the [trademark guidelines](https://github.com/ansible/awx-logos/blob/master/TRADEMARKS.md). - -To install the assets, clone the `awx-logos` repo so that it is next to your `awx` clone. As you progress through the installation steps, you'll be setting variables in the [inventory](./installer/inventory) file. To include the assets in the build, set `awx_official=true`. - -### Prerequisites - -Before you can run a deployment, you'll need the following installed in your local environment: - -- [Ansible](http://docs.ansible.com/ansible/latest/intro_installation.html) Requires Version 2.8+ -- [Docker](https://docs.docker.com/engine/installation/) - + A recent version -- [docker](https://pypi.org/project/docker/) Python module - + This is incompatible with `docker-py`. If you have previously installed `docker-py`, please uninstall it. - + We use this module instead of `docker-py` because it is what the `docker-compose` Python module requires. -- [community.general.docker_image collection](https://docs.ansible.com/ansible/latest/collections/community/general/docker_image_module.html) - + This is only required if you are using Ansible >= 2.10 -- [GNU Make](https://www.gnu.org/software/make/) -- [Git](https://git-scm.com/) Requires Version 1.8.4+ -- Python 3.6+ - -### System Requirements - -The system that runs the AWX service will need to satisfy the following requirements - -- At least 4GB of memory -- At least 2 cpu cores -- At least 20GB of space -- Running Docker, Openshift, or Kubernetes -- If you choose to use an external PostgreSQL database, please note that the minimum version is 10+. - -### Choose a deployment platform - -We currently support running AWX as a containerized application using Docker images deployed to either an OpenShift cluster or a Kubernetes cluster. The remainder of this document will walk you through the process of building the images, and deploying them to either platform. - -The [installer](./installer) directory contains an [inventory](./installer/inventory) file, and a playbook, [install.yml](./installer/install.yml). You'll begin by setting variables in the inventory file according to the platform you wish to use, and then you'll start the image build and deployment process by running the playbook. - -In the sections below, you'll find deployment details and instructions for each platform: -- [OpenShift](#openshift) -- [Kubernetes](#kubernetes) - -### Official vs Building Images - -When installing AWX you have the option of building your own image or using the image provided on DockerHub (see [awx](https://hub.docker.com/r/ansible/awx/)) - -This is controlled by the following variables in the `inventory` file +Once you have installed minikube, run the following command to start it. You may wish to customize these options. ``` -dockerhub_base=ansible -dockerhub_version=latest +$ minikube start --cpus=4 --memory=8g --addons=ingress ``` -If these variables are present then all deployments will use these hosted images. If the variables are not present then the images will be built during the install. +#### Deploying the AWX Operator -*dockerhub_base* - -> The base location on DockerHub where the images are hosted (by default this pulls a container image named `ansible/awx:tag`) - -*dockerhub_version* - -> Multiple versions are provided. `latest` always pulls the most recent. You may also select version numbers at different granularities: 1, 1.0, 1.0.1, 1.0.0.123 - -To build your own container use the `build.yml` playbook: +For a comprehensive overview of features, see [README.md](https://github.com/ansible/awx-operator/blob/devel/README.md) in the awx-operator repo. The following steps are the bare minimum to get AWX up and running. ``` -ansible-playbook tools/ansible/build.yml -e awx_version=test-build +$ minikube kubectl -- apply -f https://raw.githubusercontent.com/ansible/awx-operator/devel/deploy/awx-operator.yaml ``` -The resulting image will automatically be pushed to a registry if `docker_registry` is defined. +##### Verifying the Operator Deployment - - -## OpenShift - -### Prerequisites - -To complete a deployment to OpenShift, you will need access to an OpenShift cluster. For demo and testing purposes, you can use [Minishift](https://github.com/minishift/minishift) to create a single node cluster running inside a virtual machine. - -When using OpenShift for deploying AWX make sure you have correct privileges to add the security context 'privileged', otherwise the installation will fail. The privileged context is needed because of the use of [the bubblewrap tool](https://github.com/containers/bubblewrap) to add an additional layer of security when using containers. - -You will also need to have the `oc` command in your PATH. The `install.yml` playbook will call out to `oc` when logging into, and creating objects on the cluster. - -The default resource requests per-deployment requires: - -> Memory: 6GB -> CPU: 3 cores - -This can be tuned by overriding the variables found in [/installer/roles/kubernetes/defaults/main.yml](/installer/roles/kubernetes/defaults/main.yml). Special care should be taken when doing this as undersized instances will experience crashes and resource exhaustion. - -For more detail on how resource requests are formed see: [https://docs.openshift.com/container-platform/latest/dev_guide/compute_resources.html#dev-compute-resources](https://docs.openshift.com/container-platform/latest/dev_guide/compute_resources.html#dev-compute-resources) - -### Pre-install steps - -Before starting the install, review the [inventory](./installer/inventory) file, and uncomment and provide values for the following variables found in the `[all:vars]` section: - -*openshift_host* - -> IP address or hostname of the OpenShift cluster. If you're using Minishift, this will be the value returned by `minishift ip`. - - -*openshift_skip_tls_verify* - -> Boolean. Set to True if using self-signed certs. - -*openshift_project* - -> Name of the OpenShift project that will be created, and used as the namespace for the AWX app. Defaults to *awx*. - -*openshift_user* - -> Username of the OpenShift user that will create the project, and deploy the application. Defaults to *developer*. - -*openshift_pg_emptydir* - -> Boolean. Set to True to use an emptyDir volume when deploying the PostgreSQL pod. Note: This should only be used for demo and testing purposes. - -*docker_registry* - -> IP address and port, or URL, for accessing a registry that the OpenShift cluster can access. Defaults to *172.30.1.1:5000*, the internal registry delivered with Minishift. This is not needed if you are using official hosted images. - -*docker_registry_repository* - -> Namespace to use when pushing and pulling images to and from the registry. Generally this will match the project name. It defaults to *awx*. This is not needed if you are using official hosted images. - -*docker_registry_username* - -> Username of the user that will push images to the registry. Will generally match the *openshift_user* value. Defaults to *developer*. This is not needed if you are using official hosted images. - -#### Deploying to Minishift - -Install Minishift by following the [installation guide](https://docs.openshift.org/latest/minishift/getting-started/installing.html). - -The recommended minimum resources for your Minishift VM: - -```bash -$ minishift start --cpus=4 --memory=8GB -``` - -The Minishift VM contains a Docker daemon, which you can use to build the AWX images. This is generally the approach you should take, and we recommend doing so. To use this instance, run the following command to setup your environment: - -```bash -# Set DOCKER environment variable to point to the Minishift VM -$ eval $(minishift docker-env) -``` - -**Note** - -> If you choose to not use the Docker instance running inside the VM, and build the images externally, you will have to enable the OpenShift cluster to access the images. This involves pushing the images to an external Docker registry, and granting the cluster access to it, or exposing the internal registry, and pushing the images into it. - -#### PostgreSQL - -By default, AWX will deploy a PostgreSQL pod inside of your cluster. You will need to create a [Persistent Volume Claim](https://docs.openshift.org/latest/dev_guide/persistent_volumes.html) which is named `postgresql` by default, and can be overridden by setting the `openshift_pg_pvc_name` variable. For testing and demo purposes, you may set `openshift_pg_emptydir=yes`. - -If you wish to use an external database, in the inventory file, set the value of `pg_hostname`, and update `pg_username`, `pg_password`, `pg_admin_password`, `pg_database`, and `pg_port` with the connection information. When setting `pg_hostname` the installer will assume you have configured the database in that location and will not launch the postgresql pod. - -### Run the installer - -To start the install, you will pass two *extra* variables on the command line. The first is *openshift_password*, which is the password for the *openshift_user*, and the second is *docker_registry_password*, which is the password associated with *docker_registry_username*. - -If you're using the OpenShift internal registry, then you'll pass an access token for the *docker_registry_password* value, rather than a password. The `oc whoami -t` command will generate the required token, as long as you're logged into the cluster via `oc cluster login`. - -Run the following command (docker_registry_password is optional if using official images): - -```bash -# Start the install -$ ansible-playbook -i inventory install.yml -e openshift_password=developer -e docker_registry_password=$(oc whoami -t) -``` - -### Post-install - -After the playbook run completes, check the status of the deployment by running `oc get pods`: - -```bash -# View the running pods -$ oc get pods - -NAME READY STATUS RESTARTS AGE -awx-3886581826-5mv0l 4/4 Running 0 8s -postgresql-1-l85fh 1/1 Running 0 20m +After a few seconds, the operator should be up and running. Verify it by running the following command: ``` - -In the above example, the name of the AWX pod is `awx-3886581826-5mv0l`. Before accessing the AWX web interface, setup tasks and database migrations need to complete. These tasks are running in the `awx_task` container inside the AWX pod. To monitor their status, tail the container's STDOUT by running the following command, replacing the AWX pod name with the pod name from your environment: - -```bash -# Follow the awx_task log output -$ oc logs -f awx-3886581826-5mv0l -c awx-celery +$ minikube kubectl get pods +NAME READY STATUS RESTARTS AGE +awx-operator-7c78bfbfd-xb6th 1/1 Running 0 11s ``` -You will see the following indicating that database migrations are running: +#### Deploy AWX -```bash -Using /etc/ansible/ansible.cfg as config file -127.0.0.1 | SUCCESS => { - "changed": false, - "db": "awx" -} -Operations to perform: - Synchronize unmigrated apps: solo, api, staticfiles, messages, channels, django_extensions, ui, rest_framework, polymorphic - Apply all migrations: sso, taggit, sessions, sites, kombu_transport_django, social_auth, contenttypes, auth, conf, main -Synchronizing apps without migrations: - Creating tables... - Running deferred SQL... - Installing custom SQL... -Running migrations: - Rendering model states... DONE - Applying contenttypes.0001_initial... OK - Applying contenttypes.0002_remove_content_type_name... OK - Applying auth.0001_initial... OK - Applying auth.0002_alter_permission_name_max_length... OK - Applying auth.0003_alter_user_email_max_length... OK - Applying auth.0004_alter_user_username_opts... OK - Applying auth.0005_alter_user_last_login_null... OK - Applying auth.0006_require_contenttypes_0002... OK - Applying taggit.0001_initial... OK - Applying taggit.0002_auto_20150616_2121... OK - ... +Once the Operator is running, you can now deploy AWX by creating a simple YAML file: + +``` +$ cat myawx.yml +--- +apiVersion: awx.ansible.com/v1beta1 +kind: AWX +metadata: + name: awx +spec: + tower_ingress_type: Ingress ``` -When you see output similar to the following, you'll know that database migrations have completed, and you can access the web interface: +And then creating the AWX object in the Kubernetes API: -```bash -Python 2.7.5 (default, Nov 6 2016, 00:28:07) -[GCC 4.8.5 20150623 (Red Hat 4.8.5-11)] on linux2 -Type "help", "copyright", "credits" or "license" for more information. -(InteractiveConsole) - ->>> ->>> Default organization added. -Demo Credential, Inventory, and Job Template added. -Successfully registered instance awx-3886581826-5mv0l -(changed: True) -Creating instance group tower -Added instance awx-3886581826-5mv0l to tower +``` +$ minikube kubectl apply -- -f myawx.yml +awx.awx.ansible.com/awx created ``` -Once database migrations complete, the web interface will be accessible. +After creating the AWX object in the Kubernetes API, the operator will begin running its reconciliation loop. -### Accessing AWX +To see what's going on, you can tail the logs of the operator pod (note that your pod name will be different): -The AWX web interface is running in the AWX pod, behind the `awx-web-svc` service. To view the service, and its port value, run the following command: - -```bash -# View available services -$ oc get services - -NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE -awx-web-svc 172.30.111.74 8052:30083/TCP 37m -postgresql 172.30.102.9 5432/TCP 38m +``` +$ minikube kubectl logs -- -f awx-operator-7c78bfbfd-xb6th ``` -The deployment process creates a route, `awx-web-svc`, to expose the service. How the ingres is actually created will vary depending on your environment, and how the cluster is configured. You can view the route, and the external IP address and hostname assigned to it, by running the following command: +After a few seconds, you will see the database and application pods show up. On a fresh system, it may take a few minutes for the container images to download. -```bash -# View available routes -$ oc get routes - -NAME HOST/PORT PATH SERVICES PORT TERMINATION WILDCARD -awx-web-svc awx-web-svc-awx.192.168.64.2.nip.io awx-web-svc http edge/Allow None +``` +$ minikube kubectl get pods +NAME READY STATUS RESTARTS AGE +awx-5ffbfd489c-bvtvf 3/3 Running 0 2m54s +awx-operator-7c78bfbfd-xb6th 1/1 Running 0 6m42s +awx-postgres-0 1/1 Running 0 2m58s ``` -The above example is taken from a Minishift instance. From a web browser, use `https` to access the `HOST/PORT` value from your environment. Using the above example, the URL to access the server would be [https://awx-web-svc-awx.192.168.64.2.nip.io](https://awx-web-svc-awx.192.168.64.2.nip.io). +##### Accessing AWX -Once you access the AWX server, you will be prompted with a login dialog. The default administrator username is `admin`, and the password is `password`. +To access the AWX UI, you'll need to grab the service url from minikube: -## Kubernetes - -### Prerequisites - -A Kubernetes deployment will require you to have access to a Kubernetes cluster as well as the following tools: - -- [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) -- [helm](https://helm.sh/docs/intro/quickstart/) - -The installation program will reference `kubectl` directly. `helm` is only necessary if you are letting the installer configure PostgreSQL for you. - -The default resource requests per-pod requires: - -> Memory: 6GB -> CPU: 3 cores - -This can be tuned by overriding the variables found in [/installer/roles/kubernetes/defaults/main.yml](/installer/roles/kubernetes/defaults/main.yml). Special care should be taken when doing this as undersized instances will experience crashes and resource exhaustion. - -For more detail on how resource requests are formed see: [https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) - -### Pre-install steps - -Before starting the install process, review the [inventory](./installer/inventory) file, and uncomment and provide values for the following variables found in the `[all:vars]` section uncommenting when necessary. Make sure the openshift and standalone docker sections are commented out: - -*kubernetes_context* - -> Prior to running the installer, make sure you've configured the context for the cluster you'll be installing to. This is how the installer knows which cluster to connect to and what authentication to use - -*kubernetes_namespace* - -> Name of the Kubernetes namespace where the AWX resources will be installed. This will be created if it doesn't exist - -*docker_registry_* - -> These settings should be used if building your own base images. You'll need access to an external registry and are responsible for making sure your kube cluster can talk to it and use it. If these are undefined and the dockerhub_ configuration settings are uncommented then the images will be pulled from dockerhub instead - -### Configuring Helm - -If you want the AWX installer to manage creating the database pod (rather than installing and configuring postgres on your own). Then you will need to have a working `helm` installation, you can find details here: [https://helm.sh/docs/intro/quickstart/](https://helm.sh/docs/intro/quickstart/). - -You do not need to create a [Persistent Volume Claim](https://docs.openshift.org/latest/dev_guide/persistent_volumes.html) as Helm does it for you. However, an existing one may be used by setting the `pg_persistence_existingclaim` variable. - -Newer Kubernetes clusters with RBAC enabled will need to make sure a service account is created, make sure to follow the instructions here [https://helm.sh/docs/topics/rbac/](https://helm.sh/docs/topics/rbac/) - -### Run the installer - -After making changes to the `inventory` file use `ansible-playbook` to begin the install - -```bash -$ ansible-playbook -i inventory install.yml +``` +$ minikube service awx-service --url +http://192.168.59.2:31868 ``` -### Post-install +On fresh installs, you will see the "AWX is currently upgrading." page until database migrations finish. -After the playbook run completes, check the status of the deployment by running `kubectl get pods --namespace awx` (replace awx with the namespace you used): +Once you are redirected to the login screen, you can now log in by obtaining the generated admin password (note: do not copy the trailing `%`): -```bash -# View the running pods, it may take a few minutes for everything to be marked in the Running state -$ kubectl get pods --namespace awx -NAME READY STATUS RESTARTS AGE -awx-2558692395-2r8ss 4/4 Running 0 29s -awx-postgresql-355348841-kltkn 1/1 Running 0 1m +``` +$ minikube kubectl -- get secret awx-admin-password -o jsonpath='{.data.password}' | base64 --decode +b6ChwVmqEiAsil2KSpH4xGaZPeZvWnWj% ``` -### Accessing AWX - -The AWX web interface is running in the AWX pod behind the `awx-web-svc` service: - -```bash -# View available services -$ kubectl get svc --namespace awx -NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE -awx-postgresql ClusterIP 10.7.250.208 5432/TCP 2m -awx-web-svc NodePort 10.7.241.35 80:30177/TCP 1m -``` - -The deployment process creates an `Ingress` named `awx-web-svc` also. Some kubernetes cloud providers will automatically handle routing configuration when an Ingress is created others may require that you more explicitly configure it. You can see what kubernetes knows about things with: - -```bash - kubectl get ing --namespace awx -NAME HOSTS ADDRESS PORTS AGE -awx-web-svc * 35.227.x.y 80 3m -``` - -If your provider is able to allocate an IP Address from the Ingress controller then you can navigate to the address and access the AWX interface. For some providers it can take a few minutes to allocate and make this accessible. For other providers it may require you to manually intervene. - -### SSL Termination - -Unlike Openshift's `Route` the Kubernetes `Ingress` doesn't yet handle SSL termination. As such the default configuration will only expose AWX through HTTP on port 80. You are responsible for configuring SSL support until support is added (either to Kubernetes or AWX itself). +Now you can log in at the URL above with the username "admin" and the password above. Happy Automating! # Installing the AWX CLI From 80c2249bdb15d6baddfa47b0097b9897f73ead44 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Fri, 5 Mar 2021 07:58:58 -0500 Subject: [PATCH 173/178] default cluster node count env var to 1 --- Makefile | 2 +- tools/docker-compose/ansible/roles/sources/defaults/main.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index a06c32c2de..71375bf1dc 100644 --- a/Makefile +++ b/Makefile @@ -534,7 +534,7 @@ awx/projects: @mkdir -p $@ COMPOSE_UP_OPTS ?= -CLUSER_NODE_COUNT ?= 1 +CLUSTER_NODE_COUNT ?= 1 docker-compose-sources: ansible-playbook -i tools/docker-compose/inventory tools/docker-compose/ansible/sources.yml \ diff --git a/tools/docker-compose/ansible/roles/sources/defaults/main.yml b/tools/docker-compose/ansible/roles/sources/defaults/main.yml index cff661deb8..ed0cfee862 100644 --- a/tools/docker-compose/ansible/roles/sources/defaults/main.yml +++ b/tools/docker-compose/ansible/roles/sources/defaults/main.yml @@ -5,4 +5,4 @@ awx_image: 'quay.io/ansible/awx_devel' pg_port: 5432 pg_username: 'awx' pg_database: 'awx' -cluster_node_count: "{{ lookup('env', 'CLUSTER_COUNT') | default(1, True) }}" +cluster_node_count: 1 From d494645914e4e3e7036a5578013e3b2e0346c1c7 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Fri, 5 Mar 2021 08:14:22 -0500 Subject: [PATCH 174/178] Add i18n config file to frontend container image --- awx/ui_next/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/ui_next/Dockerfile b/awx/ui_next/Dockerfile index a710a820cd..1d35f9221d 100644 --- a/awx/ui_next/Dockerfile +++ b/awx/ui_next/Dockerfile @@ -8,6 +8,7 @@ WORKDIR /ui_next ADD public public ADD package.json package.json ADD package-lock.json package-lock.json +ADD .linguirc .linguirc COPY ${NPMRC_FILE} .npmrc RUN npm install ADD src src From 53d8e8b332a9dc23e183d002c9176d4d22c49e0c Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Fri, 5 Mar 2021 11:26:32 -0500 Subject: [PATCH 175/178] point people at install instructions for older stable releases --- INSTALL.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/INSTALL.md b/INSTALL.md index 85ab2a5cbb..590ddb4d98 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -15,6 +15,11 @@ Table of Contents # Installing AWX +:warning: NOTE | +--- | +If you're installing an older release of AWX (prior to 18.0), these instructions have changed. Take a look at your version specific instructions, e.g., for AWX 17.0.1, see: [https://github.com/ansible/awx/blob/17.0.1/INSTALL.md](https://github.com/ansible/awx/blob/17.0.1/INSTALL.md) +If you're attempting to migrate an older Docker-based AWX installation, see: [Migrating Data from Local Docker](https://github.com/ansible/awx/blob/devel/tools/docker-compose/docs/data_migration.md) | + ## The AWX Operator Starting in version 18.0, the [AWX Operator](https://github.com/ansible/awx-operator) is the preferred way to install AWX. From 4c60999161a11f4c258e12596e4bfcee78ef5e12 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Fri, 5 Mar 2021 14:11:30 -0500 Subject: [PATCH 176/178] remove custom_virtualenv support from the AWX collection and docs --- awx/api/serializers.py | 4 + .../tests/functional/api/test_job_template.py | 59 ------ .../functional/api/test_organizations.py | 31 ---- awx/main/tests/functional/api/test_project.py | 30 ---- .../plugins/modules/tower_inventory_source.py | 7 +- .../plugins/modules/tower_job_template.py | 8 +- .../plugins/modules/tower_organization.py | 10 -- .../plugins/modules/tower_project.py | 9 - .../test/awx/test_inventory_source.py | 55 ------ awx_collection/test/awx/test_organization.py | 22 --- .../targets/tower_import/tasks/main.yml | 3 - .../targets/tower_organization/tasks/main.yml | 13 -- awx_collection/tools/vars/examples.yml | 1 - docs/custom_virtualenvs.md | 168 ------------------ 14 files changed, 6 insertions(+), 414 deletions(-) delete mode 100644 docs/custom_virtualenvs.md diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 6f8dcd3fad..8246ad8f1d 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1253,6 +1253,7 @@ class OrganizationSerializer(BaseSerializer): class Meta: model = Organization fields = ('*', 'max_hosts', 'custom_virtualenv', 'default_environment',) + read_only_fields = ('*', 'custom_virtualenv',) def get_related(self, obj): res = super(OrganizationSerializer, self).get_related(obj) @@ -1399,6 +1400,7 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer): fields = ('*', '-execution_environment', 'organization', 'scm_update_on_launch', 'scm_update_cache_timeout', 'allow_override', 'custom_virtualenv', 'default_environment') + \ ('last_update_failed', 'last_updated') # Backwards compatibility + read_only_fields = ('*', 'custom_virtualenv',) def get_related(self, obj): res = super(ProjectSerializer, self).get_related(obj) @@ -1978,6 +1980,7 @@ class InventorySourceOptionsSerializer(BaseSerializer): fields = ('*', 'source', 'source_path', 'source_script', 'source_vars', 'credential', 'enabled_var', 'enabled_value', 'host_filter', 'overwrite', 'overwrite_vars', 'custom_virtualenv', 'timeout', 'verbosity') + read_only_fields = ('*', 'custom_virtualenv',) def get_related(self, obj): res = super(InventorySourceOptionsSerializer, self).get_related(obj) @@ -2963,6 +2966,7 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO 'become_enabled', 'diff_mode', 'allow_simultaneous', 'custom_virtualenv', 'job_slice_count', 'webhook_service', 'webhook_credential', ) + read_only_fields = ('*', 'custom_virtualenv',) def get_related(self, obj): res = super(JobTemplateSerializer, self).get_related(obj) diff --git a/awx/main/tests/functional/api/test_job_template.py b/awx/main/tests/functional/api/test_job_template.py index 1883f6e6ec..2eeefa7eed 100644 --- a/awx/main/tests/functional/api/test_job_template.py +++ b/awx/main/tests/functional/api/test_job_template.py @@ -1,6 +1,3 @@ -import os - -from backports.tempfile import TemporaryDirectory import pytest # AWX @@ -10,7 +7,6 @@ from awx.main.models import Job, JobTemplate, CredentialType, WorkflowJobTemplat from awx.main.migrations import _save_password_keys as save_password_keys # Django -from django.conf import settings from django.apps import apps # DRF @@ -302,61 +298,6 @@ def test_save_survey_passwords_on_migration(job_template_with_survey_passwords): assert job.survey_passwords == {'SSN': '$encrypted$', 'secret_key': '$encrypted$'} -@pytest.mark.django_db -@pytest.mark.parametrize('access', ["superuser", "admin", "peon"]) -def test_job_template_custom_virtualenv(get, patch, organization_factory, job_template_factory, alice, access): - objs = organization_factory("org", superusers=['admin']) - jt = job_template_factory("jt", organization=objs.organization, - inventory='test_inv', project='test_proj').job_template - - user = alice - if access == "superuser": - user = objs.superusers.admin - elif access == "admin": - jt.admin_role.members.add(alice) - else: - jt.read_role.members.add(alice) - - with TemporaryDirectory(dir=settings.BASE_VENV_PATH) as temp_dir: - os.makedirs(os.path.join(temp_dir, 'bin', 'activate')) - url = reverse('api:job_template_detail', kwargs={'pk': jt.id}) - - if access == "peon": - patch(url, {'custom_virtualenv': temp_dir}, user=user, expect=403) - assert 'custom_virtualenv' not in get(url, user=user) - assert JobTemplate.objects.get(pk=jt.id).custom_virtualenv is None - else: - patch(url, {'custom_virtualenv': temp_dir}, user=user, expect=200) - assert get(url, user=user).data['custom_virtualenv'] == os.path.join(temp_dir, '') - - -@pytest.mark.django_db -def test_job_template_invalid_custom_virtualenv(get, patch, organization_factory, - job_template_factory): - objs = organization_factory("org", superusers=['admin']) - jt = job_template_factory("jt", organization=objs.organization, - inventory='test_inv', project='test_proj').job_template - - url = reverse('api:job_template_detail', kwargs={'pk': jt.id}) - resp = patch(url, {'custom_virtualenv': '/foo/bar'}, user=objs.superusers.admin, expect=400) - assert resp.data['custom_virtualenv'] == [ - '/foo/bar is not a valid virtualenv in {}'.format(settings.BASE_VENV_PATH) - ] - - -@pytest.mark.django_db -@pytest.mark.parametrize('value', ["", None]) -def test_job_template_unset_custom_virtualenv(get, patch, organization_factory, - job_template_factory, value): - objs = organization_factory("org", superusers=['admin']) - jt = job_template_factory("jt", organization=objs.organization, - inventory='test_inv', project='test_proj').job_template - - url = reverse('api:job_template_detail', kwargs={'pk': jt.id}) - resp = patch(url, {'custom_virtualenv': value}, user=objs.superusers.admin, expect=200) - assert resp.data['custom_virtualenv'] is None - - @pytest.mark.django_db def test_jt_organization_follows_project(post, patch, admin_user): org1 = Organization.objects.create(name='foo1') diff --git a/awx/main/tests/functional/api/test_organizations.py b/awx/main/tests/functional/api/test_organizations.py index 6c45c0c681..93994d3842 100644 --- a/awx/main/tests/functional/api/test_organizations.py +++ b/awx/main/tests/functional/api/test_organizations.py @@ -1,11 +1,6 @@ # Copyright (c) 2015 Ansible, Inc. # All Rights Reserved. -# Python -import os - -from backports.tempfile import TemporaryDirectory -from django.conf import settings import pytest # AWX @@ -242,32 +237,6 @@ def test_delete_organization_xfail2(delete, organization): delete(reverse('api:organization_detail', kwargs={'pk': organization.id}), user=None, expect=401) -@pytest.mark.django_db -def test_organization_custom_virtualenv(get, patch, organization, admin): - with TemporaryDirectory(dir=settings.BASE_VENV_PATH) as temp_dir: - os.makedirs(os.path.join(temp_dir, 'bin', 'activate')) - url = reverse('api:organization_detail', kwargs={'pk': organization.id}) - patch(url, {'custom_virtualenv': temp_dir}, user=admin, expect=200) - assert get(url, user=admin).data['custom_virtualenv'] == os.path.join(temp_dir, '') - - -@pytest.mark.django_db -def test_organization_invalid_custom_virtualenv(get, patch, organization, admin): - url = reverse('api:organization_detail', kwargs={'pk': organization.id}) - resp = patch(url, {'custom_virtualenv': '/foo/bar'}, user=admin, expect=400) - assert resp.data['custom_virtualenv'] == [ - '/foo/bar is not a valid virtualenv in {}'.format(settings.BASE_VENV_PATH) - ] - - -@pytest.mark.django_db -@pytest.mark.parametrize('value', ["", None]) -def test_organization_unset_custom_virtualenv(get, patch, organization, admin, value): - url = reverse('api:organization_detail', kwargs={'pk': organization.id}) - resp = patch(url, {'custom_virtualenv': value}, user=admin, expect=200) - assert resp.data['custom_virtualenv'] is None - - @pytest.mark.django_db def test_organization_delete(delete, admin, organization, organization_jobs_successful): url = reverse('api:organization_detail', kwargs={'pk': organization.id}) diff --git a/awx/main/tests/functional/api/test_project.py b/awx/main/tests/functional/api/test_project.py index a31eb0804a..71d685ce7b 100644 --- a/awx/main/tests/functional/api/test_project.py +++ b/awx/main/tests/functional/api/test_project.py @@ -1,7 +1,3 @@ -import os - -from backports.tempfile import TemporaryDirectory -from django.conf import settings import pytest from awx.api.versioning import reverse @@ -21,32 +17,6 @@ class TestInsightsCredential: expect=400) -@pytest.mark.django_db -def test_project_custom_virtualenv(get, patch, project, admin): - with TemporaryDirectory(dir=settings.BASE_VENV_PATH) as temp_dir: - os.makedirs(os.path.join(temp_dir, 'bin', 'activate')) - url = reverse('api:project_detail', kwargs={'pk': project.id}) - patch(url, {'custom_virtualenv': temp_dir}, user=admin, expect=200) - assert get(url, user=admin).data['custom_virtualenv'] == os.path.join(temp_dir, '') - - -@pytest.mark.django_db -def test_project_invalid_custom_virtualenv(get, patch, project, admin): - url = reverse('api:project_detail', kwargs={'pk': project.id}) - resp = patch(url, {'custom_virtualenv': '/foo/bar'}, user=admin, expect=400) - assert resp.data['custom_virtualenv'] == [ - '/foo/bar is not a valid virtualenv in {}'.format(settings.BASE_VENV_PATH) - ] - - -@pytest.mark.django_db -@pytest.mark.parametrize('value', ["", None]) -def test_project_unset_custom_virtualenv(get, patch, project, admin, value): - url = reverse('api:project_detail', kwargs={'pk': project.id}) - resp = patch(url, {'custom_virtualenv': value}, user=admin, expect=200) - assert resp.data['custom_virtualenv'] is None - - @pytest.mark.django_db def test_no_changing_overwrite_behavior_if_used(post, patch, organization, admin_user): r1 = post( diff --git a/awx_collection/plugins/modules/tower_inventory_source.py b/awx_collection/plugins/modules/tower_inventory_source.py index 9edf467617..0bde4d7af3 100644 --- a/awx_collection/plugins/modules/tower_inventory_source.py +++ b/awx_collection/plugins/modules/tower_inventory_source.py @@ -85,10 +85,6 @@ options: description: - Override vars in child groups and hosts with those from external source. type: bool - custom_virtualenv: - description: - - Local absolute file path containing a custom Python virtualenv to use. - type: str timeout: description: The amount of time (in seconds) to run before the task is canceled. type: int @@ -181,7 +177,6 @@ def main(): organization=dict(), overwrite=dict(type='bool'), overwrite_vars=dict(type='bool'), - custom_virtualenv=dict(), timeout=dict(type='int'), verbosity=dict(type='int', choices=[0, 1, 2]), update_on_launch=dict(type='bool'), @@ -265,7 +260,7 @@ def main(): OPTIONAL_VARS = ( 'description', 'source', 'source_path', 'source_vars', - 'overwrite', 'overwrite_vars', 'custom_virtualenv', + 'overwrite', 'overwrite_vars', 'timeout', 'verbosity', 'update_on_launch', 'update_cache_timeout', 'update_on_project_update', 'enabled_var', 'enabled_value', 'host_filter', ) diff --git a/awx_collection/plugins/modules/tower_job_template.py b/awx_collection/plugins/modules/tower_job_template.py index 1ed750b86e..bb52b4d4ee 100644 --- a/awx_collection/plugins/modules/tower_job_template.py +++ b/awx_collection/plugins/modules/tower_job_template.py @@ -227,10 +227,6 @@ options: description: - Maximum time in seconds to wait for a job to finish (server-side). type: int - custom_virtualenv: - description: - - Local absolute file path containing a custom Python virtualenv to use. - type: str job_slice_count: description: - The number of jobs to slice into at runtime. Will cause the Job Template to launch a workflow if value is greater than 1. @@ -304,7 +300,6 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" survey_enabled: yes survey_spec: "{{ lookup('file', 'my_survey.json') }}" - custom_virtualenv: "/var/lib/awx/venv/custom-venv/" - name: Add start notification to Job Template tower_job_template: @@ -352,7 +347,6 @@ def main(): playbook=dict(), credential=dict(), vault_credential=dict(), - custom_virtualenv=dict(), credentials=dict(type='list', elements='str'), execution_environment=dict(), forks=dict(type='int'), @@ -442,7 +436,7 @@ def main(): 'host_config_key', 'ask_scm_branch_on_launch', 'ask_diff_mode_on_launch', 'ask_variables_on_launch', 'ask_limit_on_launch', 'ask_tags_on_launch', 'ask_skip_tags_on_launch', 'ask_job_type_on_launch', 'ask_verbosity_on_launch', 'ask_inventory_on_launch', 'ask_credential_on_launch', 'survey_enabled', - 'become_enabled', 'diff_mode', 'allow_simultaneous', 'custom_virtualenv', 'job_slice_count', 'webhook_service', + 'become_enabled', 'diff_mode', 'allow_simultaneous', 'job_slice_count', 'webhook_service', ): field_val = module.params.get(field_name) if field_val is not None: diff --git a/awx_collection/plugins/modules/tower_organization.py b/awx_collection/plugins/modules/tower_organization.py index bcf6060ea6..197099d391 100644 --- a/awx_collection/plugins/modules/tower_organization.py +++ b/awx_collection/plugins/modules/tower_organization.py @@ -31,11 +31,6 @@ options: description: - The description to use for the organization. type: str - custom_virtualenv: - description: - - Local absolute file path containing a custom Python virtualenv to use. - type: str - default: '' default_environment: description: - Default Execution Environment to use for jobs owned by the Organization. @@ -92,7 +87,6 @@ EXAMPLES = ''' tower_organization: name: "Foo" description: "Foo bar organization using foo-venv" - custom_virtualenv: "/var/lib/awx/venv/foo-venv/" state: present tower_config_file: "~/tower_cli.cfg" @@ -113,7 +107,6 @@ def main(): argument_spec = dict( name=dict(required=True), description=dict(), - custom_virtualenv=dict(), default_environment=dict(), max_hosts=dict(type='int', default="0"), notification_templates_started=dict(type="list", elements='str'), @@ -130,7 +123,6 @@ def main(): # Extract our parameters name = module.params.get('name') description = module.params.get('description') - custom_virtualenv = module.params.get('custom_virtualenv') default_ee = module.params.get('default_environment') max_hosts = module.params.get('max_hosts') # instance_group_names = module.params.get('instance_groups') @@ -179,8 +171,6 @@ def main(): org_fields = {'name': module.get_item_name(organization) if organization else name} if description is not None: org_fields['description'] = description - if custom_virtualenv is not None: - org_fields['custom_virtualenv'] = custom_virtualenv if default_ee is not None: org_fields['default_environment'] = module.resolve_name_to_id('execution_environments', default_ee) if max_hosts is not None: diff --git a/awx_collection/plugins/modules/tower_project.py b/awx_collection/plugins/modules/tower_project.py index acbf3833b5..42264996cb 100644 --- a/awx_collection/plugins/modules/tower_project.py +++ b/awx_collection/plugins/modules/tower_project.py @@ -97,11 +97,6 @@ options: type: int aliases: - job_timeout - custom_virtualenv: - description: - - Local absolute file path containing a custom Python virtualenv to use - type: str - default: '' default_environment: description: - Default Execution Environment to use for jobs relating to the project. @@ -172,7 +167,6 @@ EXAMPLES = ''' organization: "test" scm_update_on_launch: True scm_update_cache_timeout: 60 - custom_virtualenv: "/var/lib/awx/var/lib/awx/venv/ansible-2.2" state: present tower_config_file: "~/tower_cli.cfg" ''' @@ -242,7 +236,6 @@ def main(): scm_update_cache_timeout=dict(type='int', default=0), allow_override=dict(type='bool', aliases=['scm_allow_override']), timeout=dict(type='int', default=0, aliases=['job_timeout']), - custom_virtualenv=dict(), default_environment=dict(), organization=dict(), notification_templates_started=dict(type="list", elements='str'), @@ -274,7 +267,6 @@ def main(): scm_update_cache_timeout = module.params.get('scm_update_cache_timeout') allow_override = module.params.get('allow_override') timeout = module.params.get('timeout') - custom_virtualenv = module.params.get('custom_virtualenv') default_ee = module.params.get('default_environment') organization = module.params.get('organization') state = module.params.get('state') @@ -333,7 +325,6 @@ def main(): 'organization': org_id, 'scm_update_on_launch': scm_update_on_launch, 'scm_update_cache_timeout': scm_update_cache_timeout, - 'custom_virtualenv': custom_virtualenv, } if description is not None: project_fields['description'] = description diff --git a/awx_collection/test/awx/test_inventory_source.py b/awx_collection/test/awx/test_inventory_source.py index fa01b16ddb..fabd3d9fb8 100644 --- a/awx_collection/test/awx/test_inventory_source.py +++ b/awx_collection/test/awx/test_inventory_source.py @@ -97,61 +97,6 @@ def test_create_inventory_source_multiple_orgs(run_module, admin_user): } -@pytest.mark.django_db -def test_create_inventory_source_with_venv(run_module, admin_user, base_inventory, mocker, project): - path = '/var/lib/awx/venv/custom-venv/foobar13489435/' - source_path = '/var/lib/awx/example_source_path/' - with mocker.patch('awx.main.models.mixins.get_custom_venv_choices', return_value=[path]): - result = run_module('tower_inventory_source', dict( - name='foo', - inventory=base_inventory.name, - state='present', - source='scm', - source_project=project.name, - custom_virtualenv=path, - source_path=source_path - ), admin_user) - assert result.pop('changed'), result - - inv_src = InventorySource.objects.get(name='foo') - assert inv_src.inventory == base_inventory - result.pop('invocation') - - assert inv_src.custom_virtualenv == path - - -@pytest.mark.django_db -def test_custom_venv_no_op(run_module, admin_user, base_inventory, mocker, project): - """If the inventory source is modified, then it should not blank fields - unrelated to the params that the user passed. - This enforces assumptions about the behavior of the AnsibleModule - default argument_spec behavior. - """ - source_path = '/var/lib/awx/example_source_path/' - inv_src = InventorySource.objects.create( - name='foo', - inventory=base_inventory, - source_project=project, - source='scm', - custom_virtualenv='/var/lib/awx/venv/foobar/' - ) - # mock needed due to API behavior, not incorrect client behavior - with mocker.patch('awx.main.models.mixins.get_custom_venv_choices', return_value=['/var/lib/awx/venv/foobar/']): - result = run_module('tower_inventory_source', dict( - name='foo', - description='this is the changed description', - inventory=base_inventory.name, - source='scm', # is required, but behavior is arguable - state='present', - source_project=project.name, - source_path=source_path - ), admin_user) - assert result.pop('changed', None), result - inv_src.refresh_from_db() - assert inv_src.custom_virtualenv == '/var/lib/awx/venv/foobar/' - assert inv_src.description == 'this is the changed description' - - @pytest.mark.django_db def test_falsy_value(run_module, admin_user, base_inventory): result = run_module('tower_inventory_source', dict( diff --git a/awx_collection/test/awx/test_organization.py b/awx_collection/test/awx/test_organization.py index 8f4872c303..ee58ab3a2c 100644 --- a/awx_collection/test/awx/test_organization.py +++ b/awx_collection/test/awx/test_organization.py @@ -20,7 +20,6 @@ def test_create_organization(run_module, admin_user): 'validate_certs': None, 'tower_oauthtoken': None, 'tower_config_file': None, - 'custom_virtualenv': None } result = run_module('tower_organization', module_args, admin_user) @@ -37,24 +36,3 @@ def test_create_organization(run_module, admin_user): } assert org.description == 'barfoo' - - -@pytest.mark.django_db -def test_create_organization_with_venv(run_module, admin_user, mocker): - path = '/var/lib/awx/venv/custom-venv/foobar13489435/' - with mocker.patch('awx.main.models.mixins.get_custom_venv_choices', return_value=[path]): - result = run_module('tower_organization', { - 'name': 'foo', - 'custom_virtualenv': path, - 'state': 'present' - }, admin_user) - assert result.pop('changed'), result - - org = Organization.objects.get(name='foo') - result.pop('invocation') - assert result == { - "name": "foo", - "id": org.id - } - - assert org.custom_virtualenv == path diff --git a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml index dbfaf5c06a..5c04958ac7 100644 --- a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml @@ -17,7 +17,6 @@ - name: "{{ org_name1 }}" description: "" max_hosts: 0 - custom_virtualenv: null related: notification_templates: [] notification_templates_started: [] @@ -40,7 +39,6 @@ - name: "{{ org_name1 }}" description: "" max_hosts: 0 - custom_virtualenv: null related: notification_templates: [] notification_templates_started: [] @@ -67,7 +65,6 @@ "name": "{{ org_name2 }}", "description": "", "max_hosts": 0, - "custom_virtualenv": null, "related": { "notification_templates": [], "notification_templates_started": [], diff --git a/awx_collection/tests/integration/targets/tower_organization/tasks/main.yml b/awx_collection/tests/integration/targets/tower_organization/tasks/main.yml index d6b174f674..a1523c45e6 100644 --- a/awx_collection/tests/integration/targets/tower_organization/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_organization/tasks/main.yml @@ -28,22 +28,10 @@ that: - "result is not changed" -- name: "Try adding a bad custom_virtualenv" - tower_organization: - name: "{{ org_name }}" - custom_virtualenv: "/does/not/exit" - register: result - ignore_errors: true - -- assert: - that: - - "result is failed" - - name: "Pass in all parameters" tower_organization: name: "{{ org_name }}" description: "A description" - custom_virtualenv: "" register: result - assert: @@ -54,7 +42,6 @@ tower_organization: name: "{{ org_name }}" description: "A new description" - custom_virtualenv: "" register: result - assert: diff --git a/awx_collection/tools/vars/examples.yml b/awx_collection/tools/vars/examples.yml index 8a8d132881..a927f970b7 100644 --- a/awx_collection/tools/vars/examples.yml +++ b/awx_collection/tools/vars/examples.yml @@ -49,4 +49,3 @@ examples: tower_config_file: "~/tower_cli.cfg" survey_enabled: yes survey_spec: "{{ '{{' }} lookup('file', 'my_survey.json') {{ '}}' }}" - custom_virtualenv: "/var/lib/awx/venv/custom-venv/" diff --git a/docs/custom_virtualenvs.md b/docs/custom_virtualenvs.md deleted file mode 100644 index 1f5b6e6a2d..0000000000 --- a/docs/custom_virtualenvs.md +++ /dev/null @@ -1,168 +0,0 @@ -Managing Custom Python Dependencies -=================================== -awx installations pre-build a special [Python -virtualenv](https://pypi.python.org/pypi/virtualenv) which is automatically -activated for all `ansible-playbook` runs invoked by awx (for example, any time -a Job Template is launched). By default, this virtualenv is located at -`/var/lib/awx/venv/ansible` on the file system. - -awx pre-installs a variety of third-party library/SDK support into this -virtualenv for its integration points with a variety of cloud providers (such -as EC2, OpenStack, Azure, etc...) - -Periodically, awx users want to add additional SDK support into this -virtualenv; this documentation describes the supported way to do so. - -Preparing a New Custom Virtualenv -================================= -awx allows a _different_ virtualenv to be specified and used on Job Template -runs. To choose a custom virtualenv, first we need to create one. Here, we are -using `/opt/my-envs/` as the directory to hold custom venvs. But you can use any -other directory and replace `/opt/my-envs/` with that. Let's create the directory -first if absent: - - NOTE: For docker installations, this directory needs to exist on awx_web AND - awx_task container - - $ sudo mkdir /opt/my-envs - -Now, we need to tell Tower to look into this directory for custom venvs. For that, -we can add this directory to the `CUSTOM_VENV_PATHS` setting as: - - $ HTTP PATCH /api/v2/settings/system/ {'CUSTOM_VENV_PATHS': ["/opt/my-envs/"]} - -If we have venvs spanned over multiple directories, we can add all the paths and -Tower will aggregate venvs from them: - - $ HTTP PATCH /api/v2/settings/system/ {'CUSTOM_VENV_PATHS': ["/path/1/to/venv/", - "/path/2/to/venv/", - "/path/3/to/venv/"]} - -Now that we have the directory setup, we can create a virtual environment in that using: - - $ sudo virtualenv /opt/my-envs/custom-venv - -Multiple versions of Python are supported, though it's important to note that -the semantics for creating virtualenvs in Python 3 has changed slightly: - - $ sudo python3 -m venv /opt/my-envs/custom-venv - -Your newly created virtualenv needs a few base dependencies to properly run -playbooks: -fact gathering): - - $ sudo /opt/my-envs/custom-venv/bin/pip install psutil - -From here, you can install _additional_ Python dependencies that you care -about, such as a per-virtualenv version of ansible itself: - - $ sudo /opt/my-envs/custom-venv/bin/pip install -U "ansible == X.Y.Z" - -...or an additional third-party SDK that's not included with the base awx installation: - - $ sudo /opt/my-envs/custom-venv/bin/pip install -U python-digitalocean - -If you want to copy them, the libraries included in awx's default virtualenv -can be found using `pip freeze`: - - $ sudo /var/lib/awx/venv/ansible/bin/pip freeze - -One important item to keep in mind is that in a clustered awx installation, -you need to ensure that the same custom virtualenv exists on _every_ local file -system at `/opt/my-envs/`. For container-based deployments, this likely -means building these steps into your own custom image building workflow, e.g., - -```diff -diff --git a/Makefile b/Makefile -index aa8b304..eb05f91 100644 ---- a/Makefile -+++ b/Makefile -@@ -164,6 +164,10 @@ requirements_ansible_dev: - $(VENV_BASE)/ansible/bin/pip install pytest mock; \ - fi - -+requirements_custom: -+ mkdir -p /opt/my-envs -+ virtualenv /opt/my-envs/my-custom-env -+ /opt/my-envs/my-custom-env/bin/pip install psutil -+ -diff --git a/installer/roles/image_build/templates/Dockerfile.j2 b/installer/roles/image_build/templates/Dockerfile.j2 -index d3b582ffcb..220ac760a3 100644 ---- a/installer/roles/image_build/templates/Dockerfile.j2 -+++ b/installer/roles/image_build/templates/Dockerfile.j2 -@@ -165,6 +165,7 @@ RUN openssl req -nodes -newkey rsa:2048 -keyout /etc/nginx/nginx.key -out /etc/n - chmod 640 /etc/nginx/nginx.{csr,key,crt} - {% else %} - COPY --from=builder /var/lib/awx /var/lib/awx -+COPY --from=builder /opt/my-envs /opt/my-envs - RUN ln -s /var/lib/awx/venv/awx/bin/awx-manage /usr/bin/awx-manage - {% endif %} -``` - -Once the AWX API is available, update the `CUSTOM_VENV_PATHS` setting as described in `Preparing a New Custom Virtualenv`. - -Kubernetes Custom Virtualenvs -============================= -You can add custom virtual environments without modifying images by including -the following variables in your `install.yml` playbook run. Your variables file -must have a variable called `custom_venvs` with a list of your custom -virtualenvs containing the name, python interpreter, ansible version, and a list -of modules that should be installed in each one: - -```yaml -# venv_vars.yaml ---- -custom_venvs: - - name: dns_team - python: python3 # Defaults to python2 - python_ansible_version: 2.8.1 - python_modules: - - dnspython - - infoblox-client - - name: windows_team - python: python2 - python_ansible_version: 2.8.0 - python_modules: - - winrm - - name: vmware_team - python_ansible_version: 2.7.10 - python_modules: - - pyvmomi -``` - -The virtualenvs will be created in `/opt/custom-venvs` by default, but you can -override that location by setting the variable `custom_venvs_path`. - -You can use the variables file like so: - - $ ansible-playbook -i inventory install.yml --extra-vars "@venv_vars.yaml" - -Once the AWX API is available, you will need to update the `CUSTOM_VENV_PATHS` -setting as described in `Preparing a New Custom Virtualenv`. - -Assigning Custom Virtualenvs -============================ -Once you've created a custom virtualenv, you can assign it at the Organization, -Project, or Job Template level: - -```http -PATCH https://awx-host.example.org/api/v2/organizations/N/ -PATCH https://awx-host.example.org/api/v2/projects/N/ -PATCH https://awx-host.example.org/api/v2/job_templates/N/ - -Content-Type: application/json -{ - "custom_virtualenv": "/opt/my-envs/custom-venv/" -} -``` - -An HTTP `GET` request to `/api/v2/config/` will provide a list of -detected installed virtualenvs: - - { - "custom_virtualenvs": [ - "/opt/my-envs/custom-venv/", - "/opt/my-envs/my-other-custom-venv/", - ], - ... - } From f294aabcc93935c42ef8d595a70c20bc4af2edbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ely=C3=A9zer=20Rezende?= Date: Fri, 5 Mar 2021 14:53:31 -0500 Subject: [PATCH 177/178] Linter fixes for Execution Environments module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix linter for the recently added Execution Environments module Signed-off-by: Elyézer Rezende --- awx_collection/plugins/modules/tower_ad_hoc_command.py | 1 + .../plugins/modules/tower_execution_environment.py | 6 +++--- awx_collection/plugins/modules/tower_export.py | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/awx_collection/plugins/modules/tower_ad_hoc_command.py b/awx_collection/plugins/modules/tower_ad_hoc_command.py index 2d099b2b1d..4ee416ccdf 100644 --- a/awx_collection/plugins/modules/tower_ad_hoc_command.py +++ b/awx_collection/plugins/modules/tower_ad_hoc_command.py @@ -132,6 +132,7 @@ def main(): wait=dict(default=False, type='bool'), interval=dict(default=1.0, type='float'), timeout=dict(default=None, type='int'), + execution_environment=dict(), ) # Create a module for ourselves diff --git a/awx_collection/plugins/modules/tower_execution_environment.py b/awx_collection/plugins/modules/tower_execution_environment.py index 320141721d..b728810323 100644 --- a/awx_collection/plugins/modules/tower_execution_environment.py +++ b/awx_collection/plugins/modules/tower_execution_environment.py @@ -16,7 +16,7 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', DOCUMENTATION = ''' --- module: tower_execution_environment -author: "Shane McDonald" +author: "Shane McDonald (@shanemcd)" short_description: create, update, or destroy Execution Environments in Ansible Tower. description: - Create, update, or destroy Execution Environments in Ansible Tower. See @@ -54,7 +54,7 @@ options: description: - determine image pull behavior choices: ["always", "missing", "never"] - default: '' + default: 'missing' type: str extends_documentation_fragment: awx.awx.auth ''' @@ -108,7 +108,7 @@ def main(): if pull: new_fields['pull'] = pull - + # Attempt to look up the related items the user specified (these will fail the module if not found) organization = module.params.get('organization') if organization: diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py index ac7ff5a499..e3b2fa37d6 100644 --- a/awx_collection/plugins/modules/tower_export.py +++ b/awx_collection/plugins/modules/tower_export.py @@ -47,6 +47,10 @@ options: description: - credential name to export type: str + execution_environments: + description: + - execution environment name to export + type: str notification_templates: description: - notification template name to export From afe8dc6ad91c032bd6cdc4404e567b3c33c7285d Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Fri, 5 Mar 2021 17:41:25 -0500 Subject: [PATCH 178/178] Fix sdb in dev env --- .../roles/sources/templates/docker-compose.yml.j2 | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 index ccc42d2b0e..6be54588d7 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 @@ -18,14 +18,6 @@ services: SDB_PORT: {{ awx_sdb_port_start }} AWX_GROUP_QUEUES: tower RECEPTORCTL_SOCKET: /var/run/receptor/receptor.sock - ports: -{% if cluster_node_count|int == 1 %} - - "8080:8080" # unused but mapped for debugging - - "8888:8888" # jupyter notebook - - "8013:8013" # http - - "8043:8043" # https -{% endif %} - - "{{ awx_sdb_port_start }}:{{ awx_sdb_port_end }}" # sdb-listen links: - postgres - redis_{{ container_postfix }} @@ -45,7 +37,8 @@ services: privileged: true tty: true ports: - - "{{ awx_sdb_port_start }}:{{ awx_sdb_port_end }}" # sdb-listen + - "6899:6899" + - "{{ awx_sdb_port_start }}-{{ awx_sdb_port_end }}:{{ awx_sdb_port_start }}-{{ awx_sdb_port_end }}" # sdb-listen {% if cluster_node_count|int == 1 %} - "8080:8080" # unused but mapped for debugging - "8888:8888" # jupyter notebook