├── .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 |
--------------------------------------------------------------------------------