def test_nested(self): self.assertEquals(py2nix([nix2py("a\nb\nc")], maxwidth=0), "[\n (a\n b\n c)\n]") self.assertEquals( py2nix({"foo": nix2py("a\nb\nc"), "bar": nix2py("d\ne\nf")}, maxwidth=0), # ugly, but probably won't happen in practice "{\n bar = d\n e\n f;\n foo = a\n b\n c;\n}", )
def test_nested(self): self.assertEquals(py2nix([nix2py('a\nb\nc')], maxwidth=0), '[\n (a\n b\n c)\n]') self.assertEquals(py2nix({'foo': nix2py('a\nb\nc'), 'bar': nix2py('d\ne\nf')}, maxwidth=0), # ugly, but probably won't happen in practice '{\n bar = d\n e\n f;\n foo = a\n b\n c;\n}')
def _eval_flags(self, exprs): flags = self._nix_path_flags() args = {key: RawValue(val) for key, val in self.args.iteritems()} flags.extend( ["--arg", "networkExprs", py2nix(exprs, inline=True), "--arg", "args", py2nix(args, inline=True), "--argstr", "uuid", self.uuid, "<nixops/eval-machine-info.nix>"]) return flags
def test_nested(self): self.assertEqual( py2nix([nix2py("a\nb\nc")], maxwidth=0), "[\n (a\n b\n c)\n]" ) self.assertEqual( py2nix({"foo": nix2py("a\nb\nc"), "bar": nix2py("d\ne\nf")}, maxwidth=0), # ugly, but probably won't happen in practice "{\n bar = d\n e\n f;\n foo = a\n b\n c;\n}", )
def _eval_flags(self, exprs): flags = self._nix_path_flags() args = {key: RawValue(val) for key, val in self.args.iteritems()} exprs_ = [RawValue(x) if x[0] == '<' else x for x in exprs] flags.extend( ["--arg", "networkExprs", py2nix(exprs_, inline=True), "--arg", "args", py2nix(args, inline=True), "--argstr", "uuid", self.uuid, "<nixops/eval-deployment.nix>" if self.modular else "<nixops/eval-machine-info.nix>"]) return flags
def test_nested(self): self.assertEquals(py2nix([nix2py('a\nb\nc')], maxwidth=0), '[\n (a\n b\n c)\n]') self.assertEquals( py2nix({ 'foo': nix2py('a\nb\nc'), 'bar': nix2py('d\ne\nf') }, maxwidth=0), # ugly, but probably won't happen in practice '{\n bar = d\n e\n f;\n foo = a\n b\n c;\n}')
def destroy_resources(self, include=[], exclude=[], wipe=False): """Destroy all active or obsolete resources.""" with self._get_deployment_lock(): def worker(m): if not should_do(m, include, exclude): return if m.destroy(wipe=wipe): self.delete_resource(m) nixops.parallel.run_tasks(nr_workers=-1, tasks=self.resources.values(), worker_fun=worker) # Remove the destroyed machines from the rollback profile. # This way, a subsequent "nix-env --delete-generations old" or # "nix-collect-garbage -d" will get rid of the machine # configurations. if self.rollback_enabled: # and len(self.active) == 0: profile = self.create_profile() attrs = {m.name: Function("builtins.storePath", m.cur_toplevel, call=True) for m in self.active.itervalues() if m.cur_toplevel} if subprocess.call( ["nix-env", "-p", profile, "--set", "*", "-I", "nixops=" + self.expr_path, "-f", "<nixops/update-profile.nix>", "--arg", "machines", py2nix(attrs, inline=True)]) != 0: raise Exception("cannot update profile ‘{0}’".format(profile))
def destroy_resources(self, include=[], exclude=[], wipe=False): """Destroy all active or obsolete resources.""" with self._get_deployment_lock(): for r in self.resources.itervalues(): r._destroyed_event = threading.Event() r._errored = False for rev_dep in r.destroy_before(self.resources.itervalues()): try: rev_dep._wait_for.append(r) except AttributeError: rev_dep._wait_for = [ r ] def worker(m): try: if not should_do(m, include, exclude): return try: for dep in m._wait_for: dep._destroyed_event.wait() # !!! Should we print a message here? if dep._errored: m._errored = True return except AttributeError: pass if m.destroy(wipe=wipe): self.delete_resource(m) except: m._errored = True raise finally: m._destroyed_event.set() nixops.parallel.run_tasks(nr_workers=-1, tasks=self.resources.values(), worker_fun=worker) # Remove the destroyed machines from the rollback profile. # This way, a subsequent "nix-env --delete-generations old" or # "nix-collect-garbage -d" will get rid of the machine # configurations. if self.rollback_enabled: # and len(self.active) == 0: profile = self.create_profile() attrs = {m.name: Function("builtins.storePath", m.cur_toplevel, call=True) for m in self.active.itervalues() if m.cur_toplevel} if subprocess.call( ["nix-env", "-p", profile, "--set", "*", "-I", "nixops=" + self.expr_path, "-f", "<nixops/update-profile.nix>", "--arg", "machines", py2nix(attrs, inline=True)]) != 0: raise Exception("cannot update profile ‘{0}’".format(profile))
def destroy_resources(self, include=[], exclude=[], wipe=False): """Destroy all active and obsolete resources.""" with self._get_deployment_lock(): self._destroy_resources(include, exclude, wipe) # Remove the destroyed machines from the rollback profile. # This way, a subsequent "nix-env --delete-generations old" or # "nix-collect-garbage -d" will get rid of the machine # configurations. if self.rollback_enabled: # and len(self.active) == 0: profile = self.create_profile() attrs = {m.name: Call(RawValue("builtins.storePath"), m.cur_toplevel) for m in self.active.itervalues() if m.cur_toplevel} if subprocess.call( ["nix-env", "-p", profile, "--set", "*", "-I", "nixops=" + self.expr_path, "-f", "<nixops/update-profile.nix>", "--arg", "machines", py2nix(attrs, inline=True)]) != 0: raise Exception("cannot update profile ‘{0}’".format(profile))
def get_physical_spec(self): """Compute the contents of the Nix expression specifying the computed physical deployment attributes""" active_machines = self.active active_resources = self.active_resources attrs_per_resource = {m.name: [] for m in active_resources.itervalues()} authorized_keys = {m.name: [] for m in active_machines.itervalues()} kernel_modules = {m.name: set() for m in active_machines.itervalues()} trusted_interfaces = {m.name: set() for m in active_machines.itervalues()} # Hostnames should be accumulated like this: # # hosts[local_name][remote_ip] = [name1, name2, ...] # # This makes hosts deterministic and is more in accordance to the # format in hosts(5), which is like this: # # ip_address canonical_hostname [aliases...] # # This is critical for example when using host names for access # control, because the canonical_hostname is returned in reverse # lookups. hosts = defaultdict(lambda: defaultdict(list)) for m in active_machines.itervalues(): for m2 in active_machines.itervalues(): ip = m.address_to(m2) if ip: hosts[m.name][ip] += [m2.name, m2.name + "-unencrypted"] # Always use the encrypted/unencrypted suffixes for aliases rather # than for the canonical name! hosts[m.name]["127.0.0.1"].append(m.name + "-encrypted") def index_to_private_ip(index): n = 105 + index / 256 assert n <= 255 return "192.168.{0}.{1}".format(n, index % 256) def do_machine(m): defn = self.definitions[m.name] attrs_list = attrs_per_resource[m.name] # Emit configuration to realise encrypted peer-to-peer links. for m2_name in defn.encrypted_links_to: if m2_name not in active_machines: raise Exception("‘deployment.encryptedLinksTo’ in machine ‘{0}’ refers to an unknown machine ‘{1}’" .format(m.name, m2_name)) m2 = active_machines[m2_name] # Don't create two tunnels between a pair of machines. if m.name in self.definitions[m2.name].encrypted_links_to and m.name >= m2.name: continue local_ipv4 = index_to_private_ip(m.index) remote_ipv4 = index_to_private_ip(m2.index) local_tunnel = 10000 + m2.index remote_tunnel = 10000 + m.index attrs_list.append({ ('networking', 'p2pTunnels', 'ssh', m2.name): { 'target': '{0}-unencrypted'.format(m2.name), 'localTunnel': local_tunnel, 'remoteTunnel': remote_tunnel, 'localIPv4': local_ipv4, 'remoteIPv4': remote_ipv4, 'privateKey': '/root/.ssh/id_charon_vpn', } }) # FIXME: set up the authorized_key file such that ‘m’ # can do nothing more than create a tunnel. authorized_keys[m2.name].append(m.public_vpn_key) kernel_modules[m.name].add('tun') kernel_modules[m2.name].add('tun') hosts[m.name][remote_ipv4] += [m2.name, m2.name + "-encrypted"] hosts[m2.name][local_ipv4] += [m.name, m.name + "-encrypted"] trusted_interfaces[m.name].add('tun' + str(local_tunnel)) trusted_interfaces[m2.name].add('tun' + str(remote_tunnel)) private_ipv4 = m.private_ipv4 if private_ipv4: attrs_list.append({ ('networking', 'privateIPv4'): private_ipv4 }) public_ipv4 = m.public_ipv4 if public_ipv4: attrs_list.append({ ('networking', 'publicIPv4'): public_ipv4 }) if self.nixos_version_suffix: attrs_list.append({ ('system', 'nixosVersionSuffix'): self.nixos_version_suffix }) for m in active_machines.itervalues(): do_machine(m) def emit_resource(r): config = [] config.extend(attrs_per_resource[r.name]) if is_machine(r): # Sort the hosts by its canonical host names. sorted_hosts = sorted(hosts[r.name].iteritems(), key=lambda item: item[1][0]) # Just to remember the format: # ip_address canonical_hostname [aliases...] extra_hosts = ["{0} {1}".format(ip, ' '.join(names)) for ip, names in sorted_hosts] if authorized_keys[r.name]: config.append({ ('users', 'extraUsers', 'root'): { ('openssh', 'authorizedKeys', 'keys'): authorized_keys[r.name] }, ('services', 'openssh'): { 'extraConfig': "PermitTunnel yes\n" }, }) config.append({ ('boot', 'kernelModules'): list(kernel_modules[r.name]), ('networking', 'firewall'): { 'trustedInterfaces': list(trusted_interfaces[r.name]) }, ('networking', 'extraHosts'): '\n'.join(extra_hosts) + "\n" }) # Add SSH public host keys for all machines in network for m2 in active_machines.itervalues(): if hasattr(m2, 'public_host_key') and m2.public_host_key: # Using references to files in same tempdir for now, until NixOS has support # for adding the keys directly as string. This way at least it is compatible # with older versions of NixOS as well. # TODO: after reasonable amount of time replace with string option config.append({ ('services', 'openssh', 'knownHosts', m2.name): { 'hostNames': [m2.name + "-unencrypted", m2.name + "-encrypted", m2.name], 'publicKeyFile': RawValue( "./{0}.public_host_key".format(m2.name) ), } }) merged = reduce(nixmerge, config) if len(config) > 0 else {} physical = r.get_physical_spec() if len(merged) == 0 and len(physical) == 0: return {} else: return r.prefix_definition({ r.name: Function("{ config, pkgs, ... }", { 'config': merged, 'imports': [physical], }) }) return py2nix(reduce(nixmerge, [ emit_resource(r) for r in active_resources.itervalues() ])) + "\n"
def set_argstr(self, name, value): """Set a persistent argument to the deployment specification.""" assert isinstance(value, basestring) self.set_arg(name, py2nix(value, inline=True))
def assert_nix(self, nix_expr, expected, maxwidth=80, inline=False): result = py2nix(nix_expr, maxwidth=maxwidth, inline=inline) self.assertEqual(result, expected, "Expected:\n{0}\nGot:\n{1}".format(expected, result))
def test_simple(self): self.assertEquals(py2nix(nix2py("{\na = b;\n}"), maxwidth=0), "{\na = b;\n}") self.assertEquals(py2nix(nix2py("\n{\na = b;\n}\n"), maxwidth=0), "{\na = b;\n}")
def print_physical_backup_spec(backupid): depl = open_deployment(args) config = {} for m in depl.active.itervalues(): config[m.name] = m.get_physical_backup_spec(backupid) sys.stdout.write(py2nix(config))
gatewayId = resources.vpcInternetGateways.igw-test; }; vpcInternetGateways.igw-test = { resources, ... }: { inherit region; vpcId = resources.vpc.vpc-test; }; }; } """) CFG_DNS_SUPPORT = ("enable_dns_support.nix", py2nix({ ('resources', 'vpc', 'vpc-test', 'enableDnsSupport'): True })) CFG_IPV6 = ( "ipv6.nix", py2nix({ ('resources', 'vpc', 'vpc-test', 'amazonProvidedIpv6CidrBlock'): True })) CFG_NAT_GTW = ("nat_gtw.nix", """ { resources.elasticIPs.nat-eip = { region = "us-east-1"; vpc = true;
def get_physical_spec(self): """Compute the contents of the Nix expression specifying the computed physical deployment attributes""" active_machines = self.active active_resources = self.active_resources attrs_per_resource = {m.name: [] for m in active_resources.itervalues()} authorized_keys = {m.name: [] for m in active_machines.itervalues()} kernel_modules = {m.name: set() for m in active_machines.itervalues()} trusted_interfaces = {m.name: set() for m in active_machines.itervalues()} # Hostnames should be accumulated like this: # # hosts[local_name][remote_ip] = [name1, name2, ...] # # This makes hosts deterministic and is more in accordance to the # format in hosts(5), which is like this: # # ip_address canonical_hostname [aliases...] # # This is critical for example when using host names for access # control, because the canonical_hostname is returned in reverse # lookups. hosts = defaultdict(lambda: defaultdict(list)) def index_to_private_ip(index): n = 105 + index / 256 assert n <= 255 return "192.168.{0}.{1}".format(n, index % 256) def do_machine(m): defn = self.definitions[m.name] attrs_list = attrs_per_resource[m.name] # Emit configuration to realise encrypted peer-to-peer links. for m2 in active_resources.itervalues(): ip = m.address_to(m2) if ip: hosts[m.name][ip] += [m2.name, m2.name + "-unencrypted"] # Always use the encrypted/unencrypted suffixes for aliases rather # than for the canonical name! hosts[m.name]["127.0.0.1"].append(m.name + "-encrypted") for m2_name in defn.encrypted_links_to: if m2_name not in active_machines: raise Exception( "‘deployment.encryptedLinksTo’ in machine ‘{0}’ refers to an unknown machine ‘{1}’".format( m.name, m2_name ) ) m2 = active_machines[m2_name] # Don't create two tunnels between a pair of machines. if m.name in self.definitions[m2.name].encrypted_links_to and m.name >= m2.name: continue local_ipv4 = index_to_private_ip(m.index) remote_ipv4 = index_to_private_ip(m2.index) local_tunnel = 10000 + m2.index remote_tunnel = 10000 + m.index attrs_list.append( { ("networking", "p2pTunnels", "ssh", m2.name): { "target": "{0}-unencrypted".format(m2.name), "targetPort": m2.ssh_port, "localTunnel": local_tunnel, "remoteTunnel": remote_tunnel, "localIPv4": local_ipv4, "remoteIPv4": remote_ipv4, "privateKey": "/root/.ssh/id_charon_vpn", } } ) # FIXME: set up the authorized_key file such that ‘m’ # can do nothing more than create a tunnel. authorized_keys[m2.name].append(m.public_vpn_key) kernel_modules[m.name].add("tun") kernel_modules[m2.name].add("tun") hosts[m.name][remote_ipv4] += [m2.name, m2.name + "-encrypted"] hosts[m2.name][local_ipv4] += [m.name, m.name + "-encrypted"] trusted_interfaces[m.name].add("tun" + str(local_tunnel)) trusted_interfaces[m2.name].add("tun" + str(remote_tunnel)) private_ipv4 = m.private_ipv4 if private_ipv4: attrs_list.append({("networking", "privateIPv4"): private_ipv4}) public_ipv4 = m.public_ipv4 if public_ipv4: attrs_list.append({("networking", "publicIPv4"): public_ipv4}) public_vpn_key = m.public_vpn_key if public_vpn_key: attrs_list.append({("networking", "vpnPublicKey"): public_vpn_key}) # Set system.stateVersion if the Nixpkgs version supports it. if nixops.util.parse_nixos_version(defn.config["nixosVersion"]) >= ["15", "09"]: attrs_list.append( {("system", "stateVersion"): Call(RawValue("lib.mkDefault"), m.state_version or "14.12")} ) if self.nixos_version_suffix: attrs_list.append({("system", "nixosVersionSuffix"): self.nixos_version_suffix}) for m in active_machines.itervalues(): do_machine(m) def emit_resource(r): config = [] config.extend(attrs_per_resource[r.name]) if is_machine(r): # Sort the hosts by its canonical host names. sorted_hosts = sorted(hosts[r.name].iteritems(), key=lambda item: item[1][0]) # Just to remember the format: # ip_address canonical_hostname [aliases...] extra_hosts = ["{0} {1}".format(ip, " ".join(names)) for ip, names in sorted_hosts] if authorized_keys[r.name]: config.append( { ("users", "extraUsers", "root"): { ("openssh", "authorizedKeys", "keys"): authorized_keys[r.name] }, ("services", "openssh"): {"extraConfig": "PermitTunnel yes\n"}, } ) config.append( { ("boot", "kernelModules"): list(kernel_modules[r.name]), ("networking", "firewall"): {"trustedInterfaces": list(trusted_interfaces[r.name])}, ("networking", "extraHosts"): "\n".join(extra_hosts) + "\n", } ) # Add SSH public host keys for all machines in network. for m2 in active_machines.itervalues(): if hasattr(m2, "public_host_key") and m2.public_host_key: # Using references to files in same tempdir for now, until NixOS has support # for adding the keys directly as string. This way at least it is compatible # with older versions of NixOS as well. # TODO: after reasonable amount of time replace with string option config.append( { ("services", "openssh", "knownHosts", m2.name): { "hostNames": [m2.name + "-unencrypted", m2.name + "-encrypted", m2.name], "publicKey": m2.public_host_key, } } ) merged = reduce(nixmerge, config) if len(config) > 0 else {} physical = r.get_physical_spec() if len(merged) == 0 and len(physical) == 0: return {} else: return r.prefix_definition( {r.name: Function("{ config, lib, pkgs, ... }", {"config": merged, "imports": [physical]})} ) return py2nix(reduce(nixmerge, [emit_resource(r) for r in active_resources.itervalues()], {})) + "\n"
def test_simple(self): self.assertEquals(py2nix(nix2py('{\na = b;\n}'), maxwidth=0), '{\na = b;\n}') self.assertEquals(py2nix(nix2py('\n{\na = b;\n}\n'), maxwidth=0), '{\na = b;\n}')
def build_configs(self, include, exclude, dry_run=False, repair=False): """Build the machine configurations in the Nix store.""" def write_temp_file(tmpfile, contents): f = open(tmpfile, "w") f.write(contents) f.close() self.logger.log("building all machine configurations...") # Set the NixOS version suffix, if we're building from Git. # That way ‘nixos-version’ will show something useful on the # target machines. nixos_path = subprocess.check_output( ["nix-instantiate", "--find-file", "nixpkgs/nixos"] + self._nix_path_flags()).rstrip() get_version_script = nixos_path + "/modules/installer/tools/get-version-suffix" if os.path.exists(nixos_path + "/.git") and os.path.exists(get_version_script): self.nixos_version_suffix = subprocess.check_output(["/bin/sh", get_version_script] + self._nix_path_flags()).rstrip() phys_expr = self.tempdir + "/physical.nix" p = self.get_physical_spec() write_temp_file(phys_expr, p) if debug: print >> sys.stderr, "generated physical spec:\n" + p for m in self.active.itervalues(): if hasattr(m, "public_host_key") and m.public_host_key: # FIXME: use a method in MachineState. write_temp_file("{0}/{1}.public_host_key".format(self.tempdir, m.name), m.public_host_key + "\n") selected = [m for m in self.active.itervalues() if should_do(m, include, exclude)] names = map(lambda m: m.name, selected) # If we're not running on Linux, then perform the build on the # target machines. FIXME: Also enable this if we're on 32-bit # and want to deploy to 64-bit. if platform.system() != 'Linux' and os.environ.get('NIX_REMOTE') != 'daemon': remote_machines = [] for m in sorted(selected, key=lambda m: m.index): key_file = m.get_ssh_private_key_file() if not key_file: raise Exception("do not know private SSH key for machine ‘{0}’".format(m.name)) # FIXME: Figure out the correct machine type of ‘m’ (it might not be x86_64-linux). remote_machines.append("root@{0} {1} {2} 2 1\n".format(m.get_ssh_name(), 'i686-linux,x86_64-linux', key_file)) # Use only a single machine for now (issue #103). break remote_machines_file = "{0}/nix.machines".format(self.tempdir) with open(remote_machines_file, "w") as f: f.write("".join(remote_machines)) os.environ['NIX_REMOTE_SYSTEMS'] = remote_machines_file # FIXME: Use ‘--option use-build-hook true’ instead of setting # $NIX_BUILD_HOOK, once Nix supports that. os.environ['NIX_BUILD_HOOK'] = os.path.dirname(os.path.realpath(nixops.util.which("nix-build"))) + "/../libexec/nix/build-remote.pl" load_dir = "{0}/current-load".format(self.tempdir) if not os.path.exists(load_dir): os.makedirs(load_dir, 0700) os.environ['NIX_CURRENT_LOAD'] = load_dir try: configs_path = subprocess.check_output( ["nix-build"] + self._eval_flags(self.nix_exprs + [phys_expr]) + ["--arg", "names", py2nix(names, inline=True), "-A", "machines", "-o", self.tempdir + "/configs"] + (["--dry-run"] if dry_run else []) + (["--repair"] if repair else []), stderr=self.logger.log_file).rstrip() except subprocess.CalledProcessError: raise Exception("unable to build all machine configurations") if self.rollback_enabled: profile = self.create_profile() if subprocess.call(["nix-env", "-p", profile, "--set", configs_path]) != 0: raise Exception("cannot update profile ‘{0}’".format(profile)) return configs_path
def eval( # eval-machine-info args networkExpr: NetworkFile, # Flake conditional uuid: str, deploymentName: str, networkExprs: List[str] = [], args: Dict[str, str] = {}, pluginNixExprs: List[str] = [], checkConfigurationOptions: bool = True, # Extend internal defaults nix_path: List[str] = [], # nix-instantiate args nix_args: Dict[str, Any] = {}, attr: Optional[str] = None, extra_flags: List[str] = [], # Non-propagated args stderr: Optional[TextIO] = None, ) -> Any: exprs: List[str] = list(networkExprs) if not networkExpr.is_flake: exprs.append(networkExpr.network) argv: List[str] = ( [ "nix-instantiate", "--eval-only", "--json", "--strict", "--show-trace" ] + [os.path.join(get_expr_path(), "eval-machine-info.nix")] + ["-I", "nixops=" + get_expr_path()] + [ "--arg", "networkExprs", py2nix([RawValue(x) if x[0] == "<" else x for x in exprs]), ] + [ "--arg", "args", py2nix({key: RawValue(val) for key, val in args.items()}, inline=True), ] + ["--argstr", "uuid", uuid] + ["--argstr", "deploymentName", deploymentName] + ["--arg", "pluginNixExprs", py2nix(pluginNixExprs)] + [ "--arg", "checkConfigurationOptions", json.dumps(checkConfigurationOptions) ] + list(itertools.chain(*[["-I", x] for x in (nix_path + pluginNixExprs)])) + extra_flags) for k, v in nix_args.items(): argv.extend(["--arg", k, py2nix(v, inline=True)]) if attr: argv.extend(["-A", attr]) if networkExpr.is_flake: argv.extend(["--allowed-uris", get_expr_path()]) argv.extend(["--argstr", "flakeUri", networkExpr.network]) try: ret = subprocess.check_output(argv, stderr=stderr, text=True) return json.loads(ret) except OSError as e: raise Exception("unable to run ‘nix-instantiate’: {0}".format(e)) except subprocess.CalledProcessError: raise NixEvalError
destinationCidrBlock = "0.0.0.0/0"; gatewayId = resources.vpcInternetGateways.igw-test; }; vpcInternetGateways.igw-test = { resources, ... }: { inherit region; vpcId = resources.vpc.vpc-test; }; }; } """) CFG_DNS_SUPPORT = ("enable_dns_support.nix", py2nix({ ('resources', 'vpc', 'vpc-test', 'enableDnsSupport'): True })) CFG_IPV6 = ("ipv6.nix", py2nix({ ('resources', 'vpc', 'vpc-test', 'amazonProvidedIpv6CidrBlock'): True })) CFG_NAT_GTW = ("nat_gtw.nix", """ { resources.elasticIPs.nat-eip = { region = "us-east-1"; vpc = true; }; resources.vpcNatGateways.nat =