From 81c21f3effbc7f93dd93a180f9c883687639ef94 Mon Sep 17 00:00:00 2001 From: "Yc.S" Date: Thu, 6 Feb 2020 11:05:39 +0900 Subject: [PATCH] Support network resource management --- nix/default.nix | 4 +- nix/virtualbox-network.nix | 55 +++++ nix/virtualbox.nix | 32 ++- nixopsvbox/backends/virtualbox.py | 64 +++++- nixopsvbox/plugin.py | 1 + nixopsvbox/resources/__init__.py | 2 + nixopsvbox/resources/__init__.pyc | Bin 0 -> 221 bytes nixopsvbox/resources/virtualbox_network.py | 231 ++++++++++++++++++++ nixopsvbox/resources/virtualbox_network.pyc | Bin 0 -> 4555 bytes release.nix | 1 + setup.py | 2 +- 11 files changed, 381 insertions(+), 11 deletions(-) create mode 100644 nix/virtualbox-network.nix create mode 100644 nixopsvbox/resources/__init__.py create mode 100644 nixopsvbox/resources/__init__.pyc create mode 100644 nixopsvbox/resources/virtualbox_network.py create mode 100644 nixopsvbox/resources/virtualbox_network.pyc diff --git a/nix/default.nix b/nix/default.nix index 2c7752e..0126219 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -16,5 +16,7 @@ options = [ ./virtualbox.nix ]; - resources = { ... }: {}; + resources = { evalResources, zipAttrs, resourcesByType, ...}: { + vboxNetworks = evalResources ./virtualbox-network.nix (zipAttrs resourcesByType.vboxNetworks or []); + }; } diff --git a/nix/virtualbox-network.nix b/nix/virtualbox-network.nix new file mode 100644 index 0000000..469f54d --- /dev/null +++ b/nix/virtualbox-network.nix @@ -0,0 +1,55 @@ +{ config, lib, pkgs, uuid, name, ... }: + +with lib; +with import lib; + +rec { + options = { + type = mkOption { + default = "hostonly"; + description = '' + The type of the VirtualBox network. + Either NAT network or Host-only network can be specified. Defaults to Host-only Network. + ''; + type = types.enum [ "natnet" "hostonly" ]; + }; + + cidrBlock = mkOption { + example = "192.168.56.0/24"; + description = '' + The IPv4 CIDR block for the VirtualBox network. The following IP addresses are reserved for the network: + Network - The first address in the IP range, e.g. 192.168.56.0 in 192.168.56.0/24 + Gateway - The second address in the IP range, e.g. 192.168.56.1 in 192.168.56.0/24 + DHCP Server - The third address in the IP range, e.g. 192.168.56.2 in 192.168.56.0/24 + Broadcast - The last address in the IP range, e.g. 192.168.56.255 in 192.168.56.0/24 + ''; + type = types.str; + }; + + staticIPs = mkOption { + default = []; + description = "The list of machine to IPv4 address bindings for fixing IP address of the machine in the network"; + type = with types; listOf (submodule { + options = { + machine = mkOption { + type = either str (resource "machine"); + apply = x: if builtins.isString x then x else x._name; + description = "The name of the machine in the network"; + }; + address = mkOption { + example = "192.168.56.3"; + type = str; + description = '' + The IPv4 address assigned to the machine as static IP. + The static IP must be a non-reserved IP address. + ''; + }; + }; + }); + }; + }; + + config = { + _type = "vbox-network"; + }; +} diff --git a/nix/virtualbox.nix b/nix/virtualbox.nix index bd77647..1461079 100644 --- a/nix/virtualbox.nix +++ b/nix/virtualbox.nix @@ -1,6 +1,7 @@ -{ config, pkgs, lib, ... }: +{ config, pkgs, lib, resources, ... }: with lib; +with import lib; let @@ -91,6 +92,35 @@ in }); }; + deployment.virtualbox.networks = mkOption { + default = [ { type = "nat"; } { type = "hostonly"; name = "vboxnet0"; } ]; + description = '' + The list of networks to which the instance is attached. The network can be either + a vbox-network resource or a network not managed by NixOps. + + For the sake of backward compatibility, the default list contains the following networks: + - NAT + - Host-only network vboxnet0 + + Note: NixOps requires at least one Host-only network to access the instance for management purposes, + When multiple Host-only networks exist, the first one in the list will be used for machine connection. + ''; + type = with types; listOf (either (resource "vbox-network") + (submodule { options = { + name = mkOption { + default = ""; + description = "The name of the network not managed by NixOps"; + type = str; + }; + type = mkOption { + description = "The type of the network"; + type = enum [ "nat" "natnet" "bridge" "hostonly" "intnet" "generic" ]; + }; + }; + }) + ); + }; + deployment.virtualbox.sharedFolders = mkOption { default = {}; diff --git a/nixopsvbox/backends/virtualbox.py b/nixopsvbox/backends/virtualbox.py index 93bed3b..da9e2d3 100644 --- a/nixopsvbox/backends/virtualbox.py +++ b/nixopsvbox/backends/virtualbox.py @@ -5,9 +5,12 @@ import time import shutil import stat +import re +from collections import defaultdict from nixops.backends import MachineDefinition, MachineState from nixops.nix_expr import RawValue import nixops.known_hosts +import nixopsvbox.resources.virtualbox_network from distutils import spawn sata_ports = 8 @@ -49,6 +52,7 @@ def get_type(cls): def __init__(self, depl, name, id): MachineState.__init__(self, depl, name, id) self._disk_attached = False + self._hooks = defaultdict(list) @property def resource_id(self): @@ -123,8 +127,10 @@ def _get_vm_state(self, can_fail=False): def _start(self): + nic_num, _ = next(self._get_nic_info()) + self._logged_exec( - ["VBoxManage", "guestproperty", "set", self.vm_id, "/VirtualBox/GuestInfo/Net/1/V4/IP", '']) + ["VBoxManage", "guestproperty", "set", self.vm_id, "/VirtualBox/GuestInfo/Net/{}/V4/IP".format(nic_num - 1), '']) self._logged_exec( ["VBoxManage", "guestproperty", "set", self.vm_id, "/VirtualBox/GuestInfo/Charon/ClientPublicKey", self._client_public_key]) @@ -134,10 +140,11 @@ def _start(self): self.state = self.STARTING - def _update_ip(self): + nic_num, _ = next(self._get_nic_info()) + res = self._logged_exec( - ["VBoxManage", "guestproperty", "get", self.vm_id, "/VirtualBox/GuestInfo/Net/1/V4/IP"], + ["VBoxManage", "guestproperty", "get", self.vm_id, "/VirtualBox/GuestInfo/Net/{}/V4/IP".format(nic_num - 1)], capture_stdout=True).rstrip() if res[0:7] != "Value: ": return new_address = res[7:] @@ -173,6 +180,9 @@ def _wait_for_ip(self): self.log_end(" " + self.private_ipv4) + def create_after(self, resources, defn): + return {r for r in resources if isinstance(r, nixopsvbox.resources.virtualbox_network.VirtualBoxNetworkState)} + def create(self, defn, check, allow_reboot, allow_recreate): assert isinstance(defn, VirtualBoxDefinition) @@ -191,6 +201,9 @@ def create(self, defn, check, allow_reboot, allow_recreate): self.vm_id = vm_id self.state = self.STOPPED + for hook in self._hooks.get("after_createvm", []): + hook() + # Generate a public/private host key. if not self.public_host_key: (private, public) = nixops.util.create_key_pair() @@ -350,13 +363,10 @@ def create(self, defn, check, allow_reboot, allow_recreate): modifyvm_args = [ "--memory", str(defn.config["virtualbox"]["memorySize"]), "--vram", "10", - "--nictype1", "virtio", - "--nictype2", "virtio", - "--nic2", "hostonly", - "--hostonlyadapter2", "vboxnet0", "--nestedpaging", "off", "--paravirtprovider", "kvm" - ] + ] + [ f for fs in [ nic["flags"] for nic in self.parse_nic_spec(defn, flags=True).values() ] for f in fs if f ] + vcpus = defn.config["virtualbox"]["vcpu"] # None or integer if vcpus is not None: modifyvm_args.extend(["--cpus", str(vcpus)]) @@ -470,3 +480,41 @@ def _check(self, res): MachineState._check(self, res) else: self.state = self.UNKNOWN + + def _get_nic_info(self, network_type="hostonly"): + res = re.findall(r"nic(\d+)=\"({})\"".format(network_type), self._logged_exec( + ["VBoxManage", "showvminfo", self.vm_id, "--machinereadable"], + capture_stdout=True + )) + return ((int(num), type) for num, type in res) + + def parse_nic_spec(self, defn, num=True, flags=False): + def to_arg(type): + if type == "bridge": return "bridged" + elif type == "natnet": return "natnetwork" + else: + return type + + def to_adp(type): + if type == "bridge" : return "--bridgeadapter{}" + elif type == "hostonly": return "--hostonlyadapter{}" + elif type == "intnet" : return "--intnet{}" + elif type == "natnet" : return "--nat-network{}" + elif type == "generic" : return "--nicgenericdrv{}" + elif type == "nat" : return "" + else: + raise Exception("unknown NIC type is specified on VirtualBox VM ‘{0}’".format(self.name)) + + res = defaultdict(lambda: {}) + for i, net in enumerate(defn.config["virtualbox"]["networks"], start=1): + k = "{0}:{1}".format(net.get("name", net.get("_name")), net.get("type")) + if num : res[k]["num"] = i + if flags: res[k]["flags"] = [ + "--nic{0}".format(i) , to_arg(net.get("type")), + "--nictype{0}".format(i) , "virtio", + to_adp(net.get("type")).format(i), self.depl.resources[net.get("_name")].network_name if net.get("_name") else net.get("name", "") + ] + return res + + def add_hook(self, k, handler): + self._hooks[k].append(handler) diff --git a/nixopsvbox/plugin.py b/nixopsvbox/plugin.py index 7b65331..c4ab01e 100644 --- a/nixopsvbox/plugin.py +++ b/nixopsvbox/plugin.py @@ -16,5 +16,6 @@ def nixexprs(): @nixops.plugins.hookimpl def load(): return [ + "nixopsvbox.resources", "nixopsvbox.backends.virtualbox", ] diff --git a/nixopsvbox/resources/__init__.py b/nixopsvbox/resources/__init__.py new file mode 100644 index 0000000..261cb2b --- /dev/null +++ b/nixopsvbox/resources/__init__.py @@ -0,0 +1,2 @@ +import virtualbox_network +import __init__ diff --git a/nixopsvbox/resources/__init__.pyc b/nixopsvbox/resources/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dd62b1f960344e5b9be00a005091c9bff5619c46 GIT binary patch literal 221 zcmYLDyAHxI40K>Y2o`2OpbO*&5JF;SVe1k>6R8ntQpHIr-^~Yr9q~B%PG`$Hd-`0? zfzYrpCd2~~*Cd)sfCH$1UO+8)c&>QUz6hsc(~A1R#*{Da8b;+!x`%j6Ga4&py-iA0 zf?)z21AaGLHW(x_K$L7*9D!O*$c`<{%s-N DFAX+m literal 0 HcmV?d00001 diff --git a/nixopsvbox/resources/virtualbox_network.py b/nixopsvbox/resources/virtualbox_network.py new file mode 100644 index 0000000..fa01df4 --- /dev/null +++ b/nixopsvbox/resources/virtualbox_network.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- + +# Automatic provisioning of Virtualbox Networks. + +import os +import re +import ipaddress +import threading +from collections import namedtuple +from nixops.util import attr_property, logged_exec +from nixops.resources import ResourceDefinition, ResourceState +from nixopsvbox.backends.virtualbox import VirtualBoxDefinition, VirtualBoxState + +class VirtualBoxNetworkDefinition(ResourceDefinition): + """Definition of the VirtualBox Network""" + + @classmethod + def get_type(cls): + return "vbox-network" + + @classmethod + def get_resource_type(cls): + return "vboxNetworks" + + def __init__(self, xml): + ResourceDefinition.__init__(self, xml) + self.network_type = xml.find("attrs/attr[@name='type']/string").get("value") + self.network_cidr = xml.find("attrs/attr[@name='cidrBlock']/string").get("value") + + static_ip=namedtuple("static_ip", ["machine", "address"]) + def parse_static_ips(x): + return static_ip( + x.find("attr[@name='machine']/string").get("value"), + x.find("attr[@name='address']/string").get("value") + ) + + self.static_ips = [ parse_static_ips(f) for f in xml.findall("attrs/attr[@name='staticIPs']/list/attrs") ] + + def show_type(self): + return "{0} [{1:8} {2}]".format(self.get_type(), self.network_type, self.network_cidr) + +class VirtualBoxNetworkState(ResourceState): + """State of the VirtualBox Network""" + + network_name = attr_property("virtualbox.network_name", None) + network_type = attr_property("virtualbox.network_type", None) + network_cidr = attr_property("virtualbox.network_cidr", None) + + @classmethod + def get_type(cls): + return "vbox-network" + + def __init__(self, depl, name, id): + ResourceState.__init__(self, depl, name, id) + VirtualBoxNetwork.logger = self.logger + + def show_type(self): + s = super(VirtualBoxNetworkState, self).show_type() + if self.state == self.UP: s = "{0} [{1}]".format(s, self.network_type) + return s + + @property + def resource_id(self): + return self.network_name + + @property + def public_ipv4(self): + return self.network_cidr if self.state == self.UP else None; + + nix_name = "vboxNetworks" + + @property + def full_name(self): + return "VirtualBox network '{}'".format(self.name) + + def create(self, defn, check, allow_reboot, allow_recreate): + assert isinstance(defn, VirtualBoxNetworkDefinition) + + self.network_type = defn.network_type; + self.network_cidr = defn.network_cidr; + + if check: + pass + + if self.state != self.UP: + self.log("creating {}...".format(self.full_name)) + self.network_name = VirtualBoxNetworks[defn.network_type].create(self, defn).name + + self.state = self.UP + + def destroy(self, wipe=False): + if self.state == self.UP: + if not self.depl.logger.confirm("are you sure you want to destroy {}?".format(self.full_name)): + return False + + self.log("destroying {}...".format(self.full_name)) + VirtualBoxNetworks[self.network_type](self.network_name).destroy() + + return True + +class VirtualBoxNetwork(object): + """Wrapper for VBoxManage CLI network operations""" + + logger = None + + def __init__(self, name): + self._name = name + + @property + def name(self): + return self._name + + def update(self, state, defn): + pass + + @classmethod + def _findall_dhcped(cls): + return re.findall(r"NetworkName: *(NatNetwork\d+|HostInterfaceNetworking-vboxnet\d+) *", logged_exec([ + "VBoxManage", "list", "dhcpservers" + ], cls.logger, capture_stdout=True)) + + def setup_dhcp_server(self, state, defn): + subnet = ipaddress.ip_network(unicode(defn.network_cidr), strict=True) + + logged_exec([ + "VBoxManage" , "dhcpserver", "modify" if self._name in self._findall_dhcped() else "add", + "--netname" , self._name, + "--netmask" , str(subnet.netmask), + "--ip" , str(subnet[2]), + "--lowerip" , str(subnet[3]), + "--upperip" , str(subnet[-2]), + "--enable" + ], self.logger) + + for machine, address in defn.static_ips: + mstate = state.depl.resources.get(machine) + mdefn = state.depl.definitions.get(machine) + if isinstance(mstate, VirtualBoxState) and isinstance(mdefn, VirtualBoxDefinition): + print(mstate) + print(mdefn) + k = "{name}:{network_type}".format(**defn.__dict__) + nics = mstate.parse_nic_spec(mdefn) + if nics.get(k): + mstate.add_hook("after_createvm", lambda : logged_exec([ + "VBoxManage", "dhcpserver", "modify", + "--netname" , self._name, + "--vm" , mstate.vm_id, + "--nic" , str(nics[k]["num"]), + "--fixed-address", address + ], self.logger)) + + return subnet + + def destroy(self): + logged_exec(["VBoxManage", "dhcpserver", "remove", "--netname", self._name], self.logger, check=False) + +class VirtualBoxHostNetwork(VirtualBoxNetwork): + _if_prefix = "vboxnet"; + + def __init__(self, name): + super(VirtualBoxHostNetwork, self).__init__("HostInterfaceNetworking-{}".format(name)) + self._if_name = name + + @classmethod + def create(cls, state, defn): + name = re.match(r"^.*'(vboxnet\d+)'.*$", logged_exec([ + "VBoxManage", "hostonlyif", "create" + ], cls.logger, capture_stdout=True)).group(1) + + return cls(name).update(state, defn) + + @classmethod + def findall(cls): + return re.findall(r"Name: *(vboxnet\d+) *", logged_exec([ + "VBoxManage", "list", "hostonlyifs" + ], cls.logger, capture_stdout=True)) + + @property + def name(self): + return self._if_name + + def update(self, state, defn): + subnet = self.setup_dhcp_server(state, defn) + logged_exec(["VBoxManage", "hostonlyif", "ipconfig", self._if_name, "--ip", str(subnet[1]), "--netmask", str(subnet.netmask)], self.logger) + return self + + def destroy(self): + super(VirtualBoxHostNetwork, self).destroy() + logged_exec(["VBoxManage", "hostonlyif", "remove", self._if_name], self.logger, check=False) + +class VirtualBoxNatNetwork(VirtualBoxNetwork): + _pattern = "NatNetwork{}" + _lock = threading.Lock() + + def __init__(self, name): + super(VirtualBoxNatNetwork, self).__init__(name); + + @classmethod + def create(cls, state, defn): + def new_name(): + exists = cls.findall() + return cls._pattern.format(int(exists[-1][len(cls._pattern.format("")):])+1 if exists else 1) + + with cls._lock: + name = new_name() + logged_exec([ + "VBoxManage", "natnetwork", "add", "--netname", name, "--network", defn.network_cidr + ], cls.logger) + + return cls(name).update(state, defn) + + @classmethod + def findall(cls): + return re.findall(r"Name: *("+cls._pattern.format("")+r"\d+) *", logged_exec([ + "VBoxManage", "natnetwork", "list", cls._pattern.format("*") + ], cls.logger, capture_stdout=True)) + + def update(self, state, defn): + logged_exec(["VBoxManage", "natnetwork", "modify", "--netname", self._name, "--network", defn.network_cidr, "--enable", "--dhcp", "on"], self.logger) + self.setup_dhcp_server(state, defn) + logged_exec(["VBoxManage", "natnetwork", "start" , "--netname", self._name], self.logger) + return self + + def destroy(self): + super(VirtualBoxNatNetwork, self).destroy() + logged_exec(["VBoxManage", "natnetwork", "remove", "--netname", self._name], self.logger, check=False) + +VirtualBoxNetworks = { + "hostonly" : VirtualBoxHostNetwork, + "natnet" : VirtualBoxNatNetwork, +} diff --git a/nixopsvbox/resources/virtualbox_network.pyc b/nixopsvbox/resources/virtualbox_network.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a4844bfb7a65348c360f94f04413467660ceacd1 GIT binary patch literal 4555 zcmcgv+io015bd75_!2vI41@#{7*L2eL0JPrAVnxa4)B1K6>38g%w;s$>0PfgJF`sp zCbr_d;QRt_dEf{56dw2%J^;?C*;{}J9@wnit?GMK^{LY}mETvF6@G12{8dmsMsXRc z1piv4O6^W$t#-Ba2PzFzzoK?4N^@RCrB&6hsok3D*VS$vy;Wsvs)Ok@^;oH6rM})> zP)S|cy7VqgdKY=jPGb>A_Wr=H)zA?ADyrD~A{rF=z!v_n5wLp$m9Z`#7H#{9?ewyq z@8y}y9(V8f*jxW9Cf@25ei*0Q`9aJ22YGRCx?*B8cil#D*HBqlD;Dks%2uctb%)uo z2F+mHtkGl)!P2BE!h$k8jT-lP6V>#bFzV<8p)Rs;I;h)TrfFP~X3s zA6&^w)f;H5(lr%G?bMY$>~=R%ZSCcKyXE$5wnbBuUa^(+4p{PmyTYwTvWUgVhi>bB z=_HItqobOGL(l8zS|9ntfj!A1sFe!-O>I9`dt~NFNg8Ts;9PY+g#)Pb*fvp~fEg=K z8(8Q-@%f7oyi)sG9VvCB)o~zr-Vr~y1aU^tNOwdL(H2D|5NVCGX~%Q3gMK(5Y^yC&MhMdJ9z_Sl4#GK<&2x!rcDBN;OnDg+RN8vXQ-+lC5gG+rKRVOd{u{XqS z8FWbk2fC<;v#u3K7_OYBeVzxnz5IdP7;PCDDy$$_jG`>=+bHs6tSIW|$uMPeL7GY4 zhI|5YJB^*|TfdhlVv^wm@t+~1Osx><{|_8PzfuuLMeDjAtX0>lOBK)So6^82JcO#+ z7?B^BZlk!LP|Z>iZWc^yt7M>Lg?9?#VH2EGHW#Fcp0c^9?9!Ndv38lnOS(Ft5+UB* z6=hddvdB)-+ev9IjhkyKS(f?h%AQim3H2CrHpp=Fp*xRCG=CPk8RE*ljA}|Q%@MiC zJw9uUY~#*6y)K@`;{^GP${Rd|armETbME{jc)LiQBuaxeRbydjiLS3en5CPeKpj=o zarN({!yN>*4Q?O-=*3byNJU^Yc+To2Q%uhR&WkwWGRiFT@K|D1Y+?nQyih2Rxev}! z>X9_}D?&$AbuUoGbs?x4X_g#SCyX%-sRnxfnWGrHULMsUQUD`wKM`2=$umz~zWTy2 z;tb!OlC#Mt&hyGtOeIiKW1 z-y>`!_5CdFrJ>J5nHt6+ceuhW#t>;rjyPgo?onF@nPtJx1Z83aL3}GiJ zSDJUrs6WRZBDS!?3rJ?#dvZpEIbEAX_Q1Av}z?>q@Yp&+3Z4 zs#npoqT6%`3r!Ks{|tdBDr+s2{6rll(g{FT2r$Mv7#h<+&w^X#45A$K@fH4ss z#F;0q66<`C9}>*h9W(w>#}we^HT0U-Q3<9B+t2S?(?r|7fNBiTnT;f^nc~ZEnyeK7 z6_RAO3C*HEgE_kPvp99uP+`MMFOkdWdk`0yxddV6L6ZL7z|IpMqU*@C&vCrtqp9=; zu{tHI!jCQ>6KCE_{ntgL1?&Ca(&54=5CcZ_oMQw=Nf@0pBF27C0N&|gVP&- E0+c}s^#A|> literal 0 HcmV?d00001 diff --git a/release.nix b/release.nix index 1e685bf..023eb2e 100644 --- a/release.nix +++ b/release.nix @@ -18,6 +18,7 @@ in done ''; buildInputs = [ python2Packages.nose python2Packages.coverage ]; + propagatedBuildInputs = [ python2Packages.ipaddress ]; doCheck = true; postInstall = '' mkdir -p $out/share/nix/nixops-vbox diff --git a/setup.py b/setup.py index 1c75401..d2296a7 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ url='https://github.com/AmineChikhaoui/nixops-vbox', maintainer='Amine Chikhaoui', maintainer_email='amine.chikhaoui91@gmail.com', - packages=['nixopsvbox', 'nixopsvbox.backends'], + packages=['nixopsvbox', 'nixopsvbox.backends', 'nixopsvbox.resources'], entry_points={'nixops': ['vbox = nixopsvbox.plugin']}, py_modules=['plugin'] )