Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: NixOS/nixops
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 474a5ee2677c^
Choose a base ref
...
head repository: NixOS/nixops
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: a74e555ac51e
Choose a head ref
  • 8 commits
  • 12 files changed
  • 2 contributors

Commits on Apr 16, 2020

  1. Move from xml intermediate Nix representation to JSON

    This change is intended to make life easier for plugin authors.
    We have removed the XML parameter to ResourceDefinition and you are
    now only provided with a name & a dict containing the evaluated
    values.
    adisbladis committed Apr 16, 2020

    Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    Ma27 Maximilian Bosch
    Copy the full SHA
    474a5ee View commit details
  2. keys: apply toString to all keyFiles

    With nix-instantiate --xml, the output of evaluation of a path shows
    the original path to the file. With --json, the output shows the path
    if it were copied to the Nix store.
    
    Applying toString in the expression forces Nix to skip copying it to
    the store under any circumstance.
    grahamc authored and adisbladis committed Apr 16, 2020

    Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    Ma27 Maximilian Bosch
    Copy the full SHA
    7787335 View commit details
  3. Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    Ma27 Maximilian Bosch
    Copy the full SHA
    a2cefc2 View commit details
  4. Remove xml_expr_to_python function

    It's no longer required since moving to JSON representation.
    adisbladis committed Apr 16, 2020

    Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    Ma27 Maximilian Bosch
    Copy the full SHA
    2ef7085 View commit details
  5. Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    Ma27 Maximilian Bosch
    Copy the full SHA
    d6dbe6f View commit details
  6. Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    Ma27 Maximilian Bosch
    Copy the full SHA
    96e7552 View commit details
  7. Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    Ma27 Maximilian Bosch
    Copy the full SHA
    23b4be7 View commit details

Commits on Apr 17, 2020

  1. Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    Ma27 Maximilian Bosch
    Copy the full SHA
    a74e555 View commit details
Showing with 323 additions and 138 deletions.
  1. +40 −0 doc/plugins/authoring.rst
  2. +1 −0 nix/keys.nix
  3. +29 −43 nixops/backends/__init__.py
  4. +12 −9 nixops/backends/none.py
  5. +20 −32 nixops/deployment.py
  6. +23 −6 nixops/resources/__init__.py
  7. +12 −5 nixops/resources/commandOutput.py
  8. +3 −2 nixops/resources/ssh_keypair.py
  9. +114 −40 nixops/util.py
  10. +17 −1 poetry.lock
  11. +1 −0 pyproject.toml
  12. +51 −0 tests/unit/test_util.py
40 changes: 40 additions & 0 deletions doc/plugins/authoring.rst
Original file line number Diff line number Diff line change
@@ -113,6 +113,46 @@ Important Notes
os.path.dirname(os.path.abspath(__file__)) + "/nix"
]
5. Resource subclasses must now work with Python objects instead of XML

This old-style ResourceDefinition subclass:

.. code-block:: python
class MachineDefinition(nixops.resources.ResourceDefinition):
def __init__(self, xml):
super().__init__(xml)
self.store_keys_on_machine = (
xml.find("attrs/attr[@name='storeKeysOnMachine']/bool").get("value")
== "true"
)
Should now look like:

.. code-block:: python
class MachineOptions(nixops.resources.ResourceOptions):
storeKeysOnMachine: bool
class MachineDefinition(nixops.resources.ResourceDefinition):
config: MachineOptions
store_keys_on_machine: bool
def __init__(self, name: str, config: nixops.resources.ResourceEval):
super().__init__(name, config)
self.store_keys_on_machine = config.storeKeysOnMachine
``ResourceEval`` is an immutable ``typing.Mapping`` implementation.
Also note that ``ResourceEval`` has turned Nix lists into Python tuples, dictionaries into ResourceEval objects and so on.
Please try to use generic types such as ``typing.Mapping`` instead of ``typing.Dict`` and ``typing.Sequence`` instead of ``typing.List``.

``ResourceOptions`` is an immutable object that provides type validation both with ``mypy`` _and_ at runtime.
Any attributes which are not explicitly typed are passed through as-is.


On with Poetry
----

1 change: 1 addition & 0 deletions nix/keys.nix
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ let
options.keyFile = mkOption {
default = null;
type = types.nullOr types.path;
apply = toString;
description = ''
When non-null, contents of the specified file will be deployed to the
specified key on the target machine. If the key name is
72 changes: 29 additions & 43 deletions nixops/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -3,56 +3,42 @@
import os
import re
import subprocess
from typing import Dict, Any, List, Optional, Union, Set
from typing import Mapping, Any, List, Optional, Union, Set, Sequence
import nixops.util
import nixops.resources
import nixops.ssh_util
import xml.etree.ElementTree as ET


class MachineOptions(nixops.resources.ResourceOptions):
storeKeysOnMachine: bool
targetPort: int
alwaysActivate: bool
owners: Sequence[str]
hasFastConnection: bool
keys: Mapping[str, Mapping]
nixosRelease: str


class MachineDefinition(nixops.resources.ResourceDefinition):
"""Base class for NixOps machine definitions."""

def __init__(self, xml, config={}) -> None:
nixops.resources.ResourceDefinition.__init__(self, xml, config)
self.store_keys_on_machine = (
xml.find("attrs/attr[@name='storeKeysOnMachine']/bool").get("value")
== "true"
)
self.ssh_port = int(xml.find("attrs/attr[@name='targetPort']/int").get("value"))
self.always_activate = (
xml.find("attrs/attr[@name='alwaysActivate']/bool").get("value") == "true"
)
self.owners = [
e.get("value")
for e in xml.findall("attrs/attr[@name='owners']/list/string")
]
self.has_fast_connection = (
xml.find("attrs/attr[@name='hasFastConnection']/bool").get("value")
== "true"
)
config: MachineOptions

store_keys_on_machine: bool
ssh_port: int
always_activate: bool
owners: List[str]
has_fast_connection: bool
keys: Mapping[str, Mapping]

def _extract_key_options(x: ET.Element) -> Dict[str, str]:
opts = {}
for (key, xmlType) in (
("text", "string"),
("keyFile", "path"),
("destDir", "string"),
("user", "string"),
("group", "string"),
("permissions", "string"),
):
elem = x.find("attrs/attr[@name='{0}']/{1}".format(key, xmlType))
if elem is not None:
value = elem.get("value")
if value is not None:
opts[key] = value
return opts

self.keys = {
k.get("name"): _extract_key_options(k)
for k in xml.findall("attrs/attr[@name='keys']/attrs/attr")
}
def __init__(self, name: str, config: nixops.resources.ResourceEval):
super().__init__(name, config)
self.store_keys_on_machine = config["storeKeysOnMachine"]
self.ssh_port = config["targetPort"]
self.always_activate = config["alwaysActivate"]
self.owners = config["owners"]
self.has_fast_connection = config["hasFastConnection"]
self.keys = config["keys"]


class MachineState(nixops.resources.ResourceState):
@@ -68,7 +54,7 @@ class MachineState(nixops.resources.ResourceState):
store_keys_on_machine: bool = nixops.util.attr_property(
"storeKeysOnMachine", False, bool
)
keys: Dict[str, str] = nixops.util.attr_property("keys", {}, "json")
keys: Mapping[str, str] = nixops.util.attr_property("keys", {}, "json")
owners: List[str] = nixops.util.attr_property("owners", [], "json")

# Nix store path of the last global configuration deployed to this
@@ -210,7 +196,7 @@ def remove_backup(self, backup_id, keep_physical=False):
"don't know how to remove a backup for machine ‘{0}’".format(self.name)
)

def get_backups(self) -> Dict[str, Dict[str, Any]]:
def get_backups(self) -> Mapping[str, Mapping[str, Any]]:
self.warn("don't know how to list backups for ‘{0}’".format(self.name))
return {}

21 changes: 12 additions & 9 deletions nixops/backends/none.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
# -*- coding: utf-8 -*-
from typing import Dict, Optional
import os
import sys
import nixops.util

from nixops.backends import MachineDefinition, MachineState
from nixops.backends import MachineDefinition, MachineState, MachineOptions
from nixops.util import attr_property, create_key_pair
import nixops.resources


class NoneDefinition(MachineDefinition):
"""Definition of a trivial machine."""

_target_host: str
_public_ipv4: Optional[str]

config: MachineOptions

@classmethod
def get_type(cls):
return "none"

def __init__(self, xml, config):
MachineDefinition.__init__(self, xml, config)
self._target_host = xml.find("attrs/attr[@name='targetHost']/string").get(
"value"
)

public_ipv4 = xml.find("attrs/attr[@name='publicIPv4']/string")
self._public_ipv4 = None if public_ipv4 is None else public_ipv4.get("value")
def __init__(self, name: str, config: nixops.resources.ResourceEval):
super().__init__(name, config)
self._target_host = config["targetHost"]
self._public_ipv4 = config.get("publicIPv4", None)


class NoneState(MachineState):
52 changes: 20 additions & 32 deletions nixops/deployment.py
Original file line number Diff line number Diff line change
@@ -9,7 +9,6 @@
import sqlite3
import threading
from collections import defaultdict
from xml.etree import ElementTree
import re
from datetime import datetime, timedelta
import getpass
@@ -440,16 +439,15 @@ def evaluate_args(self) -> Any:
except subprocess.CalledProcessError:
raise NixEvalError

def evaluate_config(self, attr):
def evaluate_config(self, attr) -> Dict:
try:
# FIXME: use --json
xml = subprocess.check_output(
_json = subprocess.check_output(
["nix-instantiate"]
+ self.extra_nix_eval_flags
+ self._eval_flags(self.nix_exprs)
+ [
"--eval-only",
"--xml",
"--json",
"--strict",
"--arg",
"checkConfigurationOptions",
@@ -461,25 +459,19 @@ def evaluate_config(self, attr):
text=True,
)
if DEBUG:
print("XML output of nix-instantiate:\n" + xml, file=sys.stderr)
print("JSON output of nix-instantiate:\n" + _json, file=sys.stderr)
except OSError as e:
raise Exception("unable to run ‘nix-instantiate’: {0}".format(e))
except subprocess.CalledProcessError:
raise NixEvalError

tree = ElementTree.fromstring(xml)

# Convert the XML to a more Pythonic representation. This is
# in fact the same as what json.loads() on the output of
# "nix-instantiate --json" would yield.
config = nixops.util.xml_expr_to_python(tree.find("*"))
return (tree, config)
return json.loads(_json)

def evaluate_network(self, action: str = "") -> None:
if not self.network_attr_eval:
# Extract global deployment attributes.
try:
(_, config) = self.evaluate_config("info.network")
config = self.evaluate_config("info.network")
except Exception as e:
if action not in ("destroy", "delete"):
raise e
@@ -494,22 +486,20 @@ def evaluate(self) -> None:
self.definitions = {}
self.evaluate_network()

(tree, config) = self.evaluate_config("info")
config = self.evaluate_config("info")

tree = None

# Extract machine information.
for x in tree.findall("attrs/attr[@name='machines']/attrs/attr"):
name = x.get("name")
cfg = config["machines"][name]
defn = _create_definition(x, cfg, cfg["targetEnv"])
for name, cfg in config["machines"].items():
defn = _create_definition(name, cfg, cfg["targetEnv"])
self.definitions[name] = defn

# Extract info about other kinds of resources.
for x in tree.findall("attrs/attr[@name='resources']/attrs/attr"):
res_type = x.get("name")
for y in x.findall("attrs/attr"):
name = y.get("name")
for res_type, cfg in config["resources"].items():
for name, y in cfg.items():
defn = _create_definition(
y, config["resources"][res_type][name], res_type
name, config["resources"][res_type][name], res_type
)
self.definitions[name] = defn

@@ -604,13 +594,13 @@ def do_machine(m: nixops.backends.MachineState) -> None:
attrs_list = attrs_per_resource[m.name]

# Set system.stateVersion if the Nixpkgs version supports it.
nixos_version = nixops.util.parse_nixos_version(defn.config["nixosRelease"])
nixos_version = nixops.util.parse_nixos_version(defn.config.nixosRelease)
if nixos_version >= ["15", "09"]:
attrs_list.append(
{
("system", "stateVersion"): Call(
RawValue("lib.mkDefault"),
m.state_version or defn.config["nixosRelease"],
m.state_version or defn.config.nixosRelease,
)
}
)
@@ -1674,16 +1664,14 @@ def _subclasses(cls: Any) -> List[Any]:
return [cls] if not sub else [g for s in sub for g in _subclasses(s)]


def _create_definition(xml: Any, config: Dict[str, Any], type_name: str) -> Any:
def _create_definition(
name: str, config: Dict[str, Any], type_name: str
) -> nixops.resources.ResourceDefinition:
"""Create a resource definition object from the given XML representation of the machine's attributes."""

for cls in _subclasses(nixops.resources.ResourceDefinition):
if type_name == cls.get_resource_type():
# FIXME: backward compatibility hack
if len(inspect.getargspec(cls.__init__).args) == 2:
return cls(xml)
else:
return cls(xml, config)
return cls(name, nixops.resources.ResourceEval(config))

raise nixops.deployment.UnknownBackend(
"unknown resource type ‘{0}’".format(type_name)
29 changes: 23 additions & 6 deletions nixops/resources/__init__.py
Original file line number Diff line number Diff line change
@@ -3,14 +3,25 @@
import re
import nixops.util
from threading import Event
from typing import List, Optional, Dict, Any
from typing import List, Optional, Dict, Any, Iterator
from nixops.state import StateDict
from nixops.diff import Diff, Handler
from nixops.util import ImmutableMapping, ImmutableValidatedObject


class ResourceDefinition(object):
class ResourceEval(ImmutableMapping[Any, Any]):
pass


class ResourceOptions(ImmutableValidatedObject):
pass


class ResourceDefinition:
"""Base class for NixOps resource definitions."""

config: ResourceOptions

@classmethod
def get_type(cls) -> str:
"""A resource type identifier that must match the corresponding ResourceState class"""
@@ -21,10 +32,16 @@ def get_resource_type(cls):
"""A resource type identifier corresponding to the resources.<type> attribute in the Nix expression"""
return cls.get_type()

def __init__(self, xml, config={}):
self.config = config
self.name = xml.get("name")
assert self.name
def __init__(self, name: str, config: ResourceEval):
config_type = self.__annotations__.get("config", ResourceOptions)
if not issubclass(config_type, ResourceOptions):
raise TypeError(
'"config" type annotation must be a ResourceOptions subclass'
)

self.config = config_type(**config)
self.name = name

if not re.match("^[a-zA-Z0-9_\-][a-zA-Z0-9_\-\.]*$", self.name):
raise Exception("invalid resource name ‘{0}’".format(self.name))

Loading