{"action":"created","comment":{"url":"https://api.github.com/repos/open-craft/opencraft/pulls/comments/59198193","id":59198193,"diff_hunk":"@@ -76,54 +143,52 @@ def exclude_terminated(self):\n         \"\"\"\n         Filter out terminated servers from the queryset\n         \"\"\"\n-        return self.filter(~Q(status=Server.TERMINATED))\n+        return self.filter(~Q(_status=Status.Terminated.state_id))\n \n \n class Server(ValidateModelMixin, TimeStampedModel):\n     \"\"\"\n     A single server VM\n     \"\"\"\n-    NEW = 'new'\n-    STARTED = 'started'\n-    ACTIVE = 'active'\n-    BOOTED = 'booted'\n-    PROVISIONING = 'provisioning'\n-    REBOOTING = 'rebooting'\n-    READY = 'ready'\n-    LIVE = 'live'\n-    STOPPING = 'stopping'\n-    STOPPED = 'stopped'\n-    TERMINATING = 'terminating'\n-    TERMINATED = 'terminated'\n-\n-    STATUS_CHOICES = (\n-        (NEW, 'New - Not yet loaded'),\n-        (STARTED, 'Started - Running but not active yet'),\n-        (ACTIVE, 'Active - Running but not booted yet'),\n-        (BOOTED, 'Booted - Booted but not ready to be added to the application'),\n-        (PROVISIONING, 'Provisioning - Provisioning is in progress'),\n-        (REBOOTING, 'Rebooting - Reboot in progress, to apply changes from provisioning'),\n-        (READY, 'Ready - Rebooted and ready to add to the application'),\n-        (LIVE, 'Live - Is actively used in the application and/or accessed by users'),\n-        (STOPPING, 'Stopping - Stopping temporarily'),\n-        (STOPPED, 'Stopped - Stopped temporarily'),\n-        (TERMINATING, 'Terminating - Stopping forever'),\n-        (TERMINATED, 'Terminated - Stopped forever'),\n+    Status = Status\n+    status = ResourceStateDescriptor(state_classes=Status.states, default_state=Status.New, model_field_name='_status')\n+    _status = models.CharField(\n+        max_length=20,\n+        default=status.default_state_class.state_id,\n+        choices=status.model_field_choices,\n+        db_index=True,\n+        db_column='status',\n     )\n+    # State transitions:\n+    _status_to_started = status.transition(from_states=Status.New, to_state=Status.Started)\n+    _status_to_active = status.transition(from_states=Status.Started, to_state=Status.Active)\n+    _status_to_booted = status.transition(from_states=Status.Active, to_state=Status.Booted)\n+    _status_to_provisioning = status.transition(from_states=Status.Booted, to_state=Status.Provisioning)\n+    _status_to_rebooting = status.transition(\n+        from_states=(Status.Active, Status.Booted, Status.Ready, Status.Provisioning), to_state=Status.Rebooting","path":"instance/models/server.py","position":160,"original_position":160,"commit_id":"3d170526357a76c56b856f545d60dafe4813e791","original_commit_id":"3d170526357a76c56b856f545d60dafe4813e791","user":{"login":"e-kolpakov","id":3330048,"avatar_url":"https://avatars.githubusercontent.com/u/3330048?v=3","gravatar_id":"","url":"https://api.github.com/users/e-kolpakov","html_url":"https://github.com/e-kolpakov","followers_url":"https://api.github.com/users/e-kolpakov/followers","following_url":"https://api.github.com/users/e-kolpakov/following{/other_user}","gists_url":"https://api.github.com/users/e-kolpakov/gists{/gist_id}","starred_url":"https://api.github.com/users/e-kolpakov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/e-kolpakov/subscriptions","organizations_url":"https://api.github.com/users/e-kolpakov/orgs","repos_url":"https://api.github.com/users/e-kolpakov/repos","events_url":"https://api.github.com/users/e-kolpakov/events{/privacy}","received_events_url":"https://api.github.com/users/e-kolpakov/received_events","type":"User","site_admin":false},"body":"@bradenmacdonald nit: specifying `from_states` as a list might become problematic in future - I'll elaborate more in comment to entire PR (when I'm done with line-by-line nitpicks :)).","created_at":"2016-04-11T12:53:47Z","updated_at":"2016-04-11T12:53:47Z","html_url":"https://github.com/open-craft/opencraft/pull/51#discussion_r59198193","pull_request_url":"https://api.github.com/repos/open-craft/opencraft/pulls/51","_links":{"self":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/comments/59198193"},"html":{"href":"https://github.com/open-craft/opencraft/pull/51#discussion_r59198193"},"pull_request":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/51"}}},"pull_request":{"url":"https://api.github.com/repos/open-craft/opencraft/pulls/51","id":65869579,"html_url":"https://github.com/open-craft/opencraft/pull/51","diff_url":"https://github.com/open-craft/opencraft/pull/51.diff","patch_url":"https://github.com/open-craft/opencraft/pull/51.patch","issue_url":"https://api.github.com/repos/open-craft/opencraft/issues/51","number":51,"state":"open","locked":false,"title":"State machine (OC-906)","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 replaces the `status` and `progress` attributes of the `Server` class with a finite state machine implementation.\r\n\r\nThe immediate benefits are:\r\n* Strong guarantee that the `Server` instance is always in one of the specified states.\r\n* Transitions between states are clearly defined. One cannot use public methods to change from \"New\" to \"Provisioning\" for example.\r\n* Removed states that weren't being used\r\n\r\nThe future benefits are much greater, but this is just \"a step in the right direction\". See below for next steps to help give context.\r\n\r\n### Features:\r\n\r\n* Any \"resource\" (such as a server) can have finite state machines attached to it\r\n* Each state machine is always in one of a pre-defined number of states\r\n* The value of the state can optionally be saved to the database if the resource is a django model\r\n* The state can only be changed through the use of a *transition*; the allowed transitions ('allow changing from Loading to Ready') are clearly defined, and an attempt to make an invalid transition always raises an exception\r\n* A resource can annotate its methods with annotations like `@status.only_for(Status.Booted)` - this will automatically raise an exception if the method is called when not appropriate, and also gives access to the handy `if method.is_available()` property which can be used to determine if you're allowed to call a method without actually calling it, in a fully generic fashion (you don't need to know what states support the method and what don't). So you can do things like `while not server.run_ssh_command.is_available(): wait()` which will wait until a server is ready to run SSH commands, without you needing to know what state(s) the server is in.\r\n* When reading some state of a resource, you get an *instance* of the state, which has a pointer to the resource instance. This means that you can simplify resources by implementing methods on the state class rather than the resource. This is completely optional though.\r\n    Example:\r\n\r\n    ```python\r\n    # Instead of \r\n    class Resource:\r\n        def update(self):\r\n            if self.status == New:\r\n                ...\r\n            elif self.status == Ready:\r\n                ...\r\n            else:\r\n                raise Exception()\r\n\r\n    # You can just use:\r\n    class Resource:\r\n        def update(self):\r\n            self.status.update()\r\n    ```\r\n    When the behavior of an object varies greatly depending on its state, or you want to do cool things with subclassing, this type of abstraction is very powerful. For example, say you have many types of server: OpenStackServer, AWSServer, LinodeServer, and each provider offers different error states such as \"network_offline\", \"alien_attack\", etc. If your code has a `server` instance, you could just check the health of the server with `server.state.is_healthy()` rather than `server.state in (Server.State.Booting, Server.State.Running, OpenStackServer.State.RestoringImage, LinodeServer.State.Cloning)`\r\n\r\n### Suggested Next Steps\r\n\r\n* Change the server states to Unknown, Pending, Building, Booting, Ready, BuildFailed, and Terminated. (Unknown for when the OpenStack API is not responsive, BuildFailed for when OpenStack fails to create the server, e.g. due to a quota issue).\r\n* Rename OpenEdXInstance to SingleVMOpenEdXInstance. Give it a set of states: New, WaitingForServer, ConfiguringServer, Running, ConfigurationFailed, Error.\r\n* Create a new class OpenEdXSandbox, which manages of a collection of SingleVMOpenEdXInstance. When a new instance successfully provisions, the old ones get deleted. When provisioning fails, any  old sandbox that still runs will be kept.\r\n* Give the Server states these properties: is_steady_state (the status is not expected to change, so don't wait for it to do so - e.g. Ready, BuildFailed, Terminated), and accepts_ssh_commands. Then refactor the code so that outside of the Server class we don't need to know what set of server states there are; we can just display `server.status.name` and `server.status.description` in the UI, and we can do things like `sleep() until server.status.accepts_ssh_commands`. This will allow us to do things in the future, like support more of the OpenStack API by adding more possible server states, without being coupled to other parts of the codebase.","created_at":"2016-04-09T03:34:44Z","updated_at":"2016-04-11T12:53:47Z","closed_at":null,"merged_at":null,"merge_commit_sha":null,"assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/open-craft/opencraft/pulls/51/commits","review_comments_url":"https://api.github.com/repos/open-craft/opencraft/pulls/51/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/51/comments","statuses_url":"https://api.github.com/repos/open-craft/opencraft/statuses/3d170526357a76c56b856f545d60dafe4813e791","head":{"label":"open-craft:state-machine","ref":"state-machine","sha":"3d170526357a76c56b856f545d60dafe4813e791","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-10T04:56:08Z","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":1422,"stargazers_count":6,"watchers_count":6,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":5,"mirror_url":null,"open_issues_count":6,"forks":5,"open_issues":6,"watchers":6,"default_branch":"master"}},"base":{"label":"open-craft:master","ref":"master","sha":"77ad0ac71230863e9a5a0f2c52cdccea75e41dc6","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-10T04:56:08Z","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":1422,"stargazers_count":6,"watchers_count":6,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":5,"mirror_url":null,"open_issues_count":6,"forks":5,"open_issues":6,"watchers":6,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/51"},"html":{"href":"https://github.com/open-craft/opencraft/pull/51"},"issue":{"href":"https://api.github.com/repos/open-craft/opencraft/issues/51"},"comments":{"href":"https://api.github.com/repos/open-craft/opencraft/issues/51/comments"},"review_comments":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/51/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/51/commits"},"statuses":{"href":"https://api.github.com/repos/open-craft/opencraft/statuses/3d170526357a76c56b856f545d60dafe4813e791"}}}}