├── .flake8 ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── COPYING ├── README.md ├── ci ├── check-flake8.sh ├── check-formatting.sh ├── check-mypy.sh ├── check-nix-files.sh └── shell.nix ├── default.nix ├── env.nix ├── examples └── trivial-virtd.nix ├── nixops_virtd ├── __init__.py ├── backends │ ├── __init__.py │ └── libvirtd.py ├── nix │ ├── default.nix │ └── libvirtd.nix └── plugin.py ├── overrides.nix ├── poetry.lock ├── pyproject.toml ├── release.nix ├── shell.nix └── tests └── functional └── single_machine_libvirtd_base.nix /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, E266, E501, W503 3 | # line length is intentionally set to 80 here because black uses Bugbear 4 | # See https://github.com/psf/black/blob/master/README.md#line-length for more details 5 | max-line-length = 80 6 | max-complexity = 18 7 | select = B,C,E,F,W,T4,B9 8 | 9 | per-file-ignores = 10 | nixops/__main__.py:E402 11 | 12 | # # We need to configure the mypy.ini because the flake8-mypy's default 13 | # # options don't properly override it, so if we don't specify it we get 14 | # # half of the config from mypy.ini and half from flake8-mypy. 15 | # mypy_config = mypy.ini 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [ "master" ] 5 | pull_request: 6 | branches: [ "**" ] 7 | jobs: 8 | parsing: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | - name: Nix 14 | uses: cachix/install-nix-action@v8 15 | - name: Prefetch shell.nix 16 | run: 'nix-shell --run true' 17 | - name: Parsing 18 | run: './ci/check-nix-files.sh' 19 | black: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v2 24 | - name: Nix 25 | uses: cachix/install-nix-action@v8 26 | - name: Prefetch shell.nix 27 | run: 'nix-shell --run true' 28 | - name: Black 29 | run: './ci/check-formatting.sh' 30 | mypy: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v2 35 | - name: Nix 36 | uses: cachix/install-nix-action@v8 37 | - name: Prefetch shell.nix 38 | run: 'nix-shell --run true' 39 | - name: Mypy 40 | run: './ci/check-mypy.sh' 41 | flake8: 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Checkout 45 | uses: actions/checkout@v2 46 | - name: Nix 47 | uses: cachix/install-nix-action@v8 48 | - name: Prefetch shell.nix 49 | run: 'nix-shell --run true' 50 | - name: Mypy 51 | run: './ci/check-flake8.sh' 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | result 2 | result-* 3 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NixOps backend for libvirtd 2 | 3 | NixOps (formerly known as Charon) is a tool for deploying NixOS 4 | machines in a network or cloud. 5 | 6 | * [Manual](https://nixos.org/nixops/manual/) 7 | * [Installation](https://nixos.org/nixops/manual/#chap-installation) / [Hacking](https://nixos.org/nixops/manual/#chap-hacking) 8 | * [Continuous build](http://hydra.nixos.org/jobset/nixops/master#tabs-jobs) 9 | * [Source code](https://github.com/NixOS/nixops) 10 | * [Issue Tracker](https://github.com/NixOS/nixops/issues) 11 | 12 | ## Quick Start 13 | 14 | ### Prepare libvirtd 15 | 16 | In order to use the libvirtd backend, a couple of manual steps need to be 17 | taken. 18 | 19 | *Note:* The libvirtd backend is currently supported only on NixOS. 20 | 21 | Configure your host NixOS machine to enable libvirtd daemon, 22 | add your user to libvirtd group and change firewall not to filter DHCP packets. 23 | 24 | ```nix 25 | virtualisation.libvirtd.enable = true; 26 | users.extraUsers.myuser.extraGroups = [ "libvirtd" ]; 27 | networking.firewall.checkReversePath = false; 28 | ``` 29 | 30 | Next we have to make sure our user has access to create images by executing: 31 | 32 | ```sh 33 | images=/var/lib/libvirt/images 34 | sudo mkdir $images 35 | sudo chgrp libvirtd $images 36 | sudo chmod g+w $images 37 | ``` 38 | 39 | Create the default libvirtd storage pool for root: 40 | 41 | ```sh 42 | sudo virsh pool-define-as default dir --target $images 43 | sudo virsh pool-autostart default 44 | sudo virsh pool-start default 45 | ``` 46 | 47 | ### Deploy the example machine 48 | 49 | Create and deploy the trivial example: 50 | 51 | ```sh 52 | nixops create -d example-libvirtd examples/trivial-virtd.nix 53 | nixops deploy -d example-libvirtd 54 | ``` 55 | 56 | Your new machine doesn't do much by default, but you may connect to it by 57 | running: 58 | 59 | ```sh 60 | nixops ssh -d example-libvirtd machine 61 | ``` 62 | -------------------------------------------------------------------------------- /ci/check-flake8.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #!nix-shell ./shell.nix -i bash 3 | 4 | exec flake8 . 5 | -------------------------------------------------------------------------------- /ci/check-formatting.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #!nix-shell ./shell.nix -i bash 3 | 4 | black . --check --diff 5 | -------------------------------------------------------------------------------- /ci/check-mypy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #!nix-shell ./shell.nix -i bash 3 | 4 | mypy nixops_virtd 5 | -------------------------------------------------------------------------------- /ci/check-nix-files.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | find . -name "*.nix" -exec nix-instantiate --parse --quiet {} >/dev/null + 4 | -------------------------------------------------------------------------------- /ci/shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | pkgs.mkShell { 4 | 5 | buildInputs = [ 6 | (pkgs.poetry2nix.mkPoetryEnv { 7 | projectDir = ../.; 8 | overrides = pkgs.poetry2nix.overrides.withDefaults (import ../overrides.nix { inherit pkgs; }); 9 | }) 10 | ]; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | let 3 | overrides = import ./overrides.nix { inherit pkgs; }; 4 | in pkgs.poetry2nix.mkPoetryApplication { 5 | projectDir = ./.; 6 | overrides = pkgs.poetry2nix.overrides.withDefaults overrides; 7 | } 8 | -------------------------------------------------------------------------------- /env.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | let 3 | overrides = import ./overrides.nix { inherit pkgs; }; 4 | in pkgs.poetry2nix.mkPoetryEnv { 5 | projectDir = ./.; 6 | overrides = pkgs.poetry2nix.overrides.withDefaults overrides; 7 | } 8 | -------------------------------------------------------------------------------- /examples/trivial-virtd.nix: -------------------------------------------------------------------------------- 1 | { 2 | network.description = "Example Machine"; 3 | machine = 4 | { deployment.targetEnv = "libvirtd"; 5 | deployment.libvirtd.imageDir = "/var/lib/libvirt/images"; 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /nixops_virtd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nix-community/nixops-libvirtd/b59424bf53e74200d684a4bce1ae64d276e793a0/nixops_virtd/__init__.py -------------------------------------------------------------------------------- /nixops_virtd/backends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nix-community/nixops-libvirtd/b59424bf53e74200d684a4bce1ae64d276e793a0/nixops_virtd/backends/__init__.py -------------------------------------------------------------------------------- /nixops_virtd/backends/libvirtd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import copy 4 | import json 5 | import os 6 | import random 7 | import time 8 | from xml.etree import ElementTree 9 | 10 | # No type stubs for libvirt 11 | import libvirt # type: ignore 12 | 13 | import nixops.known_hosts 14 | import nixops.util 15 | 16 | 17 | # to prevent libvirt errors from appearing on screen, see 18 | # https://www.redhat.com/archives/libvirt-users/2017-August/msg00011.html 19 | 20 | 21 | from nixops.evaluation import eval_network 22 | from nixops.resources import ResourceOptions 23 | from nixops.backends import MachineOptions, MachineDefinition, MachineState 24 | from nixops.plugins.manager import PluginManager 25 | from typing import Optional 26 | from typing import Sequence 27 | 28 | 29 | class LibvirtdOptions(ResourceOptions): 30 | URI: str 31 | baseImage: Optional[str] 32 | baseImageSize: int 33 | cmdline: str 34 | domainType: str 35 | extraDevicesXML: str 36 | extraDomainXML: str 37 | headless: bool 38 | initrd: str 39 | kernel: str 40 | memorySize: int 41 | networks: Sequence[str] 42 | storagePool: str 43 | vcpu: int 44 | 45 | 46 | class LibvirtMachineOptions(MachineOptions): 47 | libvirtd: LibvirtdOptions 48 | 49 | 50 | class LibvirtdDefinition(MachineDefinition): 51 | """Definition of a trivial machine.""" 52 | 53 | config: LibvirtMachineOptions 54 | 55 | @classmethod 56 | def get_type(cls): 57 | return "libvirtd" 58 | 59 | def __init__(self, name, config): 60 | super().__init__(name, config) 61 | self.vcpu = self.config.libvirtd.vcpu 62 | self.memory_size = self.config.libvirtd.memorySize 63 | self.extra_devices = self.config.libvirtd.extraDevicesXML 64 | self.extra_domain = self.config.libvirtd.extraDomainXML 65 | self.headless = self.config.libvirtd.headless 66 | self.domain_type = self.config.libvirtd.domainType 67 | self.kernel = self.config.libvirtd.kernel 68 | self.initrd = self.config.libvirtd.initrd 69 | self.cmdline = self.config.libvirtd.cmdline 70 | self.storage_pool_name = self.config.libvirtd.storagePool 71 | self.uri = self.config.libvirtd.URI 72 | 73 | self.networks = list(self.config.libvirtd.networks) 74 | assert len(self.networks) > 0 75 | 76 | 77 | class LibvirtdState(MachineState[LibvirtdDefinition]): 78 | private_ipv4 = nixops.util.attr_property("privateIpv4", None) 79 | client_public_key = nixops.util.attr_property("libvirtd.clientPublicKey", None) 80 | client_private_key = nixops.util.attr_property("libvirtd.clientPrivateKey", None) 81 | primary_net = nixops.util.attr_property("libvirtd.primaryNet", None) 82 | primary_mac = nixops.util.attr_property("libvirtd.primaryMAC", None) 83 | domain_xml = nixops.util.attr_property("libvirtd.domainXML", None) 84 | disk_path = nixops.util.attr_property("libvirtd.diskPath", None) 85 | storage_volume_name = nixops.util.attr_property("libvirtd.storageVolume", None) 86 | storage_pool_name = nixops.util.attr_property("libvirtd.storagePool", None) 87 | vcpu = nixops.util.attr_property("libvirtd.vcpu", None) 88 | 89 | # older deployments may not have a libvirtd.URI attribute in the state file 90 | # using qemu:///system in such case 91 | uri = nixops.util.attr_property("libvirtd.URI", "qemu:///system") 92 | 93 | @classmethod 94 | def get_type(cls): 95 | return "libvirtd" 96 | 97 | def __init__(self, depl, name, id): 98 | MachineState.__init__(self, depl, name, id) 99 | self._conn = None 100 | self._dom = None 101 | self._pool = None 102 | self._vol = None 103 | 104 | @property 105 | def conn(self): 106 | if self._conn is None: 107 | self.logger.log("Connecting to {}...".format(self.uri)) 108 | try: 109 | self._conn = libvirt.open(self.uri) 110 | except libvirt.libvirtError as error: 111 | self.logger.error(error.get_error_message()) 112 | if error.get_error_code() == libvirt.VIR_ERR_NO_CONNECT: 113 | # this error code usually means "no connection driver available for qemu:///..." 114 | self.logger.error( 115 | "make sure qemu-system-x86_64 is installed on the target host" 116 | ) 117 | raise Exception( 118 | "Failed to connect to the hypervisor at {}".format(self.uri) 119 | ) 120 | return self._conn 121 | 122 | @property 123 | def dom(self): 124 | if self._dom is None: 125 | try: 126 | self._dom = self.conn.lookupByName(self._vm_id()) 127 | except Exception as e: 128 | self.log("Warning: %s" % e) 129 | return self._dom 130 | 131 | @property 132 | def pool(self): 133 | if self._pool is None: 134 | self._pool = self.conn.storagePoolLookupByName(self.storage_pool_name) 135 | return self._pool 136 | 137 | @property 138 | def vol(self): 139 | if self._vol is None: 140 | self._vol = self.pool.storageVolLookupByName(self.storage_volume_name) 141 | return self._vol 142 | 143 | def get_console_output(self): 144 | import sys 145 | 146 | return self._logged_exec( 147 | ["virsh", "-c", self.uri, "console", self.vm_id.decode()], stdin=sys.stdin 148 | ) 149 | 150 | def get_ssh_private_key_file(self): 151 | return self._ssh_private_key_file or self.write_ssh_private_key( 152 | self.client_private_key 153 | ) 154 | 155 | def get_ssh_flags(self, *args, **kwargs): 156 | super_flags = super(LibvirtdState, self).get_ssh_flags(*args, **kwargs) 157 | return super_flags + [ 158 | "-o", 159 | "StrictHostKeyChecking=accept-new", 160 | "-i", 161 | self.get_ssh_private_key_file(), 162 | ] 163 | 164 | def get_physical_spec(self): 165 | return { 166 | ("users", "extraUsers", "root", "openssh", "authorizedKeys", "keys"): [ 167 | self.client_public_key 168 | ] 169 | } 170 | 171 | def address_to(self, m): 172 | if isinstance(m, LibvirtdState): 173 | return m.private_ipv4 174 | return MachineState.address_to(self, m) 175 | 176 | def _vm_id(self): 177 | return "nixops-{0}-{1}".format(self.depl.uuid, self.name) 178 | 179 | def _generate_primary_mac(self): 180 | mac = [ 181 | 0x52, 182 | 0x54, 183 | 0x00, 184 | random.randint(0x00, 0x7F), 185 | random.randint(0x00, 0xFF), 186 | random.randint(0x00, 0xFF), 187 | ] 188 | self.primary_mac = ":".join(["%02x" % x for x in mac]) 189 | 190 | def create(self, defn, check, allow_reboot, allow_recreate): 191 | assert isinstance(defn, LibvirtdDefinition) 192 | self.set_common_state(defn) 193 | self.primary_net = defn.networks[0] 194 | self.storage_pool_name = defn.storage_pool_name 195 | self.uri = defn.uri 196 | 197 | # required for virConnectGetDomainCapabilities() 198 | # https://libvirt.org/formatdomaincaps.html 199 | if self.conn.getLibVersion() < 1002007: 200 | raise Exception("libvirt 1.2.7 or newer is required at the target host") 201 | 202 | if not self.primary_mac: 203 | self._generate_primary_mac() 204 | 205 | if not self.client_public_key: 206 | ( 207 | self.client_private_key, 208 | self.client_public_key, 209 | ) = nixops.util.create_key_pair() 210 | 211 | if self.storage_volume_name is None: 212 | self._prepare_storage_volume() 213 | self.storage_volume_name = self.vol.name() 214 | 215 | self.domain_xml = self._make_domain_xml(defn) 216 | 217 | if self.vm_id is None: 218 | # By using "define" we ensure that the domain is 219 | # "persistent", as opposed to "transient" (i.e. removed on reboot). 220 | self._dom = self.conn.defineXML(self.domain_xml) 221 | if self._dom is None: 222 | self.log("Failed to register domain XML with the hypervisor") 223 | return False 224 | 225 | self.vm_id = self._vm_id() 226 | 227 | self.start() 228 | return True 229 | 230 | def _prepare_storage_volume(self): 231 | self.logger.log("preparing disk image...") 232 | newEnv = copy.deepcopy(os.environ) 233 | newEnv["NIXOPS_LIBVIRTD_PUBKEY"] = self.client_public_key 234 | 235 | temp_image_path = nixops.evaluation.eval( 236 | networkExpr=self.depl.network_expr, 237 | uuid=self.depl.uuid, 238 | deploymentName=self.depl.name or "", 239 | checkConfigurationOptions=False, 240 | attr="nodes.{0}.config.deployment.libvirtd.baseImage".format(self.name), 241 | pluginNixExprs=PluginManager.nixexprs(), 242 | build=True 243 | ) 244 | 245 | temp_disk_path = os.path.join(temp_image_path, "nixos.qcow2") 246 | 247 | self.logger.log("uploading disk image...") 248 | image_info = self._get_image_info(temp_disk_path) 249 | self._vol = self._create_volume( 250 | image_info["virtual-size"], image_info["file-length"] 251 | ) 252 | self._upload_volume(temp_disk_path, image_info["file-length"]) 253 | 254 | def _get_image_info(self, filename): 255 | output = self._logged_exec( 256 | ["qemu-img", "info", "--output", "json", filename], capture_stdout=True 257 | ) 258 | 259 | info = json.loads(output) 260 | info["file-length"] = os.stat(filename).st_size 261 | 262 | return info 263 | 264 | def _create_volume(self, virtual_size, file_length): 265 | xml = """ 266 | 267 | {name} 268 | {virtual_size} 269 | {file_length} 270 | 271 | 272 | 273 | 274 | """.format( 275 | name="{}.qcow2".format(self._vm_id()), 276 | virtual_size=virtual_size, 277 | file_length=file_length, 278 | ) 279 | vol = self.pool.createXML(xml) 280 | self._vol = vol 281 | return vol 282 | 283 | def _upload_volume(self, filename, file_length): 284 | stream = self.conn.newStream() 285 | self.vol.upload(stream, offset=0, length=file_length) 286 | 287 | def read_file(stream, nbytes, f): 288 | return f.read(nbytes) 289 | 290 | with open(filename, "rb") as f: 291 | stream.sendAll(read_file, f) 292 | stream.finish() 293 | 294 | def _get_qemu_executable(self): 295 | domaincaps_xml = self.conn.getDomainCapabilities( 296 | emulatorbin=None, 297 | arch="x86_64", 298 | machine=None, 299 | virttype="kvm", 300 | ) 301 | domaincaps = ElementTree.fromstring(domaincaps_xml) 302 | return domaincaps.find("./path").text.strip() 303 | 304 | def _make_domain_xml(self, defn): 305 | qemu = self._get_qemu_executable() 306 | 307 | def maybe_mac(n): 308 | if n == self.primary_net: 309 | return '' 310 | else: 311 | return "" 312 | 313 | def iface(n): 314 | return "\n".join( 315 | [ 316 | ' ', 317 | maybe_mac(n), 318 | ' ', 319 | " ", 320 | ] 321 | ).format(n) 322 | 323 | def _make_os(defn): 324 | return [ 325 | "", 326 | ' hvm', 327 | " %s" % defn.kernel, 328 | " %s" % defn.initrd if len(defn.kernel) > 0 else "", 329 | " %s" % defn.cmdline 330 | if len(defn.kernel) > 0 331 | else "", 332 | "", 333 | ] 334 | 335 | domain_fmt = "\n".join( 336 | [ 337 | '', 338 | " {0}", 339 | ' {1}', 340 | " {4}", 341 | "\n".join(_make_os(defn)), 342 | " ", 343 | " {2}", 344 | ' ', 345 | ' ', 346 | ' ', 347 | ' ', 348 | " ", 349 | "\n".join([iface(n) for n in defn.networks]), 350 | ' ' 351 | if not defn.headless 352 | else "", 353 | ' ', 354 | ' ', 355 | defn.extra_devices, 356 | " ", 357 | defn.extra_domain, 358 | "", 359 | ] 360 | ) 361 | 362 | return domain_fmt.format( 363 | self._vm_id(), 364 | defn.memory_size, 365 | qemu, 366 | self.vol.path(), 367 | defn.vcpu, 368 | defn.domain_type, 369 | ) 370 | 371 | def _parse_ip(self): 372 | """ 373 | return an ip v4 374 | """ 375 | # alternative is VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE if qemu agent is available 376 | ifaces = self.dom.interfaceAddresses( 377 | libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE, 0 378 | ) 379 | if ifaces is None: 380 | self.log("Failed to get domain interfaces") 381 | return 382 | 383 | for (name, val) in ifaces.items(): 384 | if val["addrs"]: 385 | for ipaddr in val["addrs"]: 386 | return ipaddr["addr"] 387 | 388 | def _wait_for_ip(self, prev_time): 389 | self.log_start("waiting for IP address to appear in DHCP leases...") 390 | while True: 391 | ip = self._parse_ip() 392 | if ip: 393 | self.private_ipv4 = ip 394 | break 395 | time.sleep(1) 396 | self.log_continue(".") 397 | self.log_end(" " + self.private_ipv4) 398 | 399 | def _is_running(self): 400 | try: 401 | return self.dom.isActive() 402 | except libvirt.libvirtError: 403 | self.log("Domain %s is not running" % self.vm_id) 404 | return False 405 | 406 | def start(self): 407 | assert self.vm_id 408 | assert self.domain_xml 409 | assert self.primary_net 410 | if self._is_running(): 411 | self.log("connecting...") 412 | self.private_ipv4 = self._parse_ip() 413 | else: 414 | self.log("starting...") 415 | self.dom.create() 416 | self._wait_for_ip(0) 417 | 418 | def get_ssh_name(self): 419 | self.private_ipv4 = self._parse_ip() 420 | return self.private_ipv4 421 | 422 | def stop(self): 423 | assert self.vm_id 424 | if self._is_running(): 425 | self.log_start("shutting down... ") 426 | if self.dom.destroy() != 0: 427 | self.log("Failed destroying machine") 428 | else: 429 | self.log("not running") 430 | self.state = self.STOPPED 431 | 432 | def destroy(self, wipe=False): 433 | self.log_start("destroying... ") 434 | 435 | if self.vm_id is not None: 436 | self.stop() 437 | if self.dom.undefine() != 0: 438 | self.log("Failed undefining domain") 439 | return False 440 | 441 | if self.disk_path and os.path.exists(self.disk_path): 442 | # the deployment was created by an older NixOps version that did 443 | # not use the libvirtd API for uploading disk images 444 | os.unlink(self.disk_path) 445 | 446 | if self.storage_volume_name is not None: 447 | self.vol.delete() 448 | 449 | return True 450 | -------------------------------------------------------------------------------- /nixops_virtd/nix/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | config_exporters = { optionalAttrs, ... }: [ 3 | (config: { libvirtd = optionalAttrs (config.deployment.targetEnv == "libvirtd") config.deployment.libvirtd; }) 4 | ]; 5 | options = [ 6 | ./libvirtd.nix 7 | ]; 8 | resources = { ... }: {}; 9 | } 10 | -------------------------------------------------------------------------------- /nixops_virtd/nix/libvirtd.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, lib, ... }: 2 | 3 | with lib; 4 | 5 | let 6 | the_key = builtins.getEnv "NIXOPS_LIBVIRTD_PUBKEY"; 7 | ssh_image = import { 8 | name = "libvirtd-ssh-image"; 9 | format = "qcow2"; 10 | diskSize = config.deployment.libvirtd.baseImageSize * 1024; 11 | config = config; 12 | contents = [{ 13 | source = (pkgs.writeText "authorized_keys.d-root" the_key); 14 | target = "/etc/ssh/authorized_keys.d/root"; 15 | }]; 16 | lib = pkgs.lib; 17 | inherit pkgs; 18 | }; 19 | in 20 | 21 | { 22 | 23 | ###### interface 24 | 25 | options = { 26 | deployment.libvirtd.storagePool = mkOption { 27 | type = types.str; 28 | default = "default"; 29 | description = '' 30 | The storage pool where the virtual disk is be created. 31 | ''; 32 | }; 33 | 34 | deployment.libvirtd.URI = mkOption { 35 | type = types.str; 36 | default = "qemu:///system"; 37 | description = '' 38 | Connection URI. 39 | ''; 40 | }; 41 | 42 | deployment.libvirtd.vcpu = mkOption { 43 | default = 1; 44 | type = types.int; 45 | description = '' 46 | Number of Virtual CPUs. 47 | ''; 48 | }; 49 | 50 | deployment.libvirtd.memorySize = mkOption { 51 | default = 512; 52 | type = types.int; 53 | description = '' 54 | Memory size (M) of virtual machine. 55 | ''; 56 | }; 57 | 58 | deployment.libvirtd.headless = mkOption { 59 | type = types.bool; 60 | default = false; 61 | description = '' 62 | If set VM is started in headless mode, 63 | i.e., without a visible display on the host's desktop. 64 | ''; 65 | }; 66 | 67 | deployment.libvirtd.baseImageSize = mkOption { 68 | default = 10; 69 | type = types.int; 70 | description = '' 71 | The size (G) of base image of virtual machine. 72 | ''; 73 | }; 74 | 75 | deployment.libvirtd.baseImage = mkOption { 76 | default = null; 77 | example = "/home/alice/base-disk.qcow2"; 78 | type = with types; nullOr path; 79 | description = '' 80 | The disk is created using the specified 81 | disk image as a base. 82 | ''; 83 | }; 84 | 85 | deployment.libvirtd.networks = mkOption { 86 | default = [ "default" ]; 87 | type = types.listOf types.str; 88 | description = "Names of libvirt networks to attach the VM to."; 89 | }; 90 | 91 | deployment.libvirtd.extraDevicesXML = mkOption { 92 | default = ""; 93 | type = types.str; 94 | description = "Additional XML appended at the end of device tag in domain xml. See https://libvirt.org/formatdomain.html"; 95 | }; 96 | 97 | deployment.libvirtd.extraDomainXML = mkOption { 98 | default = ""; 99 | type = types.str; 100 | description = "Additional XML appended at the end of domain xml. See https://libvirt.org/formatdomain.html"; 101 | }; 102 | 103 | deployment.libvirtd.domainType = mkOption { 104 | default = "kvm"; 105 | type = types.str; 106 | description = "Specify the type of libvirt domain to create (see '$ virsh capabilities | grep domain' for valid domain types"; 107 | }; 108 | 109 | deployment.libvirtd.cmdline = mkOption { 110 | default = ""; 111 | type = types.str; 112 | description = "Specify the kernel cmdline (valid only with the kernel setting)."; 113 | }; 114 | 115 | deployment.libvirtd.initrd = mkOption { 116 | default = ""; 117 | type = types.str; 118 | description = "Specify the kernel initrd (valid only with the kernel setting)."; 119 | }; 120 | 121 | deployment.libvirtd.kernel = mkOption { 122 | default = ""; 123 | type = types.str; # with types; nullOr path; 124 | description = "Specify the host kernel to launch (valid for kvm)."; 125 | }; 126 | }; 127 | 128 | ###### implementation 129 | 130 | config = mkIf (config.deployment.targetEnv == "libvirtd") { 131 | deployment.libvirtd.baseImage = mkDefault ssh_image; 132 | 133 | nixpkgs.system = mkOverride 900 "x86_64-linux"; 134 | 135 | fileSystems."/".device = "/dev/disk/by-label/nixos"; 136 | 137 | boot.loader.grub.version = 2; 138 | boot.loader.grub.device = "/dev/sda"; 139 | boot.loader.timeout = 0; 140 | 141 | services.openssh.enable = true; 142 | services.openssh.startWhenNeeded = false; 143 | services.openssh.extraConfig = "UseDNS no"; 144 | 145 | deployment.hasFastConnection = true; 146 | }; 147 | 148 | } 149 | -------------------------------------------------------------------------------- /nixops_virtd/plugin.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import nixops.plugins 3 | from nixops.plugins import Plugin 4 | 5 | 6 | class NixopsLibvirtdPlugin(Plugin): 7 | @staticmethod 8 | def nixexprs(): 9 | return [os.path.dirname(os.path.abspath(__file__)) + "/nix"] 10 | 11 | @staticmethod 12 | def load(): 13 | return [ 14 | "nixops_virtd.backends.libvirtd", 15 | ] 16 | 17 | 18 | @nixops.plugins.hookimpl 19 | def plugin(): 20 | return NixopsLibvirtdPlugin() 21 | -------------------------------------------------------------------------------- /overrides.nix: -------------------------------------------------------------------------------- 1 | { pkgs }: 2 | 3 | self: super: { 4 | nixops = super.nixops.overridePythonAttrs({ nativeBuildInputs ? [], ... }: { 5 | format = "pyproject"; 6 | nativeBuildInputs = nativeBuildInputs ++ [ self.poetry ]; 7 | }); 8 | 9 | libvirt-python = super.libvirt-python.overridePythonAttrs({ nativeBuildInputs ? [], ... }: { 10 | format = "pyproject"; 11 | nativeBuildInputs = nativeBuildInputs ++ [ pkgs.pkgconfig ]; 12 | propagatedBuildInputs = [ pkgs.libvirt ]; 13 | }); 14 | 15 | pathspec = super.pathspec.overridePythonAttrs({ nativeBuildInputs ? [], ... }: { 16 | nativeBuildInputs = nativeBuildInputs ++ [ self.flit-core ]; 17 | }); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "black" 5 | version = "22.12.0" 6 | description = "The uncompromising code formatter." 7 | category = "dev" 8 | optional = false 9 | python-versions = ">=3.7" 10 | files = [ 11 | {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, 12 | {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, 13 | {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, 14 | {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, 15 | {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, 16 | {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, 17 | {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, 18 | {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, 19 | {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, 20 | {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, 21 | {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, 22 | {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, 23 | ] 24 | 25 | [package.dependencies] 26 | click = ">=8.0.0" 27 | mypy-extensions = ">=0.4.3" 28 | pathspec = ">=0.9.0" 29 | platformdirs = ">=2" 30 | tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} 31 | 32 | [package.extras] 33 | colorama = ["colorama (>=0.4.3)"] 34 | d = ["aiohttp (>=3.7.4)"] 35 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 36 | uvloop = ["uvloop (>=0.15.2)"] 37 | 38 | [[package]] 39 | name = "click" 40 | version = "8.1.3" 41 | description = "Composable command line interface toolkit" 42 | category = "dev" 43 | optional = false 44 | python-versions = ">=3.7" 45 | files = [ 46 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 47 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 48 | ] 49 | 50 | [package.dependencies] 51 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 52 | 53 | [[package]] 54 | name = "colorama" 55 | version = "0.4.6" 56 | description = "Cross-platform colored terminal text." 57 | category = "dev" 58 | optional = false 59 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 60 | files = [ 61 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 62 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 63 | ] 64 | 65 | [[package]] 66 | name = "flake8" 67 | version = "3.9.2" 68 | description = "the modular source code checker: pep8 pyflakes and co" 69 | category = "dev" 70 | optional = false 71 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 72 | files = [ 73 | {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, 74 | {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, 75 | ] 76 | 77 | [package.dependencies] 78 | mccabe = ">=0.6.0,<0.7.0" 79 | pycodestyle = ">=2.7.0,<2.8.0" 80 | pyflakes = ">=2.3.0,<2.4.0" 81 | 82 | [[package]] 83 | name = "libvirt-python" 84 | version = "9.0.0" 85 | description = "The libvirt virtualization API python binding" 86 | category = "main" 87 | optional = false 88 | python-versions = "*" 89 | files = [ 90 | {file = "libvirt-python-9.0.0.tar.gz", hash = "sha256:49702d33fa8cbcae19fa727467a69f7ae2241b3091324085ca1cc752b2b414ce"}, 91 | ] 92 | 93 | [[package]] 94 | name = "mccabe" 95 | version = "0.6.1" 96 | description = "McCabe checker, plugin for flake8" 97 | category = "dev" 98 | optional = false 99 | python-versions = "*" 100 | files = [ 101 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 102 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 103 | ] 104 | 105 | [[package]] 106 | name = "mypy" 107 | version = "0.961" 108 | description = "Optional static typing for Python" 109 | category = "dev" 110 | optional = false 111 | python-versions = ">=3.6" 112 | files = [ 113 | {file = "mypy-0.961-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:697540876638ce349b01b6786bc6094ccdaba88af446a9abb967293ce6eaa2b0"}, 114 | {file = "mypy-0.961-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b117650592e1782819829605a193360a08aa99f1fc23d1d71e1a75a142dc7e15"}, 115 | {file = "mypy-0.961-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bdd5ca340beffb8c44cb9dc26697628d1b88c6bddf5c2f6eb308c46f269bb6f3"}, 116 | {file = "mypy-0.961-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3e09f1f983a71d0672bbc97ae33ee3709d10c779beb613febc36805a6e28bb4e"}, 117 | {file = "mypy-0.961-cp310-cp310-win_amd64.whl", hash = "sha256:e999229b9f3198c0c880d5e269f9f8129c8862451ce53a011326cad38b9ccd24"}, 118 | {file = "mypy-0.961-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b24be97351084b11582fef18d79004b3e4db572219deee0212078f7cf6352723"}, 119 | {file = "mypy-0.961-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f4a21d01fc0ba4e31d82f0fff195682e29f9401a8bdb7173891070eb260aeb3b"}, 120 | {file = "mypy-0.961-cp36-cp36m-win_amd64.whl", hash = "sha256:439c726a3b3da7ca84a0199a8ab444cd8896d95012c4a6c4a0d808e3147abf5d"}, 121 | {file = "mypy-0.961-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5a0b53747f713f490affdceef835d8f0cb7285187a6a44c33821b6d1f46ed813"}, 122 | {file = "mypy-0.961-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e9f70df36405c25cc530a86eeda1e0867863d9471fe76d1273c783df3d35c2e"}, 123 | {file = "mypy-0.961-cp37-cp37m-win_amd64.whl", hash = "sha256:b88f784e9e35dcaa075519096dc947a388319cb86811b6af621e3523980f1c8a"}, 124 | {file = "mypy-0.961-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d5aaf1edaa7692490f72bdb9fbd941fbf2e201713523bdb3f4038be0af8846c6"}, 125 | {file = "mypy-0.961-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9f5f5a74085d9a81a1f9c78081d60a0040c3efb3f28e5c9912b900adf59a16e6"}, 126 | {file = "mypy-0.961-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f4b794db44168a4fc886e3450201365c9526a522c46ba089b55e1f11c163750d"}, 127 | {file = "mypy-0.961-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:64759a273d590040a592e0f4186539858c948302c653c2eac840c7a3cd29e51b"}, 128 | {file = "mypy-0.961-cp38-cp38-win_amd64.whl", hash = "sha256:63e85a03770ebf403291ec50097954cc5caf2a9205c888ce3a61bd3f82e17569"}, 129 | {file = "mypy-0.961-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f1332964963d4832a94bebc10f13d3279be3ce8f6c64da563d6ee6e2eeda932"}, 130 | {file = "mypy-0.961-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:006be38474216b833eca29ff6b73e143386f352e10e9c2fbe76aa8549e5554f5"}, 131 | {file = "mypy-0.961-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9940e6916ed9371809b35b2154baf1f684acba935cd09928952310fbddaba648"}, 132 | {file = "mypy-0.961-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a5ea0875a049de1b63b972456542f04643daf320d27dc592d7c3d9cd5d9bf950"}, 133 | {file = "mypy-0.961-cp39-cp39-win_amd64.whl", hash = "sha256:1ece702f29270ec6af25db8cf6185c04c02311c6bb21a69f423d40e527b75c56"}, 134 | {file = "mypy-0.961-py3-none-any.whl", hash = "sha256:03c6cc893e7563e7b2949b969e63f02c000b32502a1b4d1314cabe391aa87d66"}, 135 | {file = "mypy-0.961.tar.gz", hash = "sha256:f730d56cb924d371c26b8eaddeea3cc07d78ff51c521c6d04899ac6904b75492"}, 136 | ] 137 | 138 | [package.dependencies] 139 | mypy-extensions = ">=0.4.3" 140 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 141 | typing-extensions = ">=3.10" 142 | 143 | [package.extras] 144 | dmypy = ["psutil (>=4.0)"] 145 | python2 = ["typed-ast (>=1.4.0,<2)"] 146 | reports = ["lxml"] 147 | 148 | [[package]] 149 | name = "mypy-extensions" 150 | version = "1.0.0" 151 | description = "Type system extensions for programs checked with the mypy type checker." 152 | category = "dev" 153 | optional = false 154 | python-versions = ">=3.5" 155 | files = [ 156 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 157 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 158 | ] 159 | 160 | [[package]] 161 | name = "nixops" 162 | version = "2.0.0" 163 | description = "NixOS cloud provisioning and deployment tool" 164 | category = "main" 165 | optional = false 166 | python-versions = "^3.10" 167 | files = [] 168 | develop = false 169 | 170 | [package.dependencies] 171 | pluggy = "^1.0.0" 172 | PrettyTable = "^0.7.2" 173 | typeguard = "^2.7.1" 174 | typing-extensions = "^3.7.4" 175 | 176 | [package.source] 177 | type = "git" 178 | url = "https://github.com/NixOS/nixops.git" 179 | reference = "HEAD" 180 | resolved_reference = "5013072c5ca34247d7dce545c3a7b1954948fd4d" 181 | 182 | [[package]] 183 | name = "nose" 184 | version = "1.3.7" 185 | description = "nose extends unittest to make testing easier" 186 | category = "dev" 187 | optional = false 188 | python-versions = "*" 189 | files = [ 190 | {file = "nose-1.3.7-py2-none-any.whl", hash = "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a"}, 191 | {file = "nose-1.3.7-py3-none-any.whl", hash = "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac"}, 192 | {file = "nose-1.3.7.tar.gz", hash = "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98"}, 193 | ] 194 | 195 | [[package]] 196 | name = "pathspec" 197 | version = "0.11.0" 198 | description = "Utility library for gitignore style pattern matching of file paths." 199 | category = "dev" 200 | optional = false 201 | python-versions = ">=3.7" 202 | files = [ 203 | {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, 204 | {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, 205 | ] 206 | 207 | [[package]] 208 | name = "platformdirs" 209 | version = "3.0.0" 210 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 211 | category = "dev" 212 | optional = false 213 | python-versions = ">=3.7" 214 | files = [ 215 | {file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"}, 216 | {file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"}, 217 | ] 218 | 219 | [package.extras] 220 | docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] 221 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] 222 | 223 | [[package]] 224 | name = "pluggy" 225 | version = "1.0.0" 226 | description = "plugin and hook calling mechanisms for python" 227 | category = "main" 228 | optional = false 229 | python-versions = ">=3.6" 230 | files = [ 231 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 232 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 233 | ] 234 | 235 | [package.extras] 236 | dev = ["pre-commit", "tox"] 237 | testing = ["pytest", "pytest-benchmark"] 238 | 239 | [[package]] 240 | name = "prettytable" 241 | version = "0.7.2" 242 | description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format." 243 | category = "main" 244 | optional = false 245 | python-versions = "*" 246 | files = [ 247 | {file = "prettytable-0.7.2.tar.bz2", hash = "sha256:853c116513625c738dc3ce1aee148b5b5757a86727e67eff6502c7ca59d43c36"}, 248 | {file = "prettytable-0.7.2.tar.gz", hash = "sha256:2d5460dc9db74a32bcc8f9f67de68b2c4f4d2f01fa3bd518764c69156d9cacd9"}, 249 | {file = "prettytable-0.7.2.zip", hash = "sha256:a53da3b43d7a5c229b5e3ca2892ef982c46b7923b51e98f0db49956531211c4f"}, 250 | ] 251 | 252 | [[package]] 253 | name = "pycodestyle" 254 | version = "2.7.0" 255 | description = "Python style guide checker" 256 | category = "dev" 257 | optional = false 258 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 259 | files = [ 260 | {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, 261 | {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, 262 | ] 263 | 264 | [[package]] 265 | name = "pyflakes" 266 | version = "2.3.1" 267 | description = "passive checker of Python programs" 268 | category = "dev" 269 | optional = false 270 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 271 | files = [ 272 | {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, 273 | {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, 274 | ] 275 | 276 | [[package]] 277 | name = "tomli" 278 | version = "2.0.1" 279 | description = "A lil' TOML parser" 280 | category = "dev" 281 | optional = false 282 | python-versions = ">=3.7" 283 | files = [ 284 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 285 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 286 | ] 287 | 288 | [[package]] 289 | name = "typeguard" 290 | version = "2.13.3" 291 | description = "Run-time type checker for Python" 292 | category = "main" 293 | optional = false 294 | python-versions = ">=3.5.3" 295 | files = [ 296 | {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"}, 297 | {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"}, 298 | ] 299 | 300 | [package.extras] 301 | doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 302 | test = ["mypy", "pytest", "typing-extensions"] 303 | 304 | [[package]] 305 | name = "typing-extensions" 306 | version = "3.10.0.2" 307 | description = "Backported and Experimental Type Hints for Python 3.5+" 308 | category = "main" 309 | optional = false 310 | python-versions = "*" 311 | files = [ 312 | {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, 313 | {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, 314 | {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, 315 | ] 316 | 317 | [metadata] 318 | lock-version = "2.0" 319 | python-versions = "^3.10" 320 | content-hash = "859675c11b50100dee315f24e9501788ad7a5f9afe1d1811e822ed3c285ebe43" 321 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "nixops_virtd" 3 | version = "1.0" 4 | description = "NixOps plugin for virtd" 5 | authors = ["Amine Chikhaoui "] 6 | include = [ "nixops_libvirtd/nix/*.nix" ] 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.10" 10 | nixops = {git = "https://github.com/NixOS/nixops.git"} 11 | libvirt-python = "^9.0" 12 | 13 | [tool.poetry.plugins."nixops"] 14 | virtd = "nixops_virtd.plugin" 15 | 16 | [tool.poetry.dev-dependencies] 17 | nose = "^1.3.7" 18 | mypy = "^0.961" 19 | black = "^22.6.0" 20 | flake8 = "^3.8.2" 21 | 22 | [build-system] 23 | requires = ["poetry>=0.12"] 24 | build-backend = "poetry.masonry.api" 25 | -------------------------------------------------------------------------------- /release.nix: -------------------------------------------------------------------------------- 1 | { nixopsLibvirtd ? { outPath = ./.; revCount = 0; shortRev = "abcdef"; rev = "HEAD"; } 2 | , nixpkgs ? 3 | , officialRelease ? false 4 | }: 5 | let 6 | pkgs = import nixpkgs {}; 7 | version = "1.7" + (if officialRelease then "" else "pre${toString nixopsLibvirtd.revCount}_${nixopsLibvirtd.shortRev}"); 8 | in 9 | rec { 10 | build = pkgs.lib.genAttrs [ "x86_64-linux" "i686-linux" "x86_64-darwin" ] (system: 11 | with import nixpkgs { inherit system; }; 12 | python2Packages.buildPythonApplication rec { 13 | name = "nixops-libvirtd"; 14 | src = ./.; 15 | prePatch = '' 16 | for i in setup.py; do 17 | substituteInPlace $i --subst-var-by version ${version} 18 | done 19 | ''; 20 | buildInputs = [ python2Packages.nose python2Packages.coverage ]; 21 | propagatedBuildInputs = [ python2Packages.libvirt ]; 22 | doCheck = true; 23 | postInstall = '' 24 | mkdir -p $out/share/nix/nixops-libvirtd 25 | cp -av nix/* $out/share/nix/nixops-libvirtd 26 | ''; 27 | meta.description = "NixOps libvirtd backend for ${stdenv.system}"; 28 | } 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | pkgs.mkShell { 4 | buildInputs = [ 5 | (import ./env.nix { inherit pkgs; }) 6 | pkgs.poetry 7 | pkgs.pkgconfig 8 | pkgs.libvirt 9 | ]; 10 | } 11 | -------------------------------------------------------------------------------- /tests/functional/single_machine_libvirtd_base.nix: -------------------------------------------------------------------------------- 1 | { 2 | machine = 3 | { resources, ... }: 4 | { 5 | deployment.targetEnv = "libvirtd"; 6 | deployment.libvirtd = { 7 | headless = true; 8 | }; 9 | }; 10 | } 11 | --------------------------------------------------------------------------------