{"action":"created","comment":{"url":"https://api.github.com/repos/woju/qubes-core-admin/pulls/comments/60732764","id":60732764,"diff_hunk":"@@ -134,370 +116,181 @@ def kernels_dir(self):\n         If :py:attr:`self.vm.kernel` is :py:obj:`None`, the this points inside\n         :py:attr:`self.vm.dir_path`\n         '''\n-        return os.path.join(qubes.config.system_path['qubes_base_dir'],\n-            qubes.config.system_path['qubes_kernels_base_dir'], self.vm.kernel)\\\n-            if self.vm.kernel is not None \\\n-        else os.path.join(self.vm.dir_path,\n-            qubes.config.vm_files['kernels_subdir'])\n-\n-\n-    @property\n-    def modules_img(self):\n-        '''Path to image with modules.\n-\n-        Depending on domain, this may be global or inside domain's dir.\n-        '''\n-\n-        modules_path = os.path.join(self.kernels_dir, 'modules.img')\n-\n-        if os.path.exists(modules_path):\n-            return modules_path\n-        else:\n-            return None\n-\n-\n-    @property\n-    def modules_img_rw(self):\n-        ''':py:obj:`True` if module image should be mounted RW, :py:obj:`False`\n-        otherwise.'''\n-        return self.vm.kernel is None\n-\n-\n-    def abspath(self, path, rel=None):\n-        '''Make absolute path.\n-\n-        If given path is relative, it is interpreted as relative to\n-        :py:attr:`self.vm.dir_path` or given *rel*.\n-        '''\n-        return path if os.path.isabs(path) \\\n-            else os.path.join(rel or self.vm.dir_path, path)\n-\n-\n-    @staticmethod\n-    def _copy_file(source, destination):\n-        '''Effective file copy, preserving sparse files etc.\n-        '''\n-        # TODO: Windows support\n-\n-        # We prefer to use Linux's cp, because it nicely handles sparse files\n-        try:\n-            subprocess.check_call(['cp', '--reflink=auto', source, destination])\n-        except subprocess.CalledProcessError:\n-            raise IOError('Error while copying {!r} to {!r}'.format(\n-                source, destination))\n+        assert 'kernel' in self.vm.volumes, \"VM has no kernel pool\"\n+        return self.vm.volumes['kernel'].kernels_dir\n \n     def get_disk_utilization(self):\n-        return get_disk_usage(self.vm.dir_path)\n+        ''' Returns summed up disk utilization for all domain volumes '''\n+        result = 0\n+        for volume in self.vm.volumes.values():\n+            result += volume.usage\n+        return result\n \n+    # TODO Remove this wrapper\n     def get_disk_utilization_private_img(self):\n-        # pylint: disable=invalid-name\n-        return get_disk_usage(self.private_img)\n+        # pylint: disable=invalid-name,missing-docstring\n+        return self.vm.volume['private'].usage\n \n+    # TODO Remove this wrapper\n     def get_private_img_sz(self):\n-        if not os.path.exists(self.private_img):\n-            return 0\n-\n-        return os.path.getsize(self.private_img)\n-\n-    def resize_private_img(self, size):\n-        raise NotImplementedError()\n-\n-    def create_on_disk_private_img(self, source_template=None):\n-        raise NotImplementedError()\n+        # :pylint: disable=missing-docstring\n+        return self.vm.volume['private'].size\n \n-    def create_on_disk_root_img(self, source_template=None):\n-        raise NotImplementedError()\n+    def resize(self, volume, size):\n+        ''' Resize volume '''\n+        self.get_pool(volume).resize(volume, size)\n \n+    # TODO rename it to create()\n     def create_on_disk(self, source_template=None):\n+        # :pylint: disable=missing-docstring\n         if source_template is None and hasattr(self.vm, 'template'):\n             source_template = self.vm.template\n \n         old_umask = os.umask(002)\n \n-        self.vm.log.info('Creating directory: {0}'.format(self.vm.dir_path))\n-        os.mkdir(self.vm.dir_path)\n-        self.create_on_disk_private_img(source_template)\n-        self.create_on_disk_root_img(source_template)\n-        self.reset_volatile_storage()\n+        self.log.info('Creating directory: {0}'.format(self.vm.dir_path))\n+        os.makedirs(self.vm.dir_path)\n+        for name, volume in self.vm.volumes.items():\n+            source_volume = None\n+            if source_template and hasattr(source_template, 'volumes'):\n+                source_volume = source_template.volumes[name]\n+            self.get_pool(volume).create(volume, source_volume=source_volume)\n \n         os.umask(old_umask)\n \n-    def clone_disk_files(self, src_vm):\n+    def clone(self, src_vm):\n         self.vm.log.info('Creating directory: {0}'.format(self.vm.dir_path))\n-        os.mkdir(self.vm.dir_path)\n-\n-        if hasattr(src_vm, 'private_img'):\n-            self.vm.log.info('Copying the private image: {} -> {}'.format(\n-                src_vm.private_img, self.vm.private_img))\n-            self._copy_file(src_vm.private_img, self.vm.private_img)\n-\n-        if src_vm.updateable and hasattr(src_vm, 'root_img'):\n-            self.vm.log.info('Copying the root image: {} -> {}'.format(\n-                src_vm.root_img, self.root_img))\n-            self._copy_file(src_vm.root_img, self.root_img)\n-\n-            # TODO: modules?\n-            # XXX which modules? -woju\n-\n-\n-    @staticmethod\n-    def rename(newpath, oldpath):\n-        '''Move storage directory, most likely during domain's rename.\n-\n-        .. note::\n-            The arguments are in different order than in :program:`cp` utility.\n+        if not os.path.exists(self.vm.dir_path):\n+            self.log.info('Creating directory: {0}'.format(self.vm.dir_path))\n+            os.makedirs(self.vm.dir_path)\n+        for name, target in self.vm.volumes.items():\n+            pool = self.get_pool(target)\n+            source = src_vm.volumes[name]\n+            volume = pool.clone(source, target)\n+            assert volume, \"%s.clone() returned '%s'\" % (pool.__class__,\n+                                                         volume)\n+            self.vm.volumes[name] = volume\n+\n+    def rename(self, old_name, new_name):\n+        ''' Notify the pools that the domain was renamed '''\n+        volumes = self.vm.volumes\n+        for name, volume in volumes.items():\n+            pool = self.get_pool(volume)\n+            volumes[name] = pool.rename(volume, old_name, new_name)\n \n-        .. versionchange:: 4.0\n-            This is now dummy method that just passes everything to\n-            :py:func:`os.rename`.\n+    def verify_files(self):\n+        '''Verify that the storage is sane.\n \n-        :param str newpath: New path\n-        :param str oldpath: Old path\n+        On success, returns normally. On failure, raises exception.\n         '''\n-\n-        os.rename(oldpath, newpath)\n-\n-\n-    def verify_files(self):\n         if not os.path.exists(self.vm.dir_path):\n-            raise qubes.exc.QubesVMError(self.vm,\n+            raise qubes.exc.QubesVMError(\n+                self.vm,\n                 'VM directory does not exist: {}'.format(self.vm.dir_path))\n \n-        if hasattr(self.vm, 'root_img') and not os.path.exists(self.root_img):\n-            raise qubes.exc.QubesVMError(self.vm,\n-                'VM root image file does not exist: {}'.format(self.root_img))\n-\n-        if hasattr(self.vm, 'private_img') \\\n-                and not os.path.exists(self.private_img):\n-            raise qubes.exc.QubesVMError(self.vm,\n-                'VM private image file does not exist: {}'.format(\n-                    self.private_img))\n-\n-        if self.modules_img is not None \\\n-                and not os.path.exists(self.modules_img):\n-            raise qubes.exc.QubesVMError(self.vm,\n-                'VM kernel modules image does not exists: {}'.format(\n-                    self.modules_img))\n-\n-\n-    def remove_from_disk(self):\n+    def remove(self):\n+        for name, volume in self.vm.volumes.items():\n+            self.log.info('Removing volume %s: %s' % (name, volume.vid))\n+            self.get_pool(volume).remove(volume)\n         shutil.rmtree(self.vm.dir_path)\n \n+    def start(self):\n+        ''' Execute the start method on each pool '''\n+        for volume in self.vm.volumes.values():\n+            self.get_pool(volume).start(volume)\n \n-    def reset_volatile_storage(self):\n-        # Re-create only for template based VMs\n-        try:\n-            if self.vm.template is not None and self.volatile_img:\n-                if os.path.exists(self.volatile_img):\n-                    os.remove(self.volatile_img)\n-        except AttributeError: # self.vm.template\n-            pass\n-\n-        # For StandaloneVM create it only if not already exists\n-        # (eg after backup-restore)\n-        if hasattr(self, 'volatile_img') \\\n-                and not os.path.exists(self.volatile_img):\n-            self.vm.log.info(\n-                'Creating volatile image: {0}'.format(self.volatile_img))\n-            subprocess.check_call(\n-                [qubes.config.system_path[\"prepare_volatile_img_cmd\"],\n-                    self.volatile_img,\n-                    str(self.root_img_size / 1024 / 1024)])\n-\n-\n-    def prepare_for_vm_startup(self):\n-        self.reset_volatile_storage()\n-\n-        if hasattr(self.vm, 'private_img') \\\n-                and not os.path.exists(self.private_img):\n-            self.vm.log.info('Creating empty VM private image file: {0}'.format(\n-                self.private_img))\n-            self.create_on_disk_private_img()\n-\n-\n-def get_disk_usage_one(st):\n-    '''Extract disk usage of one inode from its stat_result struct.\n-\n-    If known, get real disk usage, as written to device by filesystem, not\n-    logical file size. Those values may be different for sparse files.\n-\n-    :param os.stat_result st: stat result\n-    :returns: disk usage\n-    '''\n-    try:\n-        return st.st_blocks * BLKSIZE\n-    except AttributeError:\n-        return st.st_size\n-\n-\n-def get_disk_usage(path):\n-    '''Get real disk usage of given path (file or directory).\n-\n-    When *path* points to directory, then it is evaluated recursively.\n-\n-    This function tries estiate real disk usage. See documentation of\n-    :py:func:`get_disk_usage_one`.\n-\n-    :param str path: path to evaluate\n-    :returns: disk usage\n-    '''\n-    try:\n-        st = os.lstat(path)\n-    except OSError:\n-        return 0\n-\n-    ret = get_disk_usage_one(st)\n-\n-    # if path is not a directory, this is skipped\n-    for dirpath, dirnames, filenames in os.walk(path):\n-        for name in dirnames + filenames:\n-            ret += get_disk_usage_one(os.lstat(os.path.join(dirpath, name)))\n-\n-    return ret\n-\n-\n-def get_pool(name, vm):\n-    \"\"\" Instantiates the storage for the specified vm \"\"\"\n-    config = _get_storage_config_parser()\n-\n-    klass = _get_pool_klass(name, config)\n-\n-    keys = [k for k in config.options(name) if k != 'driver' and k != 'class']\n-    values = [config.get(name, o) for o in keys]\n-    config_kwargs = dict(zip(keys, values))\n+    def stop(self):\n+        ''' Execute the start method on each pool '''\n+        for volume in self.vm.volumes.values():\n+            self.get_pool(volume).stop(volume)\n \n-    if name == 'default':\n-        kwargs = qubes.config.defaults['pool_config'].copy()\n-        kwargs.update(keys)\n-    else:\n-        kwargs = config_kwargs\n+    def get_pool(self, volume):\n+        ''' Helper function '''\n+        assert isinstance(volume, Volume), \"You need to pass a Volume\"\n+        return self.pools[volume.name]\n \n-    return klass(vm, **kwargs)\n+    def commit_template_changes(self):\n+        for volume in self.vm.volumes.values():\n+            if volume.volume_type == 'origin':\n+                self.get_pool(volume).commit_template_changes(volume)\n \n \n-def pool_exists(name):\n-    \"\"\" Check if the specified pool exists \"\"\"\n-    try:\n-        _get_pool_klass(name)\n-        return True\n-    except StoragePoolException:\n-        return False\n-\n-def add_pool(name, **kwargs):\n-    \"\"\" Add a storage pool to config.\"\"\"\n-    config = _get_storage_config_parser()\n-    config.add_section(name)\n-    for key, value in kwargs.iteritems():\n-        config.set(name, key, value)\n-    _write_config(config)\n-\n-def remove_pool(name):\n-    \"\"\" Remove a storage pool from config file.  \"\"\"\n-    config = _get_storage_config_parser()\n-    config.remove_section(name)\n-    _write_config(config)\n-\n-def _write_config(config):\n-    with open(CONFIG_FILE, 'w') as configfile:\n-        config.write(configfile)\n-\n-def _get_storage_config_parser():\n-    \"\"\" Instantiates a `ConfigParaser` for specified storage config file.\n-\n-        Returns:\n-            RawConfigParser\n-    \"\"\"\n-    config = ConfigParser.RawConfigParser()\n-    config.read(CONFIG_FILE)\n-    return config\n-\n-\n-def _get_pool_klass(name, config=None):\n-    \"\"\" Returns the storage klass for the specified pool.\n-\n-        Args:\n-            name: The pool name.\n-            config: If ``config`` is not specified\n-                    `_get_storage_config_parser()` is called.\n-\n-        Returns:\n-            type: A class inheriting from `QubesVmStorage`\n-    \"\"\"\n-    if config is None:\n-        config = _get_storage_config_parser()\n-\n-    if not config.has_section(name):\n-        raise StoragePoolException('Uknown storage pool ' + name)\n-    elif not config.has_option(name, 'driver'):\n-        raise StoragePoolException('No driver specified for pool ' + name)\n-\n-\n-    driver = config.get(name, 'driver')\n-    try:\n-        return qubes.get_entry_point_one(STORAGE_ENTRY_POINT, driver)\n-    except KeyError:\n-        raise StoragePoolException('Driver %s for pool %s' % (driver, name))\n-\n class Pool(object):\n-    def __init__(self, vm, dir_path):\n-        assert vm is not None\n-        assert dir_path is not None\n-\n-        self.vm = vm\n-        self.dir_path = dir_path\n+    ''' A Pool is used to manage different kind of volumes (File\n+        based/LVM/Btrfs/...).\n \n-        self.create_dir_if_not_exists(self.dir_path)\n-\n-        self.vmdir = self.vmdir_path(vm, self.dir_path)\n-\n-        appvms_path = os.path.join(self.dir_path, 'appvms')\n-        self.create_dir_if_not_exists(appvms_path)\n-\n-        servicevms_path = os.path.join(self.dir_path, 'servicevms')\n-        self.create_dir_if_not_exists(servicevms_path)\n-\n-        vm_templates_path = os.path.join(self.dir_path, 'vm-templates')\n-        self.create_dir_if_not_exists(vm_templates_path)\n-\n-    # XXX there is also a class attribute on the domain classes which does\n-    # exactly that -- which one should prevail?\n-    def vmdir_path(self, vm, pool_dir):\n-        \"\"\" Returns the path to vmdir depending on the type of the VM.\n-\n-            The default QubesOS file storage saves the vm images in three\n-            different directories depending on the ``QubesVM`` type:\n-\n-            * ``appvms`` for ``QubesAppVm`` or ``QubesHvm``\n-            * ``vm-templates`` for ``QubesTemplateVm`` or ``QubesTemplateHvm``\n-\n-            Args:\n-                vm: a QubesVM\n-                pool_dir: the root directory of the pool\n-\n-            Returns:\n-                string (str) absolute path to the directory where the vm files\n-                             are stored\n-        \"\"\"\n-        if vm.is_template():\n-            subdir = 'vm-templates'\n-        elif vm.is_disposablevm():\n-            subdir = 'appvms'\n-            return os.path.join(pool_dir, subdir, vm.template.name + '-dvm')\n-        else:\n-            subdir = 'appvms'\n-\n-        return os.path.join(pool_dir, subdir, vm.name)\n+        3rd Parties providing own storage implementations will need to extend\n+        this class.\n+    '''\n+    private_img_size = qubes.config.defaults['private_img_size']\n+    root_img_size = qubes.config.defaults['root_img_size']\n+\n+    def __init__(self, name=None, **kwargs):\n+        # :pylint: disable=unused-argument\n+        assert name, \"Pool name is missing\"\n+        self.name = name\n+        kwargs['name'] = self.name","path":"qubes/storage/__init__.py","position":602,"original_position":602,"commit_id":"38d1ed698163451e46b4265ded3e45f389030b06","original_commit_id":"38d1ed698163451e46b4265ded3e45f389030b06","user":{"login":"kalkin","id":145191,"avatar_url":"https://avatars.githubusercontent.com/u/145191?v=3","gravatar_id":"","url":"https://api.github.com/users/kalkin","html_url":"https://github.com/kalkin","followers_url":"https://api.github.com/users/kalkin/followers","following_url":"https://api.github.com/users/kalkin/following{/other_user}","gists_url":"https://api.github.com/users/kalkin/gists{/gist_id}","starred_url":"https://api.github.com/users/kalkin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kalkin/subscriptions","organizations_url":"https://api.github.com/users/kalkin/orgs","repos_url":"https://api.github.com/users/kalkin/repos","events_url":"https://api.github.com/users/kalkin/events{/privacy}","received_events_url":"https://api.github.com/users/kalkin/received_events","type":"User","site_admin":false},"body":"Yes you are right. I will remove the `assert` and call `super().__init__`","created_at":"2016-04-22T12:49:41Z","updated_at":"2016-04-22T12:49:42Z","html_url":"https://github.com/woju/qubes-core-admin/pull/5#discussion_r60732764","pull_request_url":"https://api.github.com/repos/woju/qubes-core-admin/pulls/5","_links":{"self":{"href":"https://api.github.com/repos/woju/qubes-core-admin/pulls/comments/60732764"},"html":{"href":"https://github.com/woju/qubes-core-admin/pull/5#discussion_r60732764"},"pull_request":{"href":"https://api.github.com/repos/woju/qubes-core-admin/pulls/5"}}},"pull_request":{"url":"https://api.github.com/repos/woju/qubes-core-admin/pulls/5","id":66842601,"html_url":"https://github.com/woju/qubes-core-admin/pull/5","diff_url":"https://github.com/woju/qubes-core-admin/pull/5.diff","patch_url":"https://github.com/woju/qubes-core-admin/pull/5.patch","issue_url":"https://api.github.com/repos/woju/qubes-core-admin/issues/5","number":5,"state":"open","locked":false,"title":"Storage Subsystem Design R4","user":{"login":"kalkin","id":145191,"avatar_url":"https://avatars.githubusercontent.com/u/145191?v=3","gravatar_id":"","url":"https://api.github.com/users/kalkin","html_url":"https://github.com/kalkin","followers_url":"https://api.github.com/users/kalkin/followers","following_url":"https://api.github.com/users/kalkin/following{/other_user}","gists_url":"https://api.github.com/users/kalkin/gists{/gist_id}","starred_url":"https://api.github.com/users/kalkin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kalkin/subscriptions","organizations_url":"https://api.github.com/users/kalkin/orgs","repos_url":"https://api.github.com/users/kalkin/repos","events_url":"https://api.github.com/users/kalkin/events{/privacy}","received_events_url":"https://api.github.com/users/kalkin/received_events","type":"User","site_admin":false},"body":"Storage Subsystem Design R4\r\n\r\nThis PR contains all the discussed storage changes from\r\nQubesOS/qubes-issues#1842 & woju/qubes-core-admin/pull/2 :\r\n\r\n- Pools shouldn't hold a a reference to the vm object.\r\n- The domain reference should only be passed on `Pool.init_volume`\r\n- Rewrote the logic for discovering the root image of an AppVM\r\n\r\nI also squashed and rebased from 80 commits down to more manageable ~40\r\n\r\nThis pr depends on the woju/qubes-core-admin/pull/4","created_at":"2016-04-18T12:10:31Z","updated_at":"2016-04-22T12:49:42Z","closed_at":null,"merged_at":null,"merge_commit_sha":"6000c9161bcc75cf42d8e6fbf38f7da06e352d51","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/woju/qubes-core-admin/pulls/5/commits","review_comments_url":"https://api.github.com/repos/woju/qubes-core-admin/pulls/5/comments","review_comment_url":"https://api.github.com/repos/woju/qubes-core-admin/pulls/comments{/number}","comments_url":"https://api.github.com/repos/woju/qubes-core-admin/issues/5/comments","statuses_url":"https://api.github.com/repos/woju/qubes-core-admin/statuses/38d1ed698163451e46b4265ded3e45f389030b06","head":{"label":"kalkin:storage-subsystem-rewrite","ref":"storage-subsystem-rewrite","sha":"38d1ed698163451e46b4265ded3e45f389030b06","user":{"login":"kalkin","id":145191,"avatar_url":"https://avatars.githubusercontent.com/u/145191?v=3","gravatar_id":"","url":"https://api.github.com/users/kalkin","html_url":"https://github.com/kalkin","followers_url":"https://api.github.com/users/kalkin/followers","following_url":"https://api.github.com/users/kalkin/following{/other_user}","gists_url":"https://api.github.com/users/kalkin/gists{/gist_id}","starred_url":"https://api.github.com/users/kalkin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kalkin/subscriptions","organizations_url":"https://api.github.com/users/kalkin/orgs","repos_url":"https://api.github.com/users/kalkin/repos","events_url":"https://api.github.com/users/kalkin/events{/privacy}","received_events_url":"https://api.github.com/users/kalkin/received_events","type":"User","site_admin":false},"repo":{"id":30782377,"name":"qubes-core-admin","full_name":"kalkin/qubes-core-admin","owner":{"login":"kalkin","id":145191,"avatar_url":"https://avatars.githubusercontent.com/u/145191?v=3","gravatar_id":"","url":"https://api.github.com/users/kalkin","html_url":"https://github.com/kalkin","followers_url":"https://api.github.com/users/kalkin/followers","following_url":"https://api.github.com/users/kalkin/following{/other_user}","gists_url":"https://api.github.com/users/kalkin/gists{/gist_id}","starred_url":"https://api.github.com/users/kalkin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kalkin/subscriptions","organizations_url":"https://api.github.com/users/kalkin/orgs","repos_url":"https://api.github.com/users/kalkin/repos","events_url":"https://api.github.com/users/kalkin/events{/privacy}","received_events_url":"https://api.github.com/users/kalkin/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/kalkin/qubes-core-admin","description":"Qubes component: core-admin","fork":true,"url":"https://api.github.com/repos/kalkin/qubes-core-admin","forks_url":"https://api.github.com/repos/kalkin/qubes-core-admin/forks","keys_url":"https://api.github.com/repos/kalkin/qubes-core-admin/keys{/key_id}","collaborators_url":"https://api.github.com/repos/kalkin/qubes-core-admin/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/kalkin/qubes-core-admin/teams","hooks_url":"https://api.github.com/repos/kalkin/qubes-core-admin/hooks","issue_events_url":"https://api.github.com/repos/kalkin/qubes-core-admin/issues/events{/number}","events_url":"https://api.github.com/repos/kalkin/qubes-core-admin/events","assignees_url":"https://api.github.com/repos/kalkin/qubes-core-admin/assignees{/user}","branches_url":"https://api.github.com/repos/kalkin/qubes-core-admin/branches{/branch}","tags_url":"https://api.github.com/repos/kalkin/qubes-core-admin/tags","blobs_url":"https://api.github.com/repos/kalkin/qubes-core-admin/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/kalkin/qubes-core-admin/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/kalkin/qubes-core-admin/git/refs{/sha}","trees_url":"https://api.github.com/repos/kalkin/qubes-core-admin/git/trees{/sha}","statuses_url":"https://api.github.com/repos/kalkin/qubes-core-admin/statuses/{sha}","languages_url":"https://api.github.com/repos/kalkin/qubes-core-admin/languages","stargazers_url":"https://api.github.com/repos/kalkin/qubes-core-admin/stargazers","contributors_url":"https://api.github.com/repos/kalkin/qubes-core-admin/contributors","subscribers_url":"https://api.github.com/repos/kalkin/qubes-core-admin/subscribers","subscription_url":"https://api.github.com/repos/kalkin/qubes-core-admin/subscription","commits_url":"https://api.github.com/repos/kalkin/qubes-core-admin/commits{/sha}","git_commits_url":"https://api.github.com/repos/kalkin/qubes-core-admin/git/commits{/sha}","comments_url":"https://api.github.com/repos/kalkin/qubes-core-admin/comments{/number}","issue_comment_url":"https://api.github.com/repos/kalkin/qubes-core-admin/issues/comments{/number}","contents_url":"https://api.github.com/repos/kalkin/qubes-core-admin/contents/{+path}","compare_url":"https://api.github.com/repos/kalkin/qubes-core-admin/compare/{base}...{head}","merges_url":"https://api.github.com/repos/kalkin/qubes-core-admin/merges","archive_url":"https://api.github.com/repos/kalkin/qubes-core-admin/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/kalkin/qubes-core-admin/downloads","issues_url":"https://api.github.com/repos/kalkin/qubes-core-admin/issues{/number}","pulls_url":"https://api.github.com/repos/kalkin/qubes-core-admin/pulls{/number}","milestones_url":"https://api.github.com/repos/kalkin/qubes-core-admin/milestones{/number}","notifications_url":"https://api.github.com/repos/kalkin/qubes-core-admin/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/kalkin/qubes-core-admin/labels{/name}","releases_url":"https://api.github.com/repos/kalkin/qubes-core-admin/releases{/id}","deployments_url":"https://api.github.com/repos/kalkin/qubes-core-admin/deployments","created_at":"2015-02-14T00:36:10Z","updated_at":"2016-04-20T12:40:29Z","pushed_at":"2016-04-18T12:18:33Z","git_url":"git://github.com/kalkin/qubes-core-admin.git","ssh_url":"git@github.com:kalkin/qubes-core-admin.git","clone_url":"https://github.com/kalkin/qubes-core-admin.git","svn_url":"https://github.com/kalkin/qubes-core-admin","homepage":null,"size":9902,"stargazers_count":1,"watchers_count":1,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":1,"default_branch":"master"}},"base":{"label":"woju:core3-devel","ref":"core3-devel","sha":"540942de475706143f1c86362e96c9fa6eb461bd","user":{"login":"woju","id":1550337,"avatar_url":"https://avatars.githubusercontent.com/u/1550337?v=3","gravatar_id":"","url":"https://api.github.com/users/woju","html_url":"https://github.com/woju","followers_url":"https://api.github.com/users/woju/followers","following_url":"https://api.github.com/users/woju/following{/other_user}","gists_url":"https://api.github.com/users/woju/gists{/gist_id}","starred_url":"https://api.github.com/users/woju/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/woju/subscriptions","organizations_url":"https://api.github.com/users/woju/orgs","repos_url":"https://api.github.com/users/woju/repos","events_url":"https://api.github.com/users/woju/events{/privacy}","received_events_url":"https://api.github.com/users/woju/received_events","type":"User","site_admin":false},"repo":{"id":31896140,"name":"qubes-core-admin","full_name":"woju/qubes-core-admin","owner":{"login":"woju","id":1550337,"avatar_url":"https://avatars.githubusercontent.com/u/1550337?v=3","gravatar_id":"","url":"https://api.github.com/users/woju","html_url":"https://github.com/woju","followers_url":"https://api.github.com/users/woju/followers","following_url":"https://api.github.com/users/woju/following{/other_user}","gists_url":"https://api.github.com/users/woju/gists{/gist_id}","starred_url":"https://api.github.com/users/woju/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/woju/subscriptions","organizations_url":"https://api.github.com/users/woju/orgs","repos_url":"https://api.github.com/users/woju/repos","events_url":"https://api.github.com/users/woju/events{/privacy}","received_events_url":"https://api.github.com/users/woju/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/woju/qubes-core-admin","description":"Qubes component: core-admin","fork":true,"url":"https://api.github.com/repos/woju/qubes-core-admin","forks_url":"https://api.github.com/repos/woju/qubes-core-admin/forks","keys_url":"https://api.github.com/repos/woju/qubes-core-admin/keys{/key_id}","collaborators_url":"https://api.github.com/repos/woju/qubes-core-admin/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/woju/qubes-core-admin/teams","hooks_url":"https://api.github.com/repos/woju/qubes-core-admin/hooks","issue_events_url":"https://api.github.com/repos/woju/qubes-core-admin/issues/events{/number}","events_url":"https://api.github.com/repos/woju/qubes-core-admin/events","assignees_url":"https://api.github.com/repos/woju/qubes-core-admin/assignees{/user}","branches_url":"https://api.github.com/repos/woju/qubes-core-admin/branches{/branch}","tags_url":"https://api.github.com/repos/woju/qubes-core-admin/tags","blobs_url":"https://api.github.com/repos/woju/qubes-core-admin/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/woju/qubes-core-admin/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/woju/qubes-core-admin/git/refs{/sha}","trees_url":"https://api.github.com/repos/woju/qubes-core-admin/git/trees{/sha}","statuses_url":"https://api.github.com/repos/woju/qubes-core-admin/statuses/{sha}","languages_url":"https://api.github.com/repos/woju/qubes-core-admin/languages","stargazers_url":"https://api.github.com/repos/woju/qubes-core-admin/stargazers","contributors_url":"https://api.github.com/repos/woju/qubes-core-admin/contributors","subscribers_url":"https://api.github.com/repos/woju/qubes-core-admin/subscribers","subscription_url":"https://api.github.com/repos/woju/qubes-core-admin/subscription","commits_url":"https://api.github.com/repos/woju/qubes-core-admin/commits{/sha}","git_commits_url":"https://api.github.com/repos/woju/qubes-core-admin/git/commits{/sha}","comments_url":"https://api.github.com/repos/woju/qubes-core-admin/comments{/number}","issue_comment_url":"https://api.github.com/repos/woju/qubes-core-admin/issues/comments{/number}","contents_url":"https://api.github.com/repos/woju/qubes-core-admin/contents/{+path}","compare_url":"https://api.github.com/repos/woju/qubes-core-admin/compare/{base}...{head}","merges_url":"https://api.github.com/repos/woju/qubes-core-admin/merges","archive_url":"https://api.github.com/repos/woju/qubes-core-admin/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/woju/qubes-core-admin/downloads","issues_url":"https://api.github.com/repos/woju/qubes-core-admin/issues{/number}","pulls_url":"https://api.github.com/repos/woju/qubes-core-admin/pulls{/number}","milestones_url":"https://api.github.com/repos/woju/qubes-core-admin/milestones{/number}","notifications_url":"https://api.github.com/repos/woju/qubes-core-admin/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/woju/qubes-core-admin/labels{/name}","releases_url":"https://api.github.com/repos/woju/qubes-core-admin/releases{/id}","deployments_url":"https://api.github.com/repos/woju/qubes-core-admin/deployments","created_at":"2015-03-09T12:04:24Z","updated_at":"2016-01-29T16:57:09Z","pushed_at":"2016-04-22T09:19:16Z","git_url":"git://github.com/woju/qubes-core-admin.git","ssh_url":"git@github.com:woju/qubes-core-admin.git","clone_url":"https://github.com/woju/qubes-core-admin.git","svn_url":"https://github.com/woju/qubes-core-admin","homepage":null,"size":11800,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":false,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":3,"forks":0,"open_issues":3,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/woju/qubes-core-admin/pulls/5"},"html":{"href":"https://github.com/woju/qubes-core-admin/pull/5"},"issue":{"href":"https://api.github.com/repos/woju/qubes-core-admin/issues/5"},"comments":{"href":"https://api.github.com/repos/woju/qubes-core-admin/issues/5/comments"},"review_comments":{"href":"https://api.github.com/repos/woju/qubes-core-admin/pulls/5/comments"},"review_comment":{"href":"https://api.github.com/repos/woju/qubes-core-admin/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/woju/qubes-core-admin/pulls/5/commits"},"statuses":{"href":"https://api.github.com/repos/woju/qubes-core-admin/statuses/38d1ed698163451e46b4265ded3e45f389030b06"}}}}