{"action":"created","comment":{"url":"https://api.github.com/repos/freenas/freenas/pulls/comments/175128925","pull_request_review_id":104613732,"id":175128925,"diff_hunk":"@@ -0,0 +1,493 @@\n+from collections import defaultdict\n+import copy\n+from datetime import datetime\n+import json\n+import os\n+import traceback\n+\n+from freenasUI.support.utils import get_license\n+from licenselib.license import ContractType\n+\n+from middlewared.alert.base import *\n+from middlewared.alert.base import AlertService as _AlertService\n+from middlewared.schema import Dict, Str, Bool, Int, accepts, Patch\n+from middlewared.service import (\n+    ConfigService, CRUDService, Service, ValidationErrors,\n+    periodic, private,\n+)\n+from middlewared.utils import load_modules, load_classes\n+\n+POLICIES = [\"IMMEDIATELY\", \"HOURLY\", \"DAILY\", \"NEVER\"]\n+DEFAULT_POLICY = \"IMMEDIATELY\"\n+\n+ALERT_SOURCES = {}\n+ALERT_SERVICES_FACTORIES = {}\n+\n+\n+class AlertPolicy:\n+    def __init__(self, key=lambda now: now):\n+        self.key = key\n+\n+        self.last_key_value = None\n+        self.last_key_value_alerts = defaultdict(lambda: defaultdict(dict))\n+\n+    def receive_alerts(self, now, alerts):\n+        gone_alerts = []\n+        new_alerts = []\n+        key = self.key(now)\n+        if key != self.last_key_value:\n+            for node in set(alerts.keys()) | set(self.last_key_value_alerts.keys()):\n+                for alert_source_name in set(alerts[node].keys()) | set(self.last_key_value_alerts[node].keys()):\n+                    for gone_alert in (set(self.last_key_value_alerts[node][alert_source_name].keys()) -\n+                                       set(alerts[node][alert_source_name].keys())):\n+                        gone_alerts.append(self.last_key_value_alerts[node][alert_source_name][gone_alert])\n+                    for new_alert in (set(alerts[node][alert_source_name].keys()) -\n+                                      set(self.last_key_value_alerts[node][alert_source_name].keys())):\n+                        new_alerts.append(alerts[node][alert_source_name][new_alert])\n+\n+            self.last_key_value = key\n+            self.last_key_value_alerts = copy.deepcopy(alerts)\n+\n+        return gone_alerts, new_alerts\n+\n+\n+class AlertService(Service):\n+    def __init__(self, middleware):\n+        super().__init__(middleware)\n+\n+        self.node = \"A\"\n+\n+        self.alerts = defaultdict(lambda: defaultdict(dict))\n+\n+        self.alert_source_last_run = defaultdict(lambda: datetime.min)\n+\n+        self.policies = {\n+            \"IMMEDIATELY\": AlertPolicy(),\n+            \"HOURLY\": AlertPolicy(lambda d: (d.date(), d.hour)),\n+            \"DAILY\": AlertPolicy(lambda d: (d.date())),\n+            \"NEVER\": AlertPolicy(lambda d: None),\n+        }\n+\n+    @private\n+    async def initialize(self):\n+        if not await self.middleware.call(\"system.is_freenas\"):\n+            if await self.middleware.call(\"notifier.failover_node\") == \"B\":\n+                self.node = \"B\"\n+\n+        for alert in await self.middleware.call(\"datastore.query\", \"system.alert\"):\n+            del alert[\"id\"]\n+            alert[\"level\"] = AlertLevel(alert[\"level\"])\n+\n+            alert = Alert(**alert)\n+\n+            self.alerts[alert.node][alert.source][alert.key] = alert\n+\n+        for policy in self.policies.values():\n+            policy.receive_alerts(datetime.utcnow(), self.alerts)\n+\n+        for module in load_modules(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.path.pardir,\n+                                                \"alert\", \"source\")):\n+            for cls in load_classes(module, AlertSource, (FilePresenceAlertSource, ThreadedAlertSource)):\n+                source = cls(self.middleware)\n+                ALERT_SOURCES[source.name] = source\n+\n+        for module in load_modules(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.path.pardir,\n+                                                \"alert\", \"service\")):\n+            for cls in load_classes(module, _AlertService, (ThreadedAlertService,)):\n+                ALERT_SERVICES_FACTORIES[cls.name()] = cls\n+\n+    @private\n+    async def terminate(self):\n+        await self.flush_alerts()\n+\n+    @accepts()\n+    async def list_policies(self):\n+        return POLICIES\n+\n+    @accepts()\n+    async def list_sources(self):\n+        return [\n+            {\n+                \"name\": source.name,\n+                \"title\": source.title,\n+            }\n+            for source in sorted(ALERT_SOURCES.values(), key=lambda source: source.title.lower())\n+        ]\n+\n+    @accepts()\n+    def list(self):\n+        return [\n+            dict(alert.__dict__,\n+                 id=f\"{alert.node};{alert.source};{alert.key}\",\n+                 level=alert.level.name,\n+                 formatted=alert.formatted)\n+            for alert in sorted(self.__get_all_alerts(), key=lambda alert: alert.title)\n+        ]\n+\n+    @accepts(Str(\"id\"))\n+    def dismiss(self, id):\n+        node, source, key = id.split(\";\", 2)\n+        alert = self.alerts[node][source][key]\n+        alert.dismissed = True\n+\n+    @accepts(Str(\"id\"))\n+    def restore(self, id):\n+        node, source, key = id.split(\";\", 2)\n+        alert = self.alerts[node][source][key]\n+        alert.dismissed = False\n+\n+    @periodic(60)\n+    async def process_alerts(self):\n+        if not await self.middleware.call(\"system.is_freenas\"):\n+            if await self.middleware.call(\"notifier.failover_node\") == \"B\":\n+                return\n+\n+        await self.__run_alerts()\n+\n+        default_settings = (await self.middleware.call(\"alertdefaultsettings.config\"))[\"settings\"]\n+\n+        all_alerts = self.__get_all_alerts()\n+\n+        now = datetime.now()\n+        for policy_name, policy in self.policies.items():\n+            gone_alerts, new_alerts = policy.receive_alerts(now, self.alerts)\n+\n+            for alert_service_desc in await self.middleware.call(\"datastore.query\", \"system.alertservice\"):\n+                service_settings = dict(default_settings, **alert_service_desc[\"settings\"])\n+\n+                service_gone_alerts = [alert for alert in gone_alerts\n+                                       if service_settings.get(alert.source, DEFAULT_POLICY) == policy_name]\n+                service_new_alerts = [alert for alert in new_alerts\n+                                      if service_settings.get(alert.source, DEFAULT_POLICY) == policy_name]\n+\n+                if not service_gone_alerts and not service_new_alerts:\n+                    continue\n+\n+                factory = ALERT_SERVICES_FACTORIES.get(alert_service_desc[\"type\"])\n+                if factory is None:\n+                    self.logger.error(\"Alert service %r does not exist\", alert_service_desc[\"type\"])\n+                    continue\n+\n+                try:\n+                    alert_service = factory(self.middleware, alert_service_desc[\"attributes\"])\n+                except Exception:\n+                    self.logger.error(\"Error creating alert service %r with parameters=%r\",\n+                                      alert_service_desc[\"type\"], alert_service_desc[\"attributes\"], exc_info=True)\n+                    continue\n+\n+                if all_alerts or service_gone_alerts or service_new_alerts:\n+                    try:\n+                        await alert_service.send(all_alerts, service_gone_alerts, service_new_alerts)\n+                    except Exception:\n+                        self.logger.error(\"Error in alert service %r\", alert_service_desc[\"type\"], exc_info=True)\n+\n+            if policy_name == \"IMMEDIATELY\":\n+                if not await self.middleware.call(\"system.is_freenas\"):\n+                    new_hardware_alerts = [alert for alert in new_alerts if ALERT_SOURCES[alert.source].hardware]\n+                    if new_hardware_alerts:\n+                        license = get_license()\n+                        if license and license.contract_type in [ContractType.silver.value, ContractType.gold.value]:\n+                            try:\n+                                support = await self.middleware.call(\"datastore.query\", \"system.support\", None,\n+                                                                     {\"get\": True})\n+                            except IndexError:\n+                                await self.middleware.call(\"datastore.insert\", \"system.support\", {})\n+\n+                                support = await self.middleware.call(\"datastore.query\", \"system.support\", None,\n+                                                                     {\"get\": True})\n+\n+                            if support[\"enabled\"]:\n+                                msg = [f\"* {alert.formatted}\" for alert in new_hardware_alerts]\n+\n+                                serial = await self.middleware.call(\"system._serial\")\n+\n+                                for name, verbose_name in (\n+                                    (\"name\", \"Contact Name\"),\n+                                    (\"title\", \"Contact Title\"),\n+                                    (\"email\", \"Contact E-mail\"),\n+                                    (\"phone\", \"Contact Phone\"),\n+                                    (\"secondary_name\", \"Secondary Contact Name\"),\n+                                    (\"secondary_title\", \"Secondary Contact Title\"),\n+                                    (\"secondary_email\", \"Secondary Contact E-mail\"),\n+                                    (\"secondary_phone\", \"Secondary Contact Phone\"),\n+                                ):\n+                                    value = getattr(support, name)\n+                                    if value:\n+                                        msg += [\"\", \"{}: {}\".format(verbose_name, value)]\n+\n+                                try:\n+                                    await self.middleware.call(\"support.new_ticket\", {\n+                                        \"title\": \"Automatic alert (%s)\" % serial,\n+                                        \"body\": \"\\n\".join(msg),\n+                                        \"attach_debug\": False,\n+                                        \"category\": \"Hardware\",\n+                                        \"criticality\": \"Loss of Functionality\",\n+                                        \"environment\": \"Production\",\n+                                        \"name\": \"Automatic Alert\",\n+                                        \"email\": \"auto-support@ixsystems.com\",\n+                                        \"phone\": \"-\",\n+                                    })\n+                                except Exception:\n+                                    self.logger.error(f\"Failed to create a support ticket\", exc_info=True)\n+\n+    async def __run_alerts(self):\n+        failover = False\n+        if not await self.middleware.call(\"system.is_freenas\"):\n+            failover = await self.middleware.call(\"notifier.failover_licensed\")\n+\n+        for alert_source in ALERT_SOURCES.values():\n+            if datetime.utcnow() < self.alert_source_last_run[alert_source.name] + alert_source.interval:\n+                continue\n+\n+            self.alert_source_last_run[alert_source.name] = datetime.utcnow()\n+\n+            self.logger.trace(\"Running alert source: %r\", alert_source.name)\n+\n+            alerts_a = await self.__run_source(alert_source.name)\n+            for alert in alerts_a:\n+                alert.node = \"A\"\n+\n+            alerts_b = []\n+            if failover and alert_source.run_on_passive_node:\n+                try:\n+                    alerts_b = [Alert(**alert)\n+                                for alert in (await self.middleware.call(\"failover.call_remote\", \"alert.run_source\",\n+                                                                         alert_source.name))]","path":"src/middlewared/middlewared/plugins/alert.py","position":255,"original_position":255,"commit_id":"e2b8462f047f06b955fee3e9be50d2db9c5ba8da","original_commit_id":"e2b8462f047f06b955fee3e9be50d2db9c5ba8da","user":{"login":"themylogin","id":669257,"avatar_url":"https://avatars3.githubusercontent.com/u/669257?v=4","gravatar_id":"","url":"https://api.github.com/users/themylogin","html_url":"https://github.com/themylogin","followers_url":"https://api.github.com/users/themylogin/followers","following_url":"https://api.github.com/users/themylogin/following{/other_user}","gists_url":"https://api.github.com/users/themylogin/gists{/gist_id}","starred_url":"https://api.github.com/users/themylogin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/themylogin/subscriptions","organizations_url":"https://api.github.com/users/themylogin/orgs","repos_url":"https://api.github.com/users/themylogin/repos","events_url":"https://api.github.com/users/themylogin/events{/privacy}","received_events_url":"https://api.github.com/users/themylogin/received_events","type":"User","site_admin":false},"body":"What is the proper check to determine if we should skip running alerts on node B?","created_at":"2018-03-16T15:35:53Z","updated_at":"2018-03-16T15:35:53Z","html_url":"https://github.com/freenas/freenas/pull/975#discussion_r175128925","pull_request_url":"https://api.github.com/repos/freenas/freenas/pulls/975","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/freenas/freenas/pulls/comments/175128925"},"html":{"href":"https://github.com/freenas/freenas/pull/975#discussion_r175128925"},"pull_request":{"href":"https://api.github.com/repos/freenas/freenas/pulls/975"}},"in_reply_to_id":175128186},"pull_request":{"url":"https://api.github.com/repos/freenas/freenas/pulls/975","id":175223542,"html_url":"https://github.com/freenas/freenas/pull/975","diff_url":"https://github.com/freenas/freenas/pull/975.diff","patch_url":"https://github.com/freenas/freenas/pull/975.patch","issue_url":"https://api.github.com/repos/freenas/freenas/issues/975","number":975,"state":"open","locked":false,"title":"New alerts","user":{"login":"themylogin","id":669257,"avatar_url":"https://avatars3.githubusercontent.com/u/669257?v=4","gravatar_id":"","url":"https://api.github.com/users/themylogin","html_url":"https://github.com/themylogin","followers_url":"https://api.github.com/users/themylogin/followers","following_url":"https://api.github.com/users/themylogin/following{/other_user}","gists_url":"https://api.github.com/users/themylogin/gists{/gist_id}","starred_url":"https://api.github.com/users/themylogin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/themylogin/subscriptions","organizations_url":"https://api.github.com/users/themylogin/orgs","repos_url":"https://api.github.com/users/themylogin/repos","events_url":"https://api.github.com/users/themylogin/events{/privacy}","received_events_url":"https://api.github.com/users/themylogin/received_events","type":"User","site_admin":false},"body":"* Execute all alert sources in middleware\r\n\r\n* Implement alert services in middleware (forget consul-alerts)\r\n\r\n* Manage alert sources and services through middleware plugin. Provide opportunity to specify alert policy for each alert source and alert service pair.\r\n\r\nTicket: #26679","created_at":"2018-03-15T11:38:11Z","updated_at":"2018-03-16T15:35:53Z","closed_at":null,"merged_at":null,"merge_commit_sha":"2d6c591a6f9ce474404f6fdd7e0fcc0c39015d88","assignee":null,"assignees":[],"requested_reviewers":[{"login":"araujobsd","id":6048727,"avatar_url":"https://avatars0.githubusercontent.com/u/6048727?v=4","gravatar_id":"","url":"https://api.github.com/users/araujobsd","html_url":"https://github.com/araujobsd","followers_url":"https://api.github.com/users/araujobsd/followers","following_url":"https://api.github.com/users/araujobsd/following{/other_user}","gists_url":"https://api.github.com/users/araujobsd/gists{/gist_id}","starred_url":"https://api.github.com/users/araujobsd/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/araujobsd/subscriptions","organizations_url":"https://api.github.com/users/araujobsd/orgs","repos_url":"https://api.github.com/users/araujobsd/repos","events_url":"https://api.github.com/users/araujobsd/events{/privacy}","received_events_url":"https://api.github.com/users/araujobsd/received_events","type":"User","site_admin":false}],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/freenas/freenas/pulls/975/commits","review_comments_url":"https://api.github.com/repos/freenas/freenas/pulls/975/comments","review_comment_url":"https://api.github.com/repos/freenas/freenas/pulls/comments{/number}","comments_url":"https://api.github.com/repos/freenas/freenas/issues/975/comments","statuses_url":"https://api.github.com/repos/freenas/freenas/statuses/e2b8462f047f06b955fee3e9be50d2db9c5ba8da","head":{"label":"freenas:FIX-26679","ref":"FIX-26679","sha":"e2b8462f047f06b955fee3e9be50d2db9c5ba8da","user":{"login":"freenas","id":3792476,"avatar_url":"https://avatars2.githubusercontent.com/u/3792476?v=4","gravatar_id":"","url":"https://api.github.com/users/freenas","html_url":"https://github.com/freenas","followers_url":"https://api.github.com/users/freenas/followers","following_url":"https://api.github.com/users/freenas/following{/other_user}","gists_url":"https://api.github.com/users/freenas/gists{/gist_id}","starred_url":"https://api.github.com/users/freenas/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/freenas/subscriptions","organizations_url":"https://api.github.com/users/freenas/orgs","repos_url":"https://api.github.com/users/freenas/repos","events_url":"https://api.github.com/users/freenas/events{/privacy}","received_events_url":"https://api.github.com/users/freenas/received_events","type":"Organization","site_admin":false},"repo":{"id":8627760,"name":"freenas","full_name":"freenas/freenas","owner":{"login":"freenas","id":3792476,"avatar_url":"https://avatars2.githubusercontent.com/u/3792476?v=4","gravatar_id":"","url":"https://api.github.com/users/freenas","html_url":"https://github.com/freenas","followers_url":"https://api.github.com/users/freenas/followers","following_url":"https://api.github.com/users/freenas/following{/other_user}","gists_url":"https://api.github.com/users/freenas/gists{/gist_id}","starred_url":"https://api.github.com/users/freenas/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/freenas/subscriptions","organizations_url":"https://api.github.com/users/freenas/orgs","repos_url":"https://api.github.com/users/freenas/repos","events_url":"https://api.github.com/users/freenas/events{/privacy}","received_events_url":"https://api.github.com/users/freenas/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/freenas/freenas","description":"FreeNAS Git Repository","fork":false,"url":"https://api.github.com/repos/freenas/freenas","forks_url":"https://api.github.com/repos/freenas/freenas/forks","keys_url":"https://api.github.com/repos/freenas/freenas/keys{/key_id}","collaborators_url":"https://api.github.com/repos/freenas/freenas/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/freenas/freenas/teams","hooks_url":"https://api.github.com/repos/freenas/freenas/hooks","issue_events_url":"https://api.github.com/repos/freenas/freenas/issues/events{/number}","events_url":"https://api.github.com/repos/freenas/freenas/events","assignees_url":"https://api.github.com/repos/freenas/freenas/assignees{/user}","branches_url":"https://api.github.com/repos/freenas/freenas/branches{/branch}","tags_url":"https://api.github.com/repos/freenas/freenas/tags","blobs_url":"https://api.github.com/repos/freenas/freenas/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/freenas/freenas/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/freenas/freenas/git/refs{/sha}","trees_url":"https://api.github.com/repos/freenas/freenas/git/trees{/sha}","statuses_url":"https://api.github.com/repos/freenas/freenas/statuses/{sha}","languages_url":"https://api.github.com/repos/freenas/freenas/languages","stargazers_url":"https://api.github.com/repos/freenas/freenas/stargazers","contributors_url":"https://api.github.com/repos/freenas/freenas/contributors","subscribers_url":"https://api.github.com/repos/freenas/freenas/subscribers","subscription_url":"https://api.github.com/repos/freenas/freenas/subscription","commits_url":"https://api.github.com/repos/freenas/freenas/commits{/sha}","git_commits_url":"https://api.github.com/repos/freenas/freenas/git/commits{/sha}","comments_url":"https://api.github.com/repos/freenas/freenas/comments{/number}","issue_comment_url":"https://api.github.com/repos/freenas/freenas/issues/comments{/number}","contents_url":"https://api.github.com/repos/freenas/freenas/contents/{+path}","compare_url":"https://api.github.com/repos/freenas/freenas/compare/{base}...{head}","merges_url":"https://api.github.com/repos/freenas/freenas/merges","archive_url":"https://api.github.com/repos/freenas/freenas/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/freenas/freenas/downloads","issues_url":"https://api.github.com/repos/freenas/freenas/issues{/number}","pulls_url":"https://api.github.com/repos/freenas/freenas/pulls{/number}","milestones_url":"https://api.github.com/repos/freenas/freenas/milestones{/number}","notifications_url":"https://api.github.com/repos/freenas/freenas/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/freenas/freenas/labels{/name}","releases_url":"https://api.github.com/repos/freenas/freenas/releases{/id}","deployments_url":"https://api.github.com/repos/freenas/freenas/deployments","created_at":"2013-03-07T13:23:20Z","updated_at":"2018-03-16T00:54:12Z","pushed_at":"2018-03-16T15:29:55Z","git_url":"git://github.com/freenas/freenas.git","ssh_url":"git@github.com:freenas/freenas.git","clone_url":"https://github.com/freenas/freenas.git","svn_url":"https://github.com/freenas/freenas","homepage":"http://www.freenas.org","size":523193,"stargazers_count":902,"watchers_count":902,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":286,"mirror_url":null,"archived":false,"open_issues_count":11,"license":null,"forks":286,"open_issues":11,"watchers":902,"default_branch":"master"}},"base":{"label":"freenas:master","ref":"master","sha":"addb887c25de95dc15b2aeb97e5d3a0fcb4c9c5b","user":{"login":"freenas","id":3792476,"avatar_url":"https://avatars2.githubusercontent.com/u/3792476?v=4","gravatar_id":"","url":"https://api.github.com/users/freenas","html_url":"https://github.com/freenas","followers_url":"https://api.github.com/users/freenas/followers","following_url":"https://api.github.com/users/freenas/following{/other_user}","gists_url":"https://api.github.com/users/freenas/gists{/gist_id}","starred_url":"https://api.github.com/users/freenas/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/freenas/subscriptions","organizations_url":"https://api.github.com/users/freenas/orgs","repos_url":"https://api.github.com/users/freenas/repos","events_url":"https://api.github.com/users/freenas/events{/privacy}","received_events_url":"https://api.github.com/users/freenas/received_events","type":"Organization","site_admin":false},"repo":{"id":8627760,"name":"freenas","full_name":"freenas/freenas","owner":{"login":"freenas","id":3792476,"avatar_url":"https://avatars2.githubusercontent.com/u/3792476?v=4","gravatar_id":"","url":"https://api.github.com/users/freenas","html_url":"https://github.com/freenas","followers_url":"https://api.github.com/users/freenas/followers","following_url":"https://api.github.com/users/freenas/following{/other_user}","gists_url":"https://api.github.com/users/freenas/gists{/gist_id}","starred_url":"https://api.github.com/users/freenas/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/freenas/subscriptions","organizations_url":"https://api.github.com/users/freenas/orgs","repos_url":"https://api.github.com/users/freenas/repos","events_url":"https://api.github.com/users/freenas/events{/privacy}","received_events_url":"https://api.github.com/users/freenas/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/freenas/freenas","description":"FreeNAS Git Repository","fork":false,"url":"https://api.github.com/repos/freenas/freenas","forks_url":"https://api.github.com/repos/freenas/freenas/forks","keys_url":"https://api.github.com/repos/freenas/freenas/keys{/key_id}","collaborators_url":"https://api.github.com/repos/freenas/freenas/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/freenas/freenas/teams","hooks_url":"https://api.github.com/repos/freenas/freenas/hooks","issue_events_url":"https://api.github.com/repos/freenas/freenas/issues/events{/number}","events_url":"https://api.github.com/repos/freenas/freenas/events","assignees_url":"https://api.github.com/repos/freenas/freenas/assignees{/user}","branches_url":"https://api.github.com/repos/freenas/freenas/branches{/branch}","tags_url":"https://api.github.com/repos/freenas/freenas/tags","blobs_url":"https://api.github.com/repos/freenas/freenas/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/freenas/freenas/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/freenas/freenas/git/refs{/sha}","trees_url":"https://api.github.com/repos/freenas/freenas/git/trees{/sha}","statuses_url":"https://api.github.com/repos/freenas/freenas/statuses/{sha}","languages_url":"https://api.github.com/repos/freenas/freenas/languages","stargazers_url":"https://api.github.com/repos/freenas/freenas/stargazers","contributors_url":"https://api.github.com/repos/freenas/freenas/contributors","subscribers_url":"https://api.github.com/repos/freenas/freenas/subscribers","subscription_url":"https://api.github.com/repos/freenas/freenas/subscription","commits_url":"https://api.github.com/repos/freenas/freenas/commits{/sha}","git_commits_url":"https://api.github.com/repos/freenas/freenas/git/commits{/sha}","comments_url":"https://api.github.com/repos/freenas/freenas/comments{/number}","issue_comment_url":"https://api.github.com/repos/freenas/freenas/issues/comments{/number}","contents_url":"https://api.github.com/repos/freenas/freenas/contents/{+path}","compare_url":"https://api.github.com/repos/freenas/freenas/compare/{base}...{head}","merges_url":"https://api.github.com/repos/freenas/freenas/merges","archive_url":"https://api.github.com/repos/freenas/freenas/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/freenas/freenas/downloads","issues_url":"https://api.github.com/repos/freenas/freenas/issues{/number}","pulls_url":"https://api.github.com/repos/freenas/freenas/pulls{/number}","milestones_url":"https://api.github.com/repos/freenas/freenas/milestones{/number}","notifications_url":"https://api.github.com/repos/freenas/freenas/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/freenas/freenas/labels{/name}","releases_url":"https://api.github.com/repos/freenas/freenas/releases{/id}","deployments_url":"https://api.github.com/repos/freenas/freenas/deployments","created_at":"2013-03-07T13:23:20Z","updated_at":"2018-03-16T00:54:12Z","pushed_at":"2018-03-16T15:29:55Z","git_url":"git://github.com/freenas/freenas.git","ssh_url":"git@github.com:freenas/freenas.git","clone_url":"https://github.com/freenas/freenas.git","svn_url":"https://github.com/freenas/freenas","homepage":"http://www.freenas.org","size":523193,"stargazers_count":902,"watchers_count":902,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":286,"mirror_url":null,"archived":false,"open_issues_count":11,"license":null,"forks":286,"open_issues":11,"watchers":902,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/freenas/freenas/pulls/975"},"html":{"href":"https://github.com/freenas/freenas/pull/975"},"issue":{"href":"https://api.github.com/repos/freenas/freenas/issues/975"},"comments":{"href":"https://api.github.com/repos/freenas/freenas/issues/975/comments"},"review_comments":{"href":"https://api.github.com/repos/freenas/freenas/pulls/975/comments"},"review_comment":{"href":"https://api.github.com/repos/freenas/freenas/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/freenas/freenas/pulls/975/commits"},"statuses":{"href":"https://api.github.com/repos/freenas/freenas/statuses/e2b8462f047f06b955fee3e9be50d2db9c5ba8da"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"themylogin"}}