{"action":"created","comment":{"url":"https://api.github.com/repos/open-craft/opencraft/pulls/comments/61669471","id":61669471,"diff_hunk":"@@ -0,0 +1,178 @@\n+# -*- coding: utf-8 -*-\n+#\n+# OpenCraft -- tools to aid developing and hosting free software projects\n+# Copyright (C) 2015 OpenCraft <xavier@opencraft.com>\n+#\n+# This program is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU Affero General Public License as\n+# published by the Free Software Foundation, either version 3 of the\n+# License, or (at your option) any later version.\n+#\n+# This program is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU Affero General Public License for more details.\n+#\n+# You should have received a copy of the GNU Affero General Public License\n+# along with this program.  If not, see <http://www.gnu.org/licenses/>.\n+#\n+\"\"\"\n+Open edX Instance models\n+\"\"\"\n+from django.conf import settings\n+from django.db import models\n+\n+from instance.gandi import GandiAPI\n+from instance.logging import log_exception\n+from .mixins.openedx_database import OpenEdXDatabasesMixin\n+from .mixins.version_control import GitHubInstanceMixin\n+from .instance import Instance\n+from .openedx_appserver import OpenEdXAppConfiguration, OpenEdXAppServer\n+\n+# Constants ###################################################################\n+\n+gandi = GandiAPI()\n+\n+# Models ######################################################################\n+\n+\n+class BaseOpenEdXInstance(Instance, OpenEdXAppConfiguration, OpenEdXDatabasesMixin):\n+    \"\"\"\n+    BaseOpenEdXInstance: represents a website or set of affiliated websites powered by the same\n+    OpenEdX installation.\n+    \"\"\"\n+\n+    # Most settings/fields are inherited from OpenEdXAppConfiguration\n+    sub_domain = models.CharField(max_length=50)\n+    base_domain = models.CharField(max_length=50, blank=True)\n+\n+    active_appserver = models.OneToOneField(OpenEdXAppServer, null=True, on_delete=models.SET_NULL, related_name='+')\n+\n+    class Meta:\n+        abstract = True\n+        unique_together = ('base_domain', 'sub_domain')\n+        verbose_name = 'Open edX Instance'\n+\n+    @property\n+    def domain(self):\n+        \"\"\"\n+        Instance domain name\n+        \"\"\"\n+        return '{0.sub_domain}.{0.base_domain}'.format(self)\n+\n+    @property\n+    def url(self):\n+        \"\"\"\n+        LMS URL\n+        \"\"\"\n+        return u'{0.protocol}://{0.domain}/'.format(self)\n+\n+    @property\n+    def studio_sub_domain(self):\n+        \"\"\"\n+        Studio sub-domain name (eg. 'studio.master')\n+        \"\"\"\n+        return 'studio.{}'.format(self.sub_domain)\n+\n+    @property\n+    def studio_domain(self):\n+        \"\"\"\n+        Studio full domain name (eg. 'studio.master.sandbox.opencraft.com')\n+        \"\"\"\n+        return '{0.studio_sub_domain}.{0.base_domain}'.format(self)\n+\n+    @property\n+    def studio_url(self):\n+        \"\"\"\n+        Studio URL\n+        \"\"\"\n+        return u'{0.protocol}://{0.studio_domain}/'.format(self)\n+\n+    def save(self, **kwargs):\n+        \"\"\"\n+        Set default values before saving the instance.\n+        \"\"\"\n+        # Set default field values from settings - using the `default` field attribute confuses\n+        # automatically generated migrations, generating a new one when settings don't match\n+        if not self.base_domain:\n+            self.base_domain = settings.INSTANCES_BASE_DOMAIN\n+        if self.use_ephemeral_databases is None:\n+            self.use_ephemeral_databases = settings.INSTANCE_EPHEMERAL_DATABASES\n+        super().save(**kwargs)\n+\n+    @property\n+    def appserver_set(self):\n+        \"\"\"\n+        Get the set of OpenEdxAppServers owned by this instance.\n+        \"\"\"\n+        return self.ref.openedxappserver_set\n+\n+    def set_appserver_active(self, appserver_id):\n+        \"\"\"\n+        Mark the AppServer with the given ID as the active one.\n+        \"\"\"\n+        app_server = self.appserver_set.get(pk=appserver_id)  # Make sure the AppServer is owned by this instance\n+        self.active_appserver = app_server\n+        self.save()\n+        self.logger.info('Updating DNS: LMS at %s...', self.domain)\n+        gandi.set_dns_record(type='A', name=self.sub_domain, value=self.active_appserver.server.public_ip)\n+        self.logger.info('Updating DNS: Studio at %s...', self.studio_domain)\n+        gandi.set_dns_record(type='CNAME', name=self.studio_sub_domain, value=self.sub_domain)\n+\n+    @log_exception\n+    def spawn_appserver(self):\n+        \"\"\"\n+        Provision a new AppServer. If it completes successfully, mark it as active.\n+        \"\"\"\n+        # Provision external databases:\n+        if not self.use_ephemeral_databases:\n+            # TODO: Use db row-level locking to ensure we don't get any race conditions when creating these DBs.\n+            # Use select_for_update(nowait=True) to lock this object's row, then do these steps, then refresh_from_db\n+            self.logger.info('Provisioning MySQL database...')\n+            self.provision_mysql()\n+            self.logger.info('Provisioning MongoDB databases...')\n+            self.provision_mongo()\n+            self.logger.info('Provisioning Swift container...')\n+            self.provision_swift()\n+\n+        config_fields = [field.name for field in OpenEdXAppConfiguration._meta.fields if field.name not in ('id', )]\n+        config_copy = {field_name: getattr(self, field_name) for field_name in config_fields}\n+\n+        app_server = self.appserver_set.create(\n+            # Name for the app server: this should usually generate a unique name (and won't cause any issues if not):\n+            name=\"AppServer {}\".format(self.appserver_set.count() + 1),","path":"instance/models/openedx_instance.py","position":143,"original_position":143,"commit_id":"5f60fc4c742de8f7eba72f01571cb88eae6f0446","original_commit_id":"5f60fc4c742de8f7eba72f01571cb88eae6f0446","user":{"login":"antoviaque","id":514483,"avatar_url":"https://avatars.githubusercontent.com/u/514483?v=3","gravatar_id":"","url":"https://api.github.com/users/antoviaque","html_url":"https://github.com/antoviaque","followers_url":"https://api.github.com/users/antoviaque/followers","following_url":"https://api.github.com/users/antoviaque/following{/other_user}","gists_url":"https://api.github.com/users/antoviaque/gists{/gist_id}","starred_url":"https://api.github.com/users/antoviaque/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/antoviaque/subscriptions","organizations_url":"https://api.github.com/users/antoviaque/orgs","repos_url":"https://api.github.com/users/antoviaque/repos","events_url":"https://api.github.com/users/antoviaque/events{/privacy}","received_events_url":"https://api.github.com/users/antoviaque/received_events","type":"User","site_admin":false},"body":"Why set a name explicitly then? Maybe the AppServer could generate this string himself, based using its `pk` value?","created_at":"2016-04-30T13:11:47Z","updated_at":"2016-04-30T13:11:47Z","html_url":"https://github.com/open-craft/opencraft/pull/63#discussion_r61669471","pull_request_url":"https://api.github.com/repos/open-craft/opencraft/pulls/63","_links":{"self":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/comments/61669471"},"html":{"href":"https://github.com/open-craft/opencraft/pull/63#discussion_r61669471"},"pull_request":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/63"}}},"pull_request":{"url":"https://api.github.com/repos/open-craft/opencraft/pulls/63","id":68165566,"html_url":"https://github.com/open-craft/opencraft/pull/63","diff_url":"https://github.com/open-craft/opencraft/pull/63.diff","patch_url":"https://github.com/open-craft/opencraft/pull/63.patch","issue_url":"https://api.github.com/repos/open-craft/opencraft/issues/63","number":63,"state":"open","locked":false,"title":"OC-1541: Keep the old instance running while deploying a new one","user":{"login":"bradenmacdonald","id":945577,"avatar_url":"https://avatars.githubusercontent.com/u/945577?v=3","gravatar_id":"","url":"https://api.github.com/users/bradenmacdonald","html_url":"https://github.com/bradenmacdonald","followers_url":"https://api.github.com/users/bradenmacdonald/followers","following_url":"https://api.github.com/users/bradenmacdonald/following{/other_user}","gists_url":"https://api.github.com/users/bradenmacdonald/gists{/gist_id}","starred_url":"https://api.github.com/users/bradenmacdonald/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bradenmacdonald/subscriptions","organizations_url":"https://api.github.com/users/bradenmacdonald/orgs","repos_url":"https://api.github.com/users/bradenmacdonald/repos","events_url":"https://api.github.com/users/bradenmacdonald/events{/privacy}","received_events_url":"https://api.github.com/users/bradenmacdonald/received_events","type":"User","site_admin":false},"body":"**This is WIP**. Everything is broken (on purpose).\r\n\r\nChanges:\r\n- Fix occasional pylint warning about undocumented Meta classes\r\n- Combine LogEntry classes\r\n    * ^ These two commits have been copied to #67 so they can be reviewed more easily and merged sooner\r\n- Separated `[OpenEdX]Instance` into `Instance` (long-lived, mutable, creates AppServers and marks one as active), and `AppServer` (immutable, owns one VM)\r\n- Factored Open edX specific code into `OpenEdXInstance` and `OpenEdXAppServer` respectively\r\n- Factored Open edX specific code out of the ansible mixin\r\n- Change the ansible mixin so it can optionally run multiple playbooks\r\n- Removed `instance.last_provisioning_started` since you can compute that from the most recent `AppServer.created` field\r\n- Combined `forum_version`, `notifier_version`, `xqueue_version`, and `certs_version` into a single `openedx_release` setting, since these were always set to the same value, and if one needs to be set differently it can be done using ansible_extra_settings anyways.\r\n\r\nTODO:\r\n- Generate migrations & data migrations\r\n- Fix everything that broke\r\n   * Update API\r\n   * Update UI\r\n- Update tests\r\n\r\n\r\nSuggestions for future work:\r\n- Allow stopping an AppServer build\r\n- Separate PR-watching code to a separate app and make it optional (`SandboxInstance` should not need to know about pull requests, and it should be possible to use OpenCraft IM without specifying GitHub orgs/repos to watch)\r\n- Add a ProductionInstance class\r\n- Add Terminated state to the AppServer classes\r\n- Allow provisioning a new AppServer at any time","created_at":"2016-04-28T05:23:57Z","updated_at":"2016-04-30T13:11:47Z","closed_at":null,"merged_at":null,"merge_commit_sha":"0f73db83af4c3e5f468a5bcaea8af234e48750ef","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/open-craft/opencraft/pulls/63/commits","review_comments_url":"https://api.github.com/repos/open-craft/opencraft/pulls/63/comments","review_comment_url":"https://api.github.com/repos/open-craft/opencraft/pulls/comments{/number}","comments_url":"https://api.github.com/repos/open-craft/opencraft/issues/63/comments","statuses_url":"https://api.github.com/repos/open-craft/opencraft/statuses/5f60fc4c742de8f7eba72f01571cb88eae6f0446","head":{"label":"open-craft:provision-while-running","ref":"provision-while-running","sha":"5f60fc4c742de8f7eba72f01571cb88eae6f0446","user":{"login":"open-craft","id":7414786,"avatar_url":"https://avatars.githubusercontent.com/u/7414786?v=3","gravatar_id":"","url":"https://api.github.com/users/open-craft","html_url":"https://github.com/open-craft","followers_url":"https://api.github.com/users/open-craft/followers","following_url":"https://api.github.com/users/open-craft/following{/other_user}","gists_url":"https://api.github.com/users/open-craft/gists{/gist_id}","starred_url":"https://api.github.com/users/open-craft/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/open-craft/subscriptions","organizations_url":"https://api.github.com/users/open-craft/orgs","repos_url":"https://api.github.com/users/open-craft/repos","events_url":"https://api.github.com/users/open-craft/events{/privacy}","received_events_url":"https://api.github.com/users/open-craft/received_events","type":"Organization","site_admin":false},"repo":{"id":40447973,"name":"opencraft","full_name":"open-craft/opencraft","owner":{"login":"open-craft","id":7414786,"avatar_url":"https://avatars.githubusercontent.com/u/7414786?v=3","gravatar_id":"","url":"https://api.github.com/users/open-craft","html_url":"https://github.com/open-craft","followers_url":"https://api.github.com/users/open-craft/followers","following_url":"https://api.github.com/users/open-craft/following{/other_user}","gists_url":"https://api.github.com/users/open-craft/gists{/gist_id}","starred_url":"https://api.github.com/users/open-craft/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/open-craft/subscriptions","organizations_url":"https://api.github.com/users/open-craft/orgs","repos_url":"https://api.github.com/users/open-craft/repos","events_url":"https://api.github.com/users/open-craft/events{/privacy}","received_events_url":"https://api.github.com/users/open-craft/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/open-craft/opencraft","description":"OpenCraft - Instance Manager","fork":false,"url":"https://api.github.com/repos/open-craft/opencraft","forks_url":"https://api.github.com/repos/open-craft/opencraft/forks","keys_url":"https://api.github.com/repos/open-craft/opencraft/keys{/key_id}","collaborators_url":"https://api.github.com/repos/open-craft/opencraft/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/open-craft/opencraft/teams","hooks_url":"https://api.github.com/repos/open-craft/opencraft/hooks","issue_events_url":"https://api.github.com/repos/open-craft/opencraft/issues/events{/number}","events_url":"https://api.github.com/repos/open-craft/opencraft/events","assignees_url":"https://api.github.com/repos/open-craft/opencraft/assignees{/user}","branches_url":"https://api.github.com/repos/open-craft/opencraft/branches{/branch}","tags_url":"https://api.github.com/repos/open-craft/opencraft/tags","blobs_url":"https://api.github.com/repos/open-craft/opencraft/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/open-craft/opencraft/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/open-craft/opencraft/git/refs{/sha}","trees_url":"https://api.github.com/repos/open-craft/opencraft/git/trees{/sha}","statuses_url":"https://api.github.com/repos/open-craft/opencraft/statuses/{sha}","languages_url":"https://api.github.com/repos/open-craft/opencraft/languages","stargazers_url":"https://api.github.com/repos/open-craft/opencraft/stargazers","contributors_url":"https://api.github.com/repos/open-craft/opencraft/contributors","subscribers_url":"https://api.github.com/repos/open-craft/opencraft/subscribers","subscription_url":"https://api.github.com/repos/open-craft/opencraft/subscription","commits_url":"https://api.github.com/repos/open-craft/opencraft/commits{/sha}","git_commits_url":"https://api.github.com/repos/open-craft/opencraft/git/commits{/sha}","comments_url":"https://api.github.com/repos/open-craft/opencraft/comments{/number}","issue_comment_url":"https://api.github.com/repos/open-craft/opencraft/issues/comments{/number}","contents_url":"https://api.github.com/repos/open-craft/opencraft/contents/{+path}","compare_url":"https://api.github.com/repos/open-craft/opencraft/compare/{base}...{head}","merges_url":"https://api.github.com/repos/open-craft/opencraft/merges","archive_url":"https://api.github.com/repos/open-craft/opencraft/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/open-craft/opencraft/downloads","issues_url":"https://api.github.com/repos/open-craft/opencraft/issues{/number}","pulls_url":"https://api.github.com/repos/open-craft/opencraft/pulls{/number}","milestones_url":"https://api.github.com/repos/open-craft/opencraft/milestones{/number}","notifications_url":"https://api.github.com/repos/open-craft/opencraft/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/open-craft/opencraft/labels{/name}","releases_url":"https://api.github.com/repos/open-craft/opencraft/releases{/id}","deployments_url":"https://api.github.com/repos/open-craft/opencraft/deployments","created_at":"2015-08-09T19:35:05Z","updated_at":"2016-02-17T16:53:19Z","pushed_at":"2016-04-30T09:59:50Z","git_url":"git://github.com/open-craft/opencraft.git","ssh_url":"git@github.com:open-craft/opencraft.git","clone_url":"https://github.com/open-craft/opencraft.git","svn_url":"https://github.com/open-craft/opencraft","homepage":null,"size":1595,"stargazers_count":6,"watchers_count":6,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":6,"mirror_url":null,"open_issues_count":8,"forks":6,"open_issues":8,"watchers":6,"default_branch":"master"}},"base":{"label":"open-craft:master","ref":"master","sha":"f95e9d47cbd0bc6e6d3ebaead4592b673cc2c351","user":{"login":"open-craft","id":7414786,"avatar_url":"https://avatars.githubusercontent.com/u/7414786?v=3","gravatar_id":"","url":"https://api.github.com/users/open-craft","html_url":"https://github.com/open-craft","followers_url":"https://api.github.com/users/open-craft/followers","following_url":"https://api.github.com/users/open-craft/following{/other_user}","gists_url":"https://api.github.com/users/open-craft/gists{/gist_id}","starred_url":"https://api.github.com/users/open-craft/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/open-craft/subscriptions","organizations_url":"https://api.github.com/users/open-craft/orgs","repos_url":"https://api.github.com/users/open-craft/repos","events_url":"https://api.github.com/users/open-craft/events{/privacy}","received_events_url":"https://api.github.com/users/open-craft/received_events","type":"Organization","site_admin":false},"repo":{"id":40447973,"name":"opencraft","full_name":"open-craft/opencraft","owner":{"login":"open-craft","id":7414786,"avatar_url":"https://avatars.githubusercontent.com/u/7414786?v=3","gravatar_id":"","url":"https://api.github.com/users/open-craft","html_url":"https://github.com/open-craft","followers_url":"https://api.github.com/users/open-craft/followers","following_url":"https://api.github.com/users/open-craft/following{/other_user}","gists_url":"https://api.github.com/users/open-craft/gists{/gist_id}","starred_url":"https://api.github.com/users/open-craft/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/open-craft/subscriptions","organizations_url":"https://api.github.com/users/open-craft/orgs","repos_url":"https://api.github.com/users/open-craft/repos","events_url":"https://api.github.com/users/open-craft/events{/privacy}","received_events_url":"https://api.github.com/users/open-craft/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/open-craft/opencraft","description":"OpenCraft - Instance Manager","fork":false,"url":"https://api.github.com/repos/open-craft/opencraft","forks_url":"https://api.github.com/repos/open-craft/opencraft/forks","keys_url":"https://api.github.com/repos/open-craft/opencraft/keys{/key_id}","collaborators_url":"https://api.github.com/repos/open-craft/opencraft/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/open-craft/opencraft/teams","hooks_url":"https://api.github.com/repos/open-craft/opencraft/hooks","issue_events_url":"https://api.github.com/repos/open-craft/opencraft/issues/events{/number}","events_url":"https://api.github.com/repos/open-craft/opencraft/events","assignees_url":"https://api.github.com/repos/open-craft/opencraft/assignees{/user}","branches_url":"https://api.github.com/repos/open-craft/opencraft/branches{/branch}","tags_url":"https://api.github.com/repos/open-craft/opencraft/tags","blobs_url":"https://api.github.com/repos/open-craft/opencraft/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/open-craft/opencraft/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/open-craft/opencraft/git/refs{/sha}","trees_url":"https://api.github.com/repos/open-craft/opencraft/git/trees{/sha}","statuses_url":"https://api.github.com/repos/open-craft/opencraft/statuses/{sha}","languages_url":"https://api.github.com/repos/open-craft/opencraft/languages","stargazers_url":"https://api.github.com/repos/open-craft/opencraft/stargazers","contributors_url":"https://api.github.com/repos/open-craft/opencraft/contributors","subscribers_url":"https://api.github.com/repos/open-craft/opencraft/subscribers","subscription_url":"https://api.github.com/repos/open-craft/opencraft/subscription","commits_url":"https://api.github.com/repos/open-craft/opencraft/commits{/sha}","git_commits_url":"https://api.github.com/repos/open-craft/opencraft/git/commits{/sha}","comments_url":"https://api.github.com/repos/open-craft/opencraft/comments{/number}","issue_comment_url":"https://api.github.com/repos/open-craft/opencraft/issues/comments{/number}","contents_url":"https://api.github.com/repos/open-craft/opencraft/contents/{+path}","compare_url":"https://api.github.com/repos/open-craft/opencraft/compare/{base}...{head}","merges_url":"https://api.github.com/repos/open-craft/opencraft/merges","archive_url":"https://api.github.com/repos/open-craft/opencraft/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/open-craft/opencraft/downloads","issues_url":"https://api.github.com/repos/open-craft/opencraft/issues{/number}","pulls_url":"https://api.github.com/repos/open-craft/opencraft/pulls{/number}","milestones_url":"https://api.github.com/repos/open-craft/opencraft/milestones{/number}","notifications_url":"https://api.github.com/repos/open-craft/opencraft/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/open-craft/opencraft/labels{/name}","releases_url":"https://api.github.com/repos/open-craft/opencraft/releases{/id}","deployments_url":"https://api.github.com/repos/open-craft/opencraft/deployments","created_at":"2015-08-09T19:35:05Z","updated_at":"2016-02-17T16:53:19Z","pushed_at":"2016-04-30T09:59:50Z","git_url":"git://github.com/open-craft/opencraft.git","ssh_url":"git@github.com:open-craft/opencraft.git","clone_url":"https://github.com/open-craft/opencraft.git","svn_url":"https://github.com/open-craft/opencraft","homepage":null,"size":1595,"stargazers_count":6,"watchers_count":6,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":6,"mirror_url":null,"open_issues_count":8,"forks":6,"open_issues":8,"watchers":6,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/63"},"html":{"href":"https://github.com/open-craft/opencraft/pull/63"},"issue":{"href":"https://api.github.com/repos/open-craft/opencraft/issues/63"},"comments":{"href":"https://api.github.com/repos/open-craft/opencraft/issues/63/comments"},"review_comments":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/63/comments"},"review_comment":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/63/commits"},"statuses":{"href":"https://api.github.com/repos/open-craft/opencraft/statuses/5f60fc4c742de8f7eba72f01571cb88eae6f0446"}}}}