diff --git a/openedx/core/djangoapps/theming/management/commands/create_sites_and_configurations.py b/openedx/core/djangoapps/theming/management/commands/create_sites_and_configurations.py new file mode 100644 index 0000000000000000000000000000000000000000..12166ae11b5d1a7945fe43c7ad0ea04734a2f71f --- /dev/null +++ b/openedx/core/djangoapps/theming/management/commands/create_sites_and_configurations.py @@ -0,0 +1,175 @@ +""" +This command will be run by an ansible script. +""" + +import os +import json +import fnmatch +import logging + +from provider.oauth2.models import Client +from provider.constants import CONFIDENTIAL +from edx_oauth2_provider.models import TrustedClient +from django.contrib.auth.models import User +from django.contrib.sites.models import Site +from django.core.management.base import BaseCommand + +from openedx.core.djangoapps.theming.models import SiteTheme +from openedx.core.djangoapps.site_configuration.models import SiteConfiguration + +LOG = logging.getLogger(__name__) + + +class Command(BaseCommand): + """ + Command to create the site, site themes, configuration and oauth2 clients for all WL-sites. + + Example: + ./manage.py lms create_sites_and_configurations --dns-name whitelabel --theme-path /edx/src/edx-themes/edx-platform + """ + dns_name = None + theme_path = None + ecommerce_user = None + discovery_user = None + + def add_arguments(self, parser): + """ + Add arguments to the command parser. + """ + parser.add_argument( + "--dns-name", + type=str, + help="Enter DNS name of sandbox.", + required=True + ) + + parser.add_argument( + "--theme-path", + type=str, + help="Enter theme directory path", + required=True + ) + + def _create_oauth2_client(self, url, site_name, is_discovery=True): + """ + Creates the oauth2 client and add it in trusted clients. + """ + + client, _ = Client.objects.get_or_create( + redirect_uri="{url}complete/edx-oidc/".format(url=url), + defaults={ + "user": self.discovery_user if is_discovery else self.ecommerce_user, + "name": "{site_name}_{client_type}_client".format( + site_name=site_name, + client_type="discovery" if is_discovery else "ecommerce", + ), + "url": url, + "client_id": "{client_type}-key-{site_name}".format( + client_type="discovery" if is_discovery else "ecommerce", + site_name=site_name + ), + "client_secret": "{client_type}-secret-{dns_name}".format( + client_type="discovery" if is_discovery else "ecommerce", + dns_name=self.dns_name + ), + "client_type": CONFIDENTIAL, + "logout_uri": "{url}logout/".format(url=url) + } + ) + LOG.info("Adding {client} oauth2 client as trusted client".format(client=client.name)) + TrustedClient.objects.get_or_create(client=client) + + def _create_sites(self, site_domain, theme_dir_name, site_configuration): + """ + Create Sites, SiteThemes and SiteConfigurations + """ + site, created = Site.objects.get_or_create( + domain=site_domain, + defaults={"name": theme_dir_name} + ) + if created: + LOG.info("Creating '{site_name}' SiteTheme".format(site_name=site_domain)) + SiteTheme.objects.create(site=site, theme_dir_name=theme_dir_name) + + LOG.info("Creating '{site_name}' SiteConfiguration".format(site_name=site_domain)) + SiteConfiguration.objects.create(site=site, values=site_configuration, enabled=True) + else: + LOG.info("'{site_domain}' site already exists".format(site_domain=site_domain)) + + def find(self, pattern, path): + """ + Matched the given pattern in given path and returns the list of matching files + """ + result = [] + for root, dirs, files in os.walk(path): # pylint: disable=unused-variable + for name in files: + if fnmatch.fnmatch(name, pattern): + result.append(os.path.join(root, name)) + return result + + def _get_sites_data(self): + """ + Reads the json files from theme directory and returns the site data in JSON format. + "site_a":{ + "theme_dir_name": "site_a.edu.au" + "configuration": { + "key1": "value1", + "key2": "value2" + } + } + """ + site_data = {} + for config_file in self.find('sandbox_configuration.json', self.theme_path): + LOG.info("Reading file from {file}".format(file=config_file)) + configuration_data = json.loads( + json.dumps( + json.load( + open(config_file) + ) + ).replace("{dns_name}", self.dns_name) + )['lms_configuration'] + + site_data[configuration_data['sandbox_name']] = { + "site_domain": configuration_data['site_domain'], + "theme_dir_name": configuration_data['theme_dir_name'], + "configuration": configuration_data['configuration'] + } + return site_data + + def get_or_create_service_user(self, username): + """ + Creates the service user for ecommerce and discovery. + """ + return User.objects.get_or_create( + username=username, + defaults={ + "is_staff": True, + "is_superuser": True + } + ) + + def handle(self, *args, **options): + + self.theme_path = options['theme_path'] + self.dns_name = options['dns_name'] + + self.discovery_user, _ = self.get_or_create_service_user("lms_catalog_service_user") + self.ecommerce_user, _ = self.get_or_create_service_user("ecommerce_worker") + + all_sites = self._get_sites_data() + + # creating Sites, SiteThemes, SiteConfigurations and oauth2 clients + for site_name, site_data in all_sites.items(): + site_domain = site_data['site_domain'] + + discovery_url = "https://discovery-{site_domain}/".format(site_domain=site_domain) + ecommerce_url = "https://ecommerce-{site_domain}/".format(site_domain=site_domain) + + LOG.info("Creating '{site_name}' Site".format(site_name=site_name)) + self._create_sites(site_domain, site_data['theme_dir_name'], site_data['configuration']) + + LOG.info("Creating discovery oauth2 client for '{site_name}' site".format(site_name=site_name)) + self._create_oauth2_client(discovery_url, site_name, is_discovery=True) + + LOG.info("Creating ecommerce oauth2 client for '{site_name}' site".format(site_name=site_name)) + self._create_oauth2_client(ecommerce_url, site_name, is_discovery=False) diff --git a/openedx/core/djangoapps/theming/management/commands/tests/__init__.py b/openedx/core/djangoapps/theming/management/commands/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/openedx/core/djangoapps/theming/management/commands/tests/test_create_sites_and_configurations.py b/openedx/core/djangoapps/theming/management/commands/tests/test_create_sites_and_configurations.py new file mode 100644 index 0000000000000000000000000000000000000000..a6e712856c2eee419252cbe28abda2981233b9e4 --- /dev/null +++ b/openedx/core/djangoapps/theming/management/commands/tests/test_create_sites_and_configurations.py @@ -0,0 +1,156 @@ +""" +Test cases for create_sites_and_configurations command. +""" + +import mock + +from django.test import TestCase +from django.contrib.auth.models import User +from django.contrib.sites.models import Site +from django.core.management import call_command, CommandError + +from provider.oauth2.models import Client +from edx_oauth2_provider.models import TrustedClient +from openedx.core.djangoapps.theming.models import SiteTheme + +SITES = ['site_a', 'site_b'] + + +def _generate_site_config(dns_name, site_domain): + """ Generate the site configuration for a given site """ + return { + "lms_url": "{domain}-{dns_name}.sandbox.edx.org".format(domain=site_domain, dns_name=dns_name), + "platform_name": "{domain}-{dns_name}".format(domain=site_domain, dns_name=dns_name) + } + + +def _get_sites(dns_name): + """ Creates the mocked data for management command """ + sites = {} + for site in SITES: + sites.update({ + site: { + "theme_dir_name": "{}_dir_name".format(site), + "configuration": _generate_site_config(dns_name, site), + "site_domain": "{site}-{dns_name}.sandbox.edx.org".format(site=site, dns_name=dns_name) + } + }) + return sites + + +class TestCreateSiteAndConfiguration(TestCase): + """ Test the create_site_and_configuration command """ + def setUp(self): + super(TestCreateSiteAndConfiguration, self).setUp() + + self.dns_name = "dummy_dns" + self.theme_path = "/dummyA/dummyB/" + + def _assert_sites_are_valid(self): + """ + Checks that data of all sites is valid + """ + sites = Site.objects.all() + # there is an extra default site. + self.assertEqual(len(sites), len(SITES) + 1) + for site in sites: + if site.name in SITES: + site_theme = SiteTheme.objects.get(site=site) + + self.assertEqual( + site_theme.theme_dir_name, + "{}_dir_name".format(site.name) + ) + + self.assertDictEqual( + dict(site.configuration.values), + _generate_site_config(self.dns_name, site.name) + ) + + def _assert_ecommerce_clients_are_valid(self): + """ + Checks that all ecommerce clients are valid + """ + service_user = User.objects.filter(username="ecommerce_worker") + self.assertEqual(len(service_user), 1) + self.assertTrue(service_user[0].is_staff) + + clients = Client.objects.filter(user=service_user) + self.assertEqual(len(clients), len(SITES)) + + for client in clients: + self.assertEqual(client.user.username, service_user[0].username) + site_name = client.name[:6] + ecommerce_url = "https://ecommerce-{site_name}-{dns_name}.sandbox.edx.org/".format( + site_name=site_name, + dns_name=self.dns_name + ) + self.assertEqual(client.url, ecommerce_url) + self.assertEqual( + client.redirect_uri, + "{ecommerce_url}complete/edx-oidc/".format(ecommerce_url=ecommerce_url) + ) + self.assertEqual( + len(TrustedClient.objects.filter(client=client)), + 1 + ) + + def _assert_discovery_clients_are_valid(self): + """ + Checks that all discovery clients are valid + """ + service_user = User.objects.filter(username="lms_catalog_service_user") + self.assertEqual(len(service_user), 1) + self.assertTrue(service_user[0].is_staff) + + clients = Client.objects.filter(user=service_user) + self.assertEqual(len(clients), len(SITES)) + + for client in clients: + self.assertEqual(client.user.username, service_user[0].username) + site_name = client.name[:6] + discovery_url = "https://discovery-{site_name}-{dns_name}.sandbox.edx.org/".format( + site_name=site_name, + dns_name=self.dns_name + ) + self.assertEqual(client.url, discovery_url) + self.assertEqual( + client.redirect_uri, + "{discovery_url}complete/edx-oidc/".format(discovery_url=discovery_url) + ) + self.assertEqual( + len(TrustedClient.objects.filter(client=client)), + 1 + ) + + def test_without_dns(self): + """ Test the command without dns_name """ + with self.assertRaises(CommandError): + call_command( + "create_sites_and_configurations" + ) + + @mock.patch( + 'openedx.core.djangoapps.theming.management.commands.create_sites_and_configurations.Command._get_sites_data' + ) + def test_with_dns(self, mock_get_sites): + """ Test the command with dns_name """ + mock_get_sites.return_value = _get_sites(self.dns_name) + call_command( + "create_sites_and_configurations", + "--dns-name", self.dns_name, + "--theme-path", self.theme_path + ) + self._assert_sites_are_valid() + self._assert_discovery_clients_are_valid() + self._assert_ecommerce_clients_are_valid() + + call_command( + "create_sites_and_configurations", + "--dns-name", self.dns_name, + "--theme-path", self.theme_path + ) + # if we run command with same dns then it will not duplicates the sites and oauth2 clients. + self._assert_sites_are_valid() + self._assert_discovery_clients_are_valid() + self._assert_ecommerce_clients_are_valid()