1
0
forked from dan/liminix

Compare commits

...

150 Commits

Author SHA1 Message Date
dan dc3ade7969 commentary 2024-05-14 22:27:37 +01:00
dan 7ece8dde5e add hacky wwan service with hardcoding all over 2024-05-14 22:19:34 +01:00
dan 5a19af318e ref cdc-ncm module by corretc name 9suash) 2024-05-14 21:57:28 +01:00
dan 6d36f0eb13 remove leftovers from arhcive example 2024-05-14 18:08:53 +01:00
dan 5d246e1f1a create cdc-ncm module 2024-05-14 18:07:33 +01:00
dan 0e2e44c6ee barebones usb_modeswitch package 2024-05-14 12:56:58 +01:00
dan e2c5615677 l2tp set default route via tunnel 2024-05-14 12:52:50 +01:00
dan ab59c852e2 exec xl2tpd
haven't fully worked out why, but without this s6 is unable to stop it.
2024-05-13 18:41:06 +01:00
dan 98d198960b think 2024-05-13 18:41:06 +01:00
dan d98f011292 add rudimentary l2tp service module 2024-05-13 18:41:06 +01:00
dan 991de7eb9e bordervm enable nat 2024-05-13 18:41:06 +01:00
dan 513c69cb5b gl-ar750 appendDTB 2024-05-13 18:41:06 +01:00
dan 15856d224c memorable net device names for gl-ar750
linux's view of eth1 and eth0 are opposite to that of u-boot
2024-05-13 18:41:06 +01:00
dan abbdda7b30 run dhcp server on bordervm
this is for testing clients that have dhcp upstream
2024-05-13 18:41:06 +01:00
dan e56b761ba3 rename overlaid dnsmasq package -> dnsmasqSmall
avoid clobbering the regular one
2024-05-13 18:41:06 +01:00
dan 15ec172739 tftp addresses 2024-05-13 18:41:06 +01:00
dan 6ddba0f68b think 2024-05-13 18:41:06 +01:00
dan 471c63b399 s6-rc do cleanup in "finish", don't append to "run" script
s6-supervise sends signals (e.g. SIGTERM) to the pid of the process
running "run", so how do we know if the ceanup commands are even
getting executed if the shell interpreter that is supposed to do that
got killed already?
2024-05-13 17:53:02 +01:00
dan 782feaeafa set default for firewall extraRules 2024-05-03 16:28:53 +01:00
dan ac54c89427 add busybox to bordervm for udhcpd 2024-05-01 23:09:23 +01:00
dan 5a3646cb29 add authorized keys to bordervm
You don't often need this because it has autologin, but sometimes
you want to do antics involving sshing through it to the wan port
of a test device.

Note that you probably wanted to start bordervm with funny qemu
options to even make that possible

 nix-shell --run "QEMU_NET_OPTS=hostfwd=tcp::10022-:22 run-border-vm"
2024-05-01 23:07:11 +01:00
dan e249f48cff add deps on {ins,rm}mod and kconfig for firewall module 2024-05-01 23:06:12 +01:00
dan 6661e42684 mt300a tftpboot needs appendDTB 2024-05-01 23:04:25 +01:00
dan b9ba9ef835 mt300a remove unneeded service dependencies 2024-05-01 23:03:55 +01:00
dan 8b69dcc209 pass entire config fragment to levitate, not just services
to make it useful we need to be able to set packages, passwords, ssh
keys etc
2024-04-29 20:07:01 +01:00
dan 9b3a3b9ff7 add levitate to arhcive
this is largely untested
2024-04-28 21:38:13 +01:00
dan 7d08497bcb arhcive remove coldplug fudge 2024-04-28 21:37:30 +01:00
dan 0e84adaa0e maybe don't need deps for gl-mt300a vlan devices?
will delete them next time I have that device open to test
2024-04-28 21:35:09 +01:00
dan 660ed5df8f vlan interface services depend on primary 2024-04-28 21:33:36 +01:00
dan 792a11c8c0 gl-mt300n-v2 use full path to swconfig in service stop 2024-04-28 21:32:42 +01:00
dan 7e4a05bbf8 separate kernel and base modules
this is needed for levitate
2024-04-28 12:44:27 +01:00
dan a4ba5c85e1 alphabetize list in all-modules 2024-04-28 12:42:47 +01:00
dan 723ef73d5a inout: test hotplug and coldplug 2024-04-27 22:41:30 +01:00
dan 3d4e782929 devout: run tests in postBuild
because checkPhase is not executed when cross-compiling, and this
package is always only cross-compiled
2024-04-27 21:07:25 +01:00
dan 1b6a05aec5 make uevent-watch use devout instead of direct netlink 2024-04-27 21:07:25 +01:00
dan 80628a3d90 move event matching tests to devout
in preparation for future uevent-watch not needing to do
event matching
2024-04-27 21:07:25 +01:00
dan bf0cafffed start devout alongside mdevd
ensure it starts before mdevd-coldplug so it can populate
its database
2024-04-26 20:52:12 +01:00
dan e49aba127c devout: improve socket error handling 2024-04-26 20:49:23 +01:00
dan 324465bc18 devout: write uevent KEY=value format to clients 2024-04-26 17:37:28 +01:00
dan b33249a050 devout: add readiness notification 2024-04-26 17:23:29 +01:00
dan b9c084415e devout: handle readiness on netlink socket but no event 2024-04-26 17:20:33 +01:00
dan cf9cadd212 devout: replay relevant events to new subscriber 2024-04-26 17:20:33 +01:00
dan a116fe084a devout: use socket constants from anoia.net.constants 2024-04-26 16:48:51 +01:00
dan 74cf3e0711 add anoia.net.constants for SOCK_{STREAM,DGRAM} etc
we use an ugly bit of C preprocessor to get the values from
header files, because certain constants are different on MIPS
than on other architectures
2024-04-26 16:43:09 +01:00
dan 9795f03da4 think 2024-04-26 16:41:31 +01:00
dan cdb23b147c convert anoia.fs to use lualinux 2024-04-25 21:14:37 +01:00
dan dbd1264352 convert anoia.fs to use lualinux instead of lfs 2024-04-24 20:44:32 +01:00
dan 834858d5bc think 2024-04-24 18:33:57 +01:00
dan 18335b95e3 devout: strip newlines from client terms
this is just to make testing with socat easier
2024-04-24 18:33:02 +01:00
dan 6bee2f67ac devout: add incoming netlink messages to database 2024-04-24 18:32:27 +01:00
dan b4ba3eea21 fix revents in unpack-pollfds 2024-04-24 18:31:26 +01:00
dan 16af3984c9 add lualinux to fennelrepl 2024-04-24 18:30:34 +01:00
dan ce7e395295 devout test: replace minisock with lualinux 2024-04-24 18:29:24 +01:00
dan 7e13e017eb add readline suport to fennelrepl 2024-04-24 18:28:39 +01:00
dan bbf2f53c0e cross-compile lualinux 2024-04-24 18:28:14 +01:00
dan 032d0f8aca add netlink socket
it's not hooked up to anything yet, but it proves we can
do this with lualinux
2024-04-23 23:34:25 +01:00
dan b8ac9e5279 convert devout from minisock to lualinux 2024-04-23 23:33:11 +01:00
dan ff2604ca5d think 2024-04-23 23:30:50 +01:00
dan 72789984ce add lualinux package 2024-04-23 22:41:38 +01:00
dan 90d9d0e811 update minisock to not scribble on lua strings 2024-04-23 20:19:33 +01:00
dan 97a8ae1c84 devout: add event loop and main run function 2024-04-23 20:15:02 +01:00
dan 52eb283a26 implement unsubscribe
and add ids to subscribe so that there's a unique identifier
to pass to unsubscribe
2024-04-23 20:12:46 +01:00
dan cbb1de804e switch to minisock fork witj poll() call
this is likely to be temporary as minisock is getting
replaced with lualinux
2024-04-23 20:09:41 +01:00
dan f9c03998b8 implement subscriptions with callback 2024-04-21 13:19:17 +01:00
dan 50de1b090f add the rest of the test list (all we've thought of) 2024-04-21 11:22:26 +01:00
dan 648382f64a report bodyless tests as PENDING 2024-04-21 11:19:42 +01:00
dan e9370358ae implement "remove" events 2024-04-21 11:19:06 +01:00
dan 762ce7b6b8 cut/paste devout implementation into a real module 2024-04-20 22:48:00 +01:00
dan b1c0560f4f implement fetch by path 2024-04-20 22:20:43 +01:00
dan e34135c41a improve failed test reporting 2024-04-20 21:46:37 +01:00
dan 712c9b266f implement find 2024-04-20 18:42:42 +01:00
dan 4df963996c devout: add device 2024-04-20 18:24:10 +01:00
dan 349bfecbb8 new package "devout", does nothing yet 2024-04-20 17:45:40 +01:00
dan 450d3820b2 clean up uevent-watch test using writeFennel and mainFunction
requires less cavorting with globals and stuff
2024-04-20 16:53:43 +01:00
dan 771585546d import expect= where previously it was copy-pasted 2024-04-20 15:09:50 +01:00
dan 73abf952d5 package minisock, a minimal Lua socket library 2024-04-20 15:09:17 +01:00
dan 8af4e9fd5b package anoia assert macros and point fennelrepl at them 2024-04-20 14:59:14 +01:00
dan 7e19d80130 anoia: add assert macro module
contains expect and expect=
2024-04-20 14:04:32 +01:00
dan 0f0688c802 think 2024-04-20 14:03:48 +01:00
dan b43f17f655 think 2024-04-20 12:23:04 +01:00
dan adf62d4483 arhcive: make it work when disk is attached before boot
This is a bit of a kludge (a lot of a kludge) but it will
get it running whilt I work on something better
2024-04-17 18:49:30 +01:00
dan 68eb1360f6 use appended dtb in gl-mt300n-v2 tftpboot
probably the A variant needs this as well
2024-04-17 18:48:19 +01:00
dan 19ad6cd278 watchdog: put s6 pkg on $PATH for s6-svstat 2024-04-17 13:01:10 +01:00
dan 00076c7b81 mount service: use uevent-watch 2024-04-17 12:59:13 +01:00
dan 721e7499f3 arhcive: use usb module instead of harcoded kconfig 2024-04-17 12:53:43 +01:00
dan fc723b9a35 think 2024-04-16 18:59:01 +01:00
dan a5f16dfa81 convert inout test to use uevent-watch 2024-04-15 22:15:27 +01:00
dan 41a4b1f7ef clean cruft from inout test script 2024-04-15 22:00:44 +01:00
dan 42a5699326 remove unneeded config from inout test 2024-04-15 21:19:18 +01:00
dan ea2b25168e add uevent-watch, which toggles services based on uevent msgs 2024-04-15 21:15:07 +01:00
dan 5564cf0554 add nellie.close 2024-04-14 22:45:29 +01:00
dan f3a13630d3 add multicast groups param to nellie.open 2024-04-14 22:45:29 +01:00
dan f233acf9ff netlink uevent hello world 2024-04-14 22:45:29 +01:00
dan b6a054c588 add mdevd as module
following the upstream example, it republishes uevent messages
using multicast group 4 instead of group 2 as used by udev.
2024-04-14 21:59:23 +01:00
dan b231664a06 anoia: add basename, dirname 2024-04-11 23:11:20 +01:00
dan f4bf3029fa anoia: alphabetize exports 2024-04-11 23:11:13 +01:00
dan 05f2c9a2f7 add lua in nix-shell environment 2024-04-11 23:11:06 +01:00
dan 5df5c822ea convert mount service to trigger
Good: this means it's not hanging holding the s6 dataase lock.

Bad: it's the ugliest implementation and doesn't deserve to be preserved

(tbf the ugliness is not new)
2024-04-03 23:17:36 +01:00
dan 4795dd05b7 unconditionally restart trigger services on liminix-rebuild
We call s6-rc -u -p default to restart/start the base services
on a rebuild, otherwise services that are only in the new
configuration won't come up. However, this stops any service
started by a trigger. So, workaround is to restart the trigger
service and expect it to restart the services it manages if they're
needed
2024-04-03 23:07:56 +01:00
dan a192f08881 remove missing module 2024-03-29 17:34:10 +00:00
dan a873dc6608 Merge commit 'efcfdcc' 2024-03-28 23:47:04 +00:00
dan 2fb4756a7f add soft restart option to liminix-rebuild
instead of doing a full reboot, it runs activate / and uses
s6-rc-update to install the new service database
2024-03-28 23:45:10 +00:00
dan 04f5174425 fix vanilla-configuration defaultroute 2024-03-28 22:13:21 +00:00
dan dca2e4def1 fix params to s6-rc-init
flags must precede scandir otherwise they're ignored
2024-03-28 21:56:28 +00:00
dan b60126775a improve liminix-rebuild test
* make it executable
* improve robustness
* do't hardcode services.default (why did it do this?)
2024-03-28 21:37:47 +00:00
dan 76f11bcc93 liminix-rebuild: remove -f flag from reboot call
now we have timeouts in service definitions, shouldn't need this
any more
2024-03-28 21:37:47 +00:00
dan efcfdcc21d think 2024-03-28 20:59:39 +00:00
dan 77f1a78331 ifwait block if s6-rc lock is held
otherwise it doesn't trigger the service if something else is
slow to start
2024-03-28 20:59:39 +00:00
dan 28a5dec7dd implement ifwait trigger service and use in bridge
should we convert all ifwait uses to this trigger too? seems
reasonable
2024-03-28 20:59:39 +00:00
dan fad0a47b75 add config.system.callService
this is like pkgs.callService except that it passes
config.system.service as a param so that the service
being defined can invoke other services

if this proves to be a good idea, all uses of
pkgs.callService should be changed to use it instead
2024-03-28 20:59:39 +00:00
dan af52aafc84 deep thoughts 2024-03-28 20:59:39 +00:00
dan 34442b6069 failing test for ifwait 2024-03-28 20:59:39 +00:00
dan b8a46fc05e allow buildInputs param to s6 service
this is in preparation for trigger services that need to
close over the triggered service without adding it to
s6-rc dependencies
2024-03-28 20:58:53 +00:00
dan 8ac2c6cec1 support timeouts (default 30s) for starting s6-rc services 2024-03-28 20:58:47 +00:00
dan 8879b2d1ba fix rt2x00 wifi 2024-03-28 20:58:39 +00:00
dan 83e346d5a0 add deviceName param 2024-03-22 21:55:44 +00:00
dan 156b1fe64a deep thoughts 2024-03-22 21:54:38 +00:00
dan 1a314e55b7 firewall module: provide default rules and merge extraRules
a firewall with no configuration will get a relatively sane ruleset. a
firewall with `extraRules` will get them deep merged into the default
rules.  Specifying `rules` will override the defaults
2024-03-21 12:00:34 +00:00
dan 9263b21faa create gateway profile by extracting from rotuer example 2024-03-21 10:04:42 +00:00
dan 0a820a702a extneder: delete nftables kernel config
don't need nftables on a bridge. (do we? hope not)
2024-03-20 19:05:31 +00:00
dan 4ea518e296 expose modulesPath to ease out-of-tree configuration.nix 2024-03-20 18:58:44 +00:00
dan 98318b450d deep thoughts 2024-03-16 20:16:49 +00:00
dan e4ac7f19dc fix ifwait deps 2024-03-16 20:16:49 +00:00
dan 9c22744850 deep thoughts 2024-03-16 20:16:49 +00:00
dan c697be8c28 temporary fix for cmake cross-compilation 2024-03-16 20:16:49 +00:00
dan 202a37221a Merge pull request 'tftpboot: use commandLineDtbNode' (#11) from flokli/liminix:tftpboot-honor-commandLineDtbNode into main
Reviewed-on: dan/liminix#11
2024-03-16 18:18:18 +00:00
flokli 436eb03a7b tftpboot: use commandLineDtbNode
config.boot.commandLineDtbNode can be set from `bootargs` to
`bootargs-override` (used for boards where the u-boot on the board does
set `bootargs` on its own).

In that case, the code updating the cmdline for tftpboot purposes also
needs to update this node, not the `bootargs` node.

Otherwise the kernel won't find the phram device, as it never heard
about it, as it didn't get the necessary cmdline options.
2024-03-16 20:06:38 +02:00
dan e5963ae3f7 deep thoughts 2024-03-06 23:19:47 +00:00
dan f164f19d95 service starts and stops 2024-03-06 23:19:47 +00:00
dan dd4ab41f6a rename run-event 2024-03-06 23:19:47 +00:00
dan 5d5dff6729 WIP add failing test that service starts 2024-03-06 23:19:47 +00:00
dan 570d29c368 pass command line params to run instead of reffing global 2024-03-06 23:19:47 +00:00
dan 725af00dc9 improve test for dummy0 up
if we run off the end of the events fixture, it didn't work
2024-03-06 23:19:47 +00:00
dan e1b932ec27 remove hardcoded filename in test event generator 2024-03-06 23:19:47 +00:00
dan 7173b6fb1c don't call os.exit 2024-03-06 23:19:47 +00:00
dan ed9548f21d pass event producer fn as param 2024-03-06 23:19:47 +00:00
dan 0787807a7f ifwait: don't run on load if in test harness 2024-03-06 23:19:47 +00:00
dan 38ed91f641 simplify assertion 2024-03-06 23:19:47 +00:00
dan ffe9603c39 remove file-scoped parameters var 2024-03-06 23:19:47 +00:00
dan cbd3dfefc5 ifwait fixture/test harness 2024-03-06 23:19:47 +00:00
dan 018c1868b5 ifwait: use anoia.assoc 2024-03-06 23:19:47 +00:00
dan 5184ff63f7 add anoia.nl, a convenience wrapper on netlink 2024-03-06 23:19:47 +00:00
dan 35909c9a23 add netlink to fennelrepl 2024-03-06 23:19:47 +00:00
dan 4383462199 deep thoughts 2024-03-06 23:19:47 +00:00
dan 9730cdd63b add assoc to anoia 2024-03-06 23:19:47 +00:00
dan 095853214b Merge pull request 'Fix kernel build on belkin' (#10) from sinavir/liminix:fix_kernel_build_on_belkin into main
Reviewed-on: dan/liminix#10
2024-03-06 18:21:13 +00:00
dan 9d6e50cbbc extract extneder example to a "profile"
this is a bit of an experiment to reduce the copy-paste in
examples by turning them into "application" modules.

planning to follow up with another module for "wifi router"
2024-02-27 23:13:12 +00:00
dan 94dbc56595 fix doc 2024-02-27 20:08:30 +00:00
dan 2cd7f932eb alignment may be null 2024-02-27 19:47:46 +00:00
dan 3ca0d87c27 ci.nix: alphabetise systems 2024-02-21 19:49:14 +00:00
94 changed files with 3374 additions and 467 deletions
+20 -1
View File
@@ -83,4 +83,23 @@ sponsoring this development (and funding the hardware)
2024-02-21
New port! Thanks to Raito Bezarius, Liminix now runs on the Zyxel NWA50AX,
an MT7621 (MIPS EL) dual radio WiFi AP.
an MT7621 (MIPS EL) dual radio WiFi AP.
2024-04-29
The setup for using `levitate` has changed: now it accepts an entire
config fragment, not just a list of services. Hopefully this makes it
a bit more useful :-)
defaultProfile.packages = with pkgs; [
...
(levitate.override {
config = {
services = {
inherit (config.services) dhcpc sshd watchdog;
};
defaultProfile.packages = [ mtdutils ];
users.root.openssh.authorizedKeys.keys = secrets.root.keys;
};
})
];
+714
View File
@@ -4137,3 +4137,717 @@ p-clock-init,mfp,allows-mesh-bcast crc32 62f7565f
[ 16.762697] ath10k_pci 0000:00:00.0: pdev param 0 not supported by firmware
[ 23.030622] ath10k_pci 0000:00:00.0: pdev param 0 not supported by firmware
[
Tue Feb 27 23:16:27 GMT 2024
We made it a full week with rotuer running internet chez nous and no
need for an intervention, so I am happy to call it "production". There are
still things that need fixing but they're mostly within scope for
a services refresh
I have embarked on "profiles" by creating a wap.nix
I think we could have a service module for resolvconf
It would be good to build a wap.nix example for the belkin and we
could start looking at ubifs
I've lost a chunk of notes about using events to drive desired service
state. There is probably only going to be one udev listener, so
what if we have udev as a config key thusly
udev.rules = [
{
match = {
SUBSYSTEM = "rpmsg";
ATTR.name = "DATA5_CNTL";
};
service = longrun {
name = "lte-modem";
run = "blah blah blah";
};
}
# this one would be provided by the bridge module instead of
# adding bridge member services to the default target
{
match = {
SUBSYSTEM="net";
ID_PATH="pci-0000:04:00.0";
ATTR.operstate = "up";
};
service = oneshot {
up = "ip link set dev $dev master $(output ${primary} ifname)";
down = "ip link set dev $(output ${member} ifname) nomaster";
}
}
]
This works for udev/sysfs, but we want a similar architecture(sic) for
user-generated target state so we could have services that run on e.g.
"is the ppp0 service healthy" or not. Probably there isn't a top-level
config key for each service though
services.wan = svc.ppoe.build { .... };
services.lte = watcher.build {
watching = services.wan;
match = {
# an expression matching the outputs of the service
# to be watched
health = "failing";
};
service = oneshot {
run = "start_lte_blah";
};
}
thing is, we could use this syntax also for sysfs watches, but not vice versa
... but it's not quite the same because here we're doing static matches
on contents of files, whereas the udev one is a query expression on the
sysfs database. we might need that flexibiity to implement "mount the
backup drive no matter _which_ damn sda_n_ device it appears as". I don't
know if there's the same need for service outputs - postulate the
existence of a collection of services which are all similar enough that
some other service can watch them all and do $something when one of
the changes state. Or a single service with very complicated outputs.
For example, something could watch the snmp database and update service
status depending on what it finds. Or something something mqtt...
we find that the "match" needs to be interpreted differently according
to the thing being watched. perhaps the service being watched needs to
provide a "watch me" interface somehow which accepts match criteria and
outputs a true/false. Something else then needs to
services.addmember = services.udev.watch {
match = {
SUBSYSTEM = "net";
ID_PATH = "pci-0000:04:00.0";
ATTR.operstate = "up";
};
service = oneshot {
up = "ip link set dev $dev master $(output ${primary} ifname)";
down = "ip link set dev $(output ${member} ifname) nomaster";
};
}
Sat Mar 2 15:37:29 GMT 2024
Simply put, what I think it boils down to is that we want a service
which acts as an actuator or control switch for another service,
and will start/stop that controlled service according to some
criteria.
services.addmember = svc.network.ifwatch.build {
interface = config.hardware.networkInterfaces.lan1;
# this should be part of the definition not the params
service = oneshot {
name = "member-${bridge}-${interface}";
up = "ip link set dev $dev master $(output ${primary} ifname)";
down = "ip link set dev $(output ${member} ifname) nomaster";
};
}
we could start by writing this. we need to adapt ifwait
Sun Mar 3 17:09:21 GMT 2024
this is annoyingly hard to test. the tests we'd like to write are
1) when it gets events that don't match the requirement, nothing happens
2) when it gets an event that should start the service, the
service starts
3) when stop should stop
4) when start and already started, nothing happens
5) when stop and already stopped, nothing happens
what do we do if service fails to start? s6-rc will eventually reset it
to "down", I think: do we need to take action?
Mon Mar 4 20:46:55 GMT 2024
# relevant but not correct for this model: https://www.forked.net/forums/viewtopic.php?f=13&t=3490
# power on port 5
snmpset -v 1 -c private 192.168.5.14 .1.3.6.1.4.1.318.1.1.4.4.2.1.3.5 integer 1
# power off port 5
snmpset -v 1 -c private 192.168.5.14 .1.3.6.1.4.1.318.1.1.4.4.2.1.3.5 integer 2
# toggle off/on port 5
snmpset -v 1 -c private 192.168.5.14 .1.3.6.1.4.1.318.1.1.4.4.2.1.3.5 integer 3
Wed Mar 6 18:24:29 GMT 2024
What happens when we attempt to start the service but it fails? We
assume the start was successful so we won't try and restart it again
next time we get an event that should cause it to start.
Thu Mar 7 11:48:26 GMT 2024
what next?
- fennel script needs to know where s6-rc is
- some nix syntax
- update bridge module members.nix to use the new thing
I can't find a ci derivation that uses the bridge.
Mon Mar 11 20:31:45 GMT 2024
Create a qemu config where wan and lan devices are bridged into a
single bridge
start qemu paused
Use qemu monitor commands to no-carrier the network devices
set_link virtio-net-pci.1 off
set_link virtio-net-pci.0 off
Boot the system
See if both devices are bridge members
See if reboot is possible
Use qemu monitor commands to enable the network devices
set_link virtio-net-pci.1 on
set_link virtio-net-pci.0 on
See if both devices are bridge members
disable again,check if back to starting position
Wed Mar 13 00:00:16 GMT 2024
aside: "trigger" is the least bad word I've thought of so far for
these services that stop/start other services
telent: yeah, in general 'ps afuxww' (or s6-ps -H :)) is the way to solve this, look for hung s6-rc processes and in particular their s6-svlisten1 children, where the command line will show what service is still waiting for readiness
Wed Mar 20 19:34:36 GMT 2024
Because I forgot hoe to rebuild rotuer, I tihnk it is time to improve
support for out-of-tree configurations. So I've made
modules/profiles/gateway.nix and now I can copy rotuer.nix to
telent-nixos-config.
Probably I should make nix-build work on the top-level derivation
and install liminix-rebuild as a binary?
would be good if an out-of-tree config could specify the device
it was targeting?
Fri Mar 22 20:49:54 GMT 2024
Ideally liminix-rebuild could accept a configuration file that
specifies a liminix-config file, a target hostname (maybe plus ssh
port, credentials etc) and the device name. Not going to work on that
just now but it does mean we can punt on specifying the device inside the
liminix-config which is unreasonably circular.
Maybe we'll just chuck a makefile in telent-nixos-config
Fri Mar 22 22:14:32 GMT 2024
For the service failover milestone we said
a. A configuration demonstrating a service which is restarted when it crashes
b. A failover config where service B runs iff service A is unavailable
c. A config showing different pppd behaviour when interface is flakey (retry) vs ppp password is wrong (report error, wait for resolution)
Sun Mar 24 23:41:27 GMT 2024
TODO
1) make liminix-rebuild bounce only affected services instead of
full reboot (what does it do about triggered services?)
2) sniproxy
3) see if arhcive still works. usb disk hotplug would be a good candidate for
switching to triggers
Mon Mar 25 19:35:47 GMT 2024
to make the liminix-rebuild thing restart only affected services, it needs to
know when the new service is not like the old one. By default it does not
restart a service with a changed up/down/run script unless the name has
also changed, so we need to figure out how to generate a "conversion"
file with the services that are different
pkgs/s6-rc-database/default.nix creates $out/compiled, we could add
$out/hashes to this
the other thing making this fun is that we will need to run `activate`
(which is usually done in preinit) otherwise the new configuration's
fhs directories won't exist.
so the plan woyuld be
in liminix-rebuild, when reboot was not chosen,
- run activate
- compare /run/s6-rc/compiled/hashes (old services) with
/etc/s6-rc/compiled/hashes (new services)
- whenever both files have the same column 1 and different
column 2, add that name to restart list
(need to turn restarts.fnl into a lua script)
s6-rc-update /etc/s6-rc/compiled/hashes restarts
Tue Mar 26 23:18:53 GMT 2024
activate overwrites /etc/s6-rc/compiled, which is a problem because
s6-rc-update expects to find the old compiled database here so that
it can know what to update
Maybe config.filesystem should specify /etc/s6-rc/compiled.new
and something in early boot could symlink /etc/s6-rc/compiled to it
Sat Mar 30 18:41:14 GMT 2024
soft restart doesn't restart services that are invoked by trigger,
because it has to do -p -u default so that it prunes services that
were in the old config but not the new one. Ideally we need somehow
to notify the trigger that it should respawn its service. Maybe
we could add triggers to the force restart list, if there's a way
to detect which they are? don't want to do it by adding files in
the service state directory if there may be oneshot triggers. Can
there be oneshot triggers?
The hashes file is built when we build the service database, so we
could easily(?) add something in there to mark services that
need poking whenever there's a restart. It's not perfect because the
triggered services will be bounced unnecessarily, but remember that
the alternative is a reboot ...
Mon Apr 1 00:18:50 BST 2024
i) I don't know if digressing into remote log shipping is a tangent or
an important part of making services work well.
ii) Should there be a single "machine state" value for all of the
trigger services to reference, or is it better that each trigger
service has its own private state, or (third option) one state
per "state source"? We previously handwaved that a state source
is a service
services.addmember = services.udev.watch {
match = {
SUBSYSTEM = "net";
ID_PATH = "pci-0000:04:00.0";
ATTR.operstate = "up";
};
service = oneshot {
up = "ip link set dev $dev master $(output ${primary} ifname)";
down = "ip link set dev $(output ${member} ifname) nomaster";
};
}
Tue Apr 2 19:55:25 BST 2024
We could do a test script for udev usb disk mounting, which uses the
qemu monitor to add/remove a disk.
./result/run.sh --flag -device --flag usb-ehci,id=xhci --flag -drive --flag if=none,id=usbstick,format=raw,file=./stick.img
(qemu) device_add usb-storage,bus=xhci.0,drive=usbstick
Fri Apr 5 17:11:46 BST 2024
1) write a fennel thing that reads from the udev rebroadcast socket
2) and can check sysfs for state
3) set up mdevd in liminix
Sat Apr 6 13:23:02 BST 2024
I wonder if we could replace preinit with an execline script? One for
the TODO stack
Sun Apr 7 14:03:29 BST 2024
1) we want to know what messages are sent from mdevd under various circumstances
- actually, right now the only relevant circumstances are updown and inout
2) we might get a wider variety of messages from real hardware?
3) if we log the raw messages, pref. with timestamps, then we can
write tests for the parsing
therefore: write a program that opens the netlink socket and logs
all data received
----
what's the minimum we need here? we need the inout test to open a
uevent socket and use uevents to update some state that says whether the
backup drive is plugged in
rather awkwardly, uevents don't have filesystem labels. so we also need
to run blkid to find the label of each partition, and ideally we do this
while the partition is present, not each time we get an event for it.
We have DEVNAME, DEVTYPE, SUBSYSTEM to indicate that a filesystem of interest
may be present, we should use that as a trigger to scan any known
add@/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/block/sda/sda1
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/block/sda/sda1
SUBSYSTEM=block
MAJOR=8
MINOR=1
DEVNAME=sda1
DEVTYPE=partition
DISKSEQ=2
PARTN=1
SEQNUM=1528
Some disks on loaclhost and noetbook have PARTNAME field - I assume
this is because they're GPT disks. Would it actually be better to use this
field than grovelling for filesystem label?
Tue Apr 9 21:07:50 BST 2024
Having waited for the appropriately labelled disk to appear, we then
also have to communicate its path to the service that mounts it
- create a symlink
- or use an instanced service
Creating a symlink will be fine if we can pass the symlink name as
a param to fswait
Wed Apr 10 20:53:48 BST 2024
We think that fswait will evolve into a more general
waiting-for-uevents tool. Maybe we could provide the matchers on the
command line:
waituevent -l /dev/volumes/backup-disk -s mount-srv devtype=partition partname=backup-disk
Thu Apr 11 23:09:43 BST 2024
lcommit d3a2e3a4cb80b631df2ab79d463c2c4d1adef37b
commit 4a58cf9335116ce673fcf08f70f3bca921a4c9ad
commit afca6d4b63dd39062f02827b3c29e16904770216
Sun Apr 14 19:50:27 BST 2024
how to get this on to main:
- make uevent-watcher package (it's fswait renamed)
- make mount service use it
- module for mdevd
- add nellie (generalise for other netlink uses w/params pid/family/groups)
Mon Apr 15 19:59:43 BST 2024
plan:
introduce uevent-watcher command, update test to use it
make mount service use it
Tue Apr 16 18:59:25 BST 2024
Another idea for maybe-not-now: tftp local/peer addresses could be
provided as top-level params (e.g. to nix-build).
Wed Apr 17 18:57:49 BST 2024
I hatched a plan (and forgot to save this file) to build a service
that subscribes to uevents and retains state so that other services
can know about things that happened before they started. I'm wondering
if it's really needed though, because there could be one process to
read the socket and start/stop *all* the udev triggered services. Not
sure how we'd describe this in nix though: how do all the other
services
How we would do a uevent database service (sysfsq):
for each event e from socket
if e.action in (add, change)
path[e.path] = e.attribues
if e.action == 'remove'
path.remove e.path
(update-indices e)
(fn update-indices [event]
for each k in (keys event)
index.k.v += e)
we also want to not maintain indexes when there are so many values in
the index entry to make searching it worthless.
to retrieve, look at each criterion that has an index and choose the
index with fewest elements in the value. scan that index for the other
criteria
there are 813 uevent files in sysfs on arhcive, is this all overkill?
maybe we could simplify using a hardcoded stopword list - e.g. don't
have indices for MAJOR, MINOR
what are we going to use for querying? can't be netlink because that's
a shared medium (broadcast/multicast). unix dgram socket? alternative
would be to somehow use the filesystem as a database
Wed Apr 17 22:00:29 BST 2024
tests. assuming the sysfs setup from all-events.txt, we can write tests lik
- there is a path for $foo
- the attributes are x, y, z
- when I add a device with $attributes, I can recall it
- by path
- by attribute value
- when I remove it again, I cannot access it by path or attributes
- when I add a device with $attributes major minor foo bar baz
it is added to indices for foo bar baz but not major minor
- when I remove it, it can no longer be found by looking in any index
- when I query with multiple attributes, the search is performed
using the most specific attribute (= the attribute whose
value at this key has fewest elements)
I am still looking for ways to avoid doing this, but it is potentially
the first of several "database" services that triggers could want to
use so maybe it's an emerging pattern.
https://github.com/philanc/minisock useful? we could almost replace
nellie with it only not quite (it hardcodes 0 as the "protocol" param
to socket())
Fri Apr 19 20:55:22 BST 2024
We could have a service that's present only when a devdb entry is
present. For example mount_disk only runs when partlabel=foo
Or we could have a service that continues to run as the $somedatabase
service state changes and does different things depending on the
nature of those changes. For example, [I can't think of an example
now, but it was definitely an issue the other day, maybe I dreamt it]
I don't think this will be such an issue for devdb becuase there isn't
much in it that has continuously varying values. Maybe battery health
is the exception there
The step ahead we're thinking here is: how do clients do a request? A
single one-of request for state is fine but chances are that a client
will do that to get initial state and then need to open a netlink
socket to get updates: well, if we can feed them the initial state
filtered for their needs why can't we send them the relevant updates
as well? This makes the database server design a bit more complicated
as it needs to remember each client and their subscriptions, and then
send only relevant updates to each subscribed client
* should a client be allowed multiple subscriptions on the same
connection?
* do we guarantee that every message sent is matching the subscription
or can we send other stuff as well if it makes implementation easier?
it might defeat the purpose a bit because it means the client also
needs to filter, but the client will anyway have to do some message
parsing so they can distinguish add from remove
* where do we start?
Sun Apr 21 13:31:48 BST 2024
We have the mechanics of it working (albeit implemented in the
simplest possible terms), we need to glue it to some I/O
1) open a netlink socket and read the events from it
2) "create a PF_UNIX socket of type SOCK_STREAM, and accept connections on it, then each time you accept a connection, you get a new fd"
- accept connection
- read terms from it
- register callback that writes event to connected socket
minisock has no support for "test if fd is ready" or "wait for [fds]
to become ready", either we need poll() or we could add a call for "is
this fd ready to read" and use coroutines. Fork minisock or add as
another library?
[ if we fork minisock we could expose the protocol param to Lua
so we could use it for netlink ]
Tue Apr 23 19:13:45 BST 2024
we could convert from minisock to lualinux. if we can also use that to
get rid of nellie and/or lfs, the size tradeoff is minimal
---
Is there some way we could test the devout event loop?
I can register a fd with a callback
when the fd is ready, my callback is called
when the callback return true it remains registered
when the callback return true it is unregistered and the fd is closed
loop.register
loop.registered?
loop.feed
Tue Apr 23 20:34:03 BST 2024
I think we could make the event loop abstraction leak less?
It's not actually a _loop_, all the actual GOTO 10 happens
outside of it
[X] 1) see if we can do netlink in lualinux
[X] 2) if so, convert it to lualinux
[X] 3) add netlink socket to event loop
[X] 4) make it send messages to subscribers
[X] 5) package it
[X] 6) make uevent-watcher use it instead of netlink directly
[X] 7) write an inout test variant that has the stick inserted
at boot time already
I'm also thinking we could wrap the raw fds from lualinux into small
objects with read and close methods? It would make testing easier if
nothing else - also use of with-open. Maybe do that in anoia.
when a subscriber connects we need to send them their matching current
state before subscribing them [ needs a test ]
figure out what event format the subscribers want? lua-ish or send the
same messages as udev would? If we're going to send the originals,
should we store them alongside the parsed, or reconstruct from parsed?
Sat Apr 27 21:52:11 BST 2024
We have a passing inout test. Next thing to do is try it on
the actual arhcive hardware
Next big thing is some kind of failovery service. Almost-obvious
candidate is LTE failover with aaisp l2tp tunnel
Tue Apr 30 23:27:30 BST 2024
I want to connect my new ip camera to arthur without letting it reach the
internet, or the internet reach it.
we could plug it into a gl.inet box running dhcp server on lan
and client on wan, then use NAT to expose the camera's http and rtsp
ports on whatever address it has on the wan interface
Tue May 7 22:23:49 BST 2024
If we want to build a config with an l2tp upstream, it needs an
underlying dhcp interface not pppoe as we can't use the bordervm l2tp
account simultaneously. Having bordervm do dhcp might be quite useful
anyway for other applications, although it will have to double-nat to
the internet. We could give it an aaisp /64 and have routable ipv6 but
maybe that's a level of faff too high.
Given that we can build xl2tpd and a service for it.
're using the same l2tp account for thingy that we use to simulate ppp,
we need an upstream which is not ppp
We need a less shit coldplug that copes with filenames containing spaces (!)
Fri May 10 00:33:14 BST 2024
Getting xl2tp hackily running turned out to be not a lot of work. However,
we need to figure out routing
- we need a route on lan device to the dns to lookup l2tp.aaisp.net.uk
- we need a route on lan device to l2tp.aaisp.net.uk
also it doesn't die when the tunnel closes, which is a bit shit
maybe this is where we lean into health check services
a health check service is just a service that watches another service
and kills it if it's not healthy.
for xl2tpd, "not healthy" is "there is no ppp process" or "there is no
tunnel" or "the tunnel has no sessions". I don't know how we
(robustly) test for no ppp process associated with the l2tp peer
when ppp quits, does the tunnel come down?
in xl2tld.c child_handler we respond to sigchld by closing c->fd
and setting it to -1
Sat May 11 17:55:04 BST 2024
A better way to monitor the connection health would be to ping a
computer on the internet (preferably one that doesn't mind being
pinged). If we combine autodial with "is $isp still there" then we
should have something fairly robust.
xl2tpd spawns pppd, we should equip it with config that writes the
ppp outputs (ip address etc) to the xl2tp service directory so
that it can be used like a regular ppp. This will also make
it possible to have the health check work by pinging the peer address
Sun May 12 22:33:09 BST 2024
sleep until the interface is probably up
failure counter = 0
loop indefinitely
get outputs/peer-address of watched ppp service
ping it
if ok
reset failure counter
else
increment failure counter
fi
if failure counter > threshold
bounce the ppp service
exit, if previous action didn't do that already
end
sleep(check interval)
end loop
# ps ax | grep l2tp
72 root 1316 S s6-supervise l2tp.aaisp.net.uk.l2tp
73 root 1316 S s6-supervise l2tp.aaisp.net.uk.l2tp-log
122 root 1428 S {run.user} /bin/sh ./run.user l2tp.aaisp.net.uk.l2tp
1099 root 1428 S {run.user} /bin/sh ./run.user l2tp.aaisp.net.uk.l2tp
1102 root 1104 S {xl2tpd} /nix/store/i1bbqh7vybam03l6jzf4sm4np3k4ack5
1115 root 1420 S grep l2tp
# s6-rc -d change l2tp.aaisp.net.uk.l2tp
# ps ax | grep l2tp
72 root 1316 S s6-supervise l2tp.aaisp.net.uk.l2tp
73 root 1316 S s6-supervise l2tp.aaisp.net.uk.l2tp-log
122 root 1428 S {run.user} /bin/sh ./run.user l2tp.aaisp.net.uk.l2tp
1102 root 1104 S {xl2tpd} /nix/store/i1bbqh7vybam03l6jzf4sm4np3k4ack5
1122 root 1420 S grep l2tp
we are the LAC, aaisp are the LNS
+22
View File
@@ -4,6 +4,10 @@ let
inherit (lib) mkOption mkEnableOption mdDoc types optional optionals;
in {
options.bordervm = {
keys = mkOption {
type = types.listOf types.str;
default = [];
};
l2tp = {
host = mkOption {
description = mdDoc ''
@@ -76,8 +80,19 @@ in {
};
};
services.openssh.enable = true;
services.dnsmasq = {
enable = true;
resolveLocalQueries = false;
settings = {
# domain-needed = true;
dhcp-range = [ "10.0.0.10,10.0.0.240" ];
interface = "eth1";
};
};
systemd.services.sshd.wantedBy = pkgs.lib.mkForce [ "multi-user.target" ];
virtualisation = {
qemu = {
networkingOptions = [];
@@ -108,6 +123,7 @@ in {
tufted
iptables
usbutils
busybox
];
security.sudo.wheelNeedsPassword = false;
networking = {
@@ -117,11 +133,17 @@ in {
useDHCP = false;
ipv4.addresses = [ { address = "10.0.0.1"; prefixLength = 24;}];
};
nat = {
enable = true;
internalInterfaces = [ "eth1" ];
externalInterface ="eth0";
};
};
users.users.liminix = {
isNormalUser = true;
uid = 1000;
extraGroups = [ "wheel"];
openssh.authorizedKeys.keys = cfg.keys;
};
services.getty.autologinUser = "liminix";
};
+6 -2
View File
@@ -9,8 +9,12 @@ let
borderVmConf = ./bordervm.conf-example.nix;
inherit (pkgs.lib.attrsets) genAttrs;
devices = [
"gl-ar750" "gl-mt300n-v2" "gl-mt300a"
"qemu" "qemu-aarch64" "qemu-armv7l"
"gl-ar750"
"gl-mt300a"
"gl-mt300n-v2"
"qemu"
"qemu-aarch64"
"qemu-armv7l"
"tp-archer-ax23"
"zyxel-nwa50ax"
];
+7 -1
View File
@@ -1,5 +1,6 @@
{
device
deviceName ? null
, device ? (import ./devices/${deviceName} )
, liminix-config ? <liminix-config>
, nixpkgs ? <nixpkgs>
, borderVmConf ? ./bordervm.conf.nix
@@ -20,12 +21,16 @@ let
});
eval = pkgs.lib.evalModules {
specialArgs = {
modulesPath = builtins.toString ./modules;
};
modules = [
{ _module.args = { inherit pkgs; inherit (pkgs) lim; }; }
./modules/hardware.nix
./modules/base.nix
./modules/busybox.nix
./modules/hostname.nix
./modules/kernel
device.module
liminix-config
./modules/s6
@@ -72,6 +77,7 @@ in {
min-copy-closure
fennelrepl
lzma
lua
];
};
}
+9 -2
View File
@@ -125,8 +125,14 @@
networkInterfaces =
let inherit (config.system.service.network) link;
in {
lan = link.build { ifname = "eth0"; };
wan = link.build { ifname = "eth1"; };
lan = link.build {
ifname = "lan";
devpath = "/devices/platform/ahb/19000000.eth";
};
wan = link.build {
ifname = "wan";
devpath = "/devices/platform/ahb/1a000000.eth";
};
wlan = link.build {
ifname = "wlan0";
dependencies = [ mac80211 ];
@@ -149,6 +155,7 @@
};
boot.tftp = {
loadAddress = lim.parseInt "0x00A00000";
appendDTB = true;
};
kernel = {
src = pkgs.pkgsBuildBuild.fetchurl {
+3 -3
View File
@@ -110,13 +110,11 @@
ifname = "eth0.1";
primary = eth;
vid = "1";
dependencies = [swconfig eth];
};
wan = vlan.build {
ifname = "eth0.2";
primary = eth;
vid = "2";
dependencies = [swconfig eth];
};
wlan = link.build {
ifname = "wlan0";
@@ -126,7 +124,8 @@
};
boot.tftp = {
loadAddress = lim.parseInt "0x00A00000";
};
appendDTB = true;
};
kernel = {
src = pkgs.fetchurl {
@@ -136,6 +135,7 @@
};
extraPatchPhase = ''
${openwrt.applyPatches.ramips}
${openwrt.applyPatches.rt2x00}
'';
config = {
+2 -1
View File
@@ -97,7 +97,7 @@
swconfig dev switch0 vlan 2 set ports '0 6t'
swconfig dev switch0 set apply
'';
down = "swconfig dev switch0 set reset";
down = "${pkgs.swconfig}/bin/swconfig dev switch0 set reset";
};
in rec {
eth = link.build { ifname = "eth0"; dependencies = [swconfig]; };
@@ -122,6 +122,7 @@
# 20MB seems to give enough room to uncompress the kernel
# without anything getting trodden on. 10MB was too small
loadAddress = lim.parseInt "0x1400000";
appendDTB = true;
};
kernel = {
+18 -30
View File
@@ -18,8 +18,8 @@
in rec {
boot = {
tftp = {
serverip = "192.168.8.148";
ipaddr = "192.168.8.251";
serverip = "10.0.0.1";
ipaddr = "10.0.0.8";
};
};
@@ -28,34 +28,12 @@ in rec {
../modules/network
../modules/vlan
../modules/ssh
../modules/usb.nix
../modules/watchdog
../modules/mount
];
hostname = "arhcive";
kernel = {
config = {
USB = "y";
USB_EHCI_HCD = "y";
USB_EHCI_HCD_PLATFORM = "y";
USB_OHCI_HCD = "y";
USB_OHCI_HCD_PLATFORM = "y";
USB_SUPPORT = "y";
USB_COMMON = "y";
USB_STORAGE = "y";
USB_STORAGE_DEBUG = "n";
USB_UAS = "y";
USB_ANNOUNCE_NEW_DEVICES = "y";
SCSI = "y";
BLK_DEV_SD = "y";
USB_PRINTER = "y";
MSDOS_PARTITION = "y";
EFI_PARTITION = "y";
EXT4_FS = "y";
EXT4_USE_FOR_EXT2 = "y";
FS_ENCRYPTION = "y";
};
};
services.dhcpc =
let iface = config.hardware.networkInterfaces.lan;
@@ -105,7 +83,7 @@ in rec {
};
services.mount_external_disk = svc.mount.build {
device = "LABEL=backup-disk";
partlabel = "backup-disk";
mountpoint = "/srv";
fstype = "ext4";
};
@@ -146,9 +124,7 @@ in rec {
users.root = {
passwd = lib.mkForce secrets.root.passwd;
# openssh.authorizedKeys.keys = [
# (builtins.readFile "/home/dan/.ssh/id_rsa.pub")
# ];
openssh.authorizedKeys.keys = secrets.root.keys;
};
users.backup = {
@@ -159,5 +135,17 @@ in rec {
gid=500; usernames = ["backup"];
};
defaultProfile.packages = with pkgs; [e2fsprogs strace tcpdump ];
defaultProfile.packages = with pkgs; [
e2fsprogs
mtdutils
(levitate.override {
config = {
services = {
inherit (config.services) dhcpc sshd watchdog;
};
defaultProfile.packages = [ mtdutils ];
users.root.openssh.authorizedKeys.keys = secrets.root.keys;
};
})
];
}
-1
View File
@@ -158,7 +158,6 @@ in rec {
};
services.firewall = svc.firewall.build {
ruleset = import ./demo-firewall.nix;
};
services.packet_forwarding = svc.network.forward.build { };
+16 -99
View File
@@ -8,12 +8,10 @@
config,
pkgs,
lib,
modulesPath,
...
}: let
secrets = import ./extneder-secrets.nix;
inherit (pkgs.liminix.services) oneshot longrun bundle target;
inherit (pkgs.pseudofile) dir symlink;
inherit (pkgs) dropbear ifwait serviceFns;
svc = config.system.service;
in rec {
boot = {
@@ -24,113 +22,32 @@ in rec {
};
imports = [
../modules/wlan.nix
../modules/vlan
../modules/network
../modules/hostapd
../modules/bridge
../modules/ssh
"${modulesPath}/profiles/wap.nix"
"${modulesPath}/vlan"
"${modulesPath}/ssh"
];
hostname = "extneder";
kernel = {
config = {
NETFILTER_XT_MATCH_CONNTRACK = "y";
IP6_NF_IPTABLES = "y"; # do we still need these
IP_NF_IPTABLES = "y"; # if using nftables directly
# these are copied from rotuer and need review.
# we're not running a firewall, so why do we need
# nftables config?
IP_NF_NAT = "y";
IP_NF_TARGET_MASQUERADE = "y";
NETFILTER = "y";
NETFILTER_ADVANCED = "y";
NETFILTER_XTABLES = "y";
NFT_COMPAT = "y";
NFT_CT = "y";
NFT_LOG = "y";
NFT_MASQ = "y";
NFT_NAT = "y";
NFT_REJECT = "y";
NFT_REJECT_INET = "y";
NF_CONNTRACK = "y";
NF_NAT = "y";
NF_NAT_MASQUERADE = "y";
NF_TABLES = "y";
NF_TABLES_INET = "y";
NF_TABLES_IPV4 = "y";
NF_TABLES_IPV6 = "y";
};
};
services.hostap = svc.hostapd.build {
interface = config.hardware.networkInterfaces.wlan;
params = {
country_code = "GB";
hw_mode = "g";
wmm_enabled = 1;
ieee80211n = 1;
inherit (secrets) ssid channel wpa_passphrase;
auth_algs = 1; # 1=wpa2, 2=wep, 3=both
wpa = 2; # 1=wpa, 2=wpa2, 3=both
wpa_key_mgmt = "WPA-PSK";
wpa_pairwise = "TKIP CCMP"; # auth for wpa (may not need this?)
rsn_pairwise = "CCMP"; # auth for wpa2
};
};
services.int = svc.bridge.primary.build {
ifname = "int";
};
services.dhcpc = svc.network.dhcp.client.build {
interface = services.int;
dependencies = [ config.services.hostname ];
};
services.bridge = svc.bridge.members.build {
primary = services.int;
members = with config.hardware.networkInterfaces; [
profile.wap = {
interfaces = with config.hardware.networkInterfaces; [
lan
wlan
];
};
services.sshd = svc.ssh.build {};
services.resolvconf = oneshot rec {
dependencies = [ services.dhcpc ];
name = "resolvconf";
# CHECK: https://udhcp.busybox.net/README.udhcpc says
# 'A list of DNS server' but doesn't say what separates the
# list members. Assuming it's a space or other IFS character
up = ''
. ${serviceFns}
( in_outputs ${name}
for i in $(output ${services.dhcpc} dns); do
echo "nameserver $i" > resolv.conf
done
)
'';
};
filesystem = dir {
etc = dir {
"resolv.conf" = symlink "${services.resolvconf}/.outputs/resolv.conf";
wireless = {
networks.${secrets.ssid} = {
interface = config.hardware.networkInterfaces.wlan;
inherit (secrets) channel wpa_passphrase;
country_code = "GB";
hw_mode = "g";
wmm_enabled = 1;
ieee80211n = 1;
};
};
};
services.defaultroute4 = svc.network.route.build {
via = "$(output ${services.dhcpc} router)";
target = "default";
dependencies = [services.dhcpc];
};
services.sshd = svc.ssh.build {};
users.root.passwd = lib.mkForce secrets.root.passwd;
defaultProfile.packages = with pkgs; [nftables strace tcpdump swconfig];
}
+86
View File
@@ -0,0 +1,86 @@
{
config,
pkgs,
lib,
...
}: let
secrets = import ./extneder-secrets.nix;
rsecrets = import ./rotuer-secrets.nix;
lns = "l2tp.aaisp.net.uk";
inherit (pkgs.liminix.services) oneshot longrun bundle target;
inherit (pkgs.pseudofile) dir symlink;
inherit (pkgs) writeText dropbear ifwait serviceFns;
svc = config.system.service;
in rec {
boot = {
tftp = {
serverip = "10.0.0.1";
ipaddr = "10.0.0.8";
};
};
imports = [
../modules/cdc-ncm
../modules/network
../modules/vlan
../modules/ssh
../modules/usb.nix
../modules/watchdog
../modules/mount
../modules/ppp
];
hostname = "thing";
services.dhcpc = svc.network.dhcp.client.build {
interface = config.services.wwan;
dependencies = [ config.services.hostname ];
};
services.sshd = svc.ssh.build { };
services.resolvconf = oneshot rec {
dependencies = [ services.dhcpc ];
name = "resolvconf";
up = ''
. ${serviceFns}
( in_outputs ${name}
for i in $(output ${services.dhcpc} dns); do
echo "nameserver $i" > resolv.conf
done
)
'';
};
filesystem = dir {
etc = dir {
"resolv.conf" = symlink "${services.resolvconf}/.outputs/resolv.conf";
};
srv = dir {};
};
services.lnsroute = svc.network.route.build {
via = "$(output ${services.dhcpc} router)";
target = lns;
dependencies = [services.dhcpc];
};
services.l2tp = svc.l2tp.build {
inherit lns;
ppp-options = [
"debug" "+ipv6" "noauth"
"name" rsecrets.l2tp.name
"password" rsecrets.l2tp.password
];
dependencies = [ services.lnsroute ];
};
services.defaultroute4 = svc.network.route.build {
via = "$(output ${services.l2tp} router)";
target = "default";
dependencies = [services.l2tp];
};
users.root = {
passwd = lib.mkForce secrets.root.passwd;
openssh.authorizedKeys.keys = secrets.root.keys;
};
}
+52 -149
View File
@@ -6,23 +6,18 @@
# problems.
{ config, pkgs, lib, ... } :
{ config, pkgs, lib, modulesPath, ... } :
let
secrets = {
domainName = "fake.liminix.org";
firewallRules = {};
} // (import ./rotuer-secrets.nix);
inherit (pkgs.liminix.services) oneshot longrun bundle;
inherit (pkgs.liminix.services) oneshot bundle;
inherit (pkgs) serviceFns;
svc = config.system.service;
wirelessConfig = {
country_code = "GB";
inherit (secrets) wpa_passphrase;
auth_algs = 1; # 1=wpa2, 2=wep, 3=both
wpa = 2; # 1=wpa, 2=wpa2, 3=both
wpa_key_mgmt = "WPA-PSK";
wpa_pairwise = "TKIP CCMP"; # auth for wpa (may not need this?)
rsn_pairwise = "CCMP"; # auth for wpa2
wmm_enabled = 1;
};
@@ -36,65 +31,62 @@ in rec {
};
imports = [
../modules/wlan.nix
../modules/network
../modules/ppp
../modules/dnsmasq
../modules/dhcp6c
../modules/firewall
../modules/hostapd
../modules/bridge
../modules/ntp
../modules/schnapps
../modules/ssh
../modules/outputs/btrfs.nix
../modules/outputs/extlinux.nix
"${modulesPath}/profiles/gateway.nix"
"${modulesPath}/schnapps"
"${modulesPath}/outputs/btrfs.nix"
"${modulesPath}/outputs/extlinux.nix"
];
hostname = "rotuer";
rootfsType = "btrfs";
rootOptions = "subvol=@";
boot.loader.extlinux.enable = true;
services.hostap = svc.hostapd.build {
interface = config.hardware.networkInterfaces.wlan;
params = {
ssid = secrets.ssid;
hw_mode="g";
channel = "2";
ieee80211n = 1;
} // wirelessConfig;
};
services.hostap5 = svc.hostapd.build {
interface = config.hardware.networkInterfaces.wlan5;
params = rec {
ssid = "${secrets.ssid}5";
hw_mode="a";
channel = 36;
ht_capab = "[HT40+]";
vht_oper_chwidth = 1;
vht_oper_centr_freq_seg0_idx = channel + 6;
ieee80211n = 1;
ieee80211ac = 1;
} // wirelessConfig;
};
services.int = svc.network.address.build {
interface = svc.bridge.primary.build { ifname = "int"; };
family = "inet"; address ="${secrets.lan.prefix}.1"; prefixLength = 24;
};
services.bridge = svc.bridge.members.build {
primary = services.int;
members = with config.hardware.networkInterfaces;
[ wlan
wlan5
lan0
lan1
lan2
lan3
lan4
];
profile.gateway = {
lan = {
interfaces = with config.hardware.networkInterfaces;
[
wlan wlan5
lan0 lan1 lan2 lan3 lan4
];
inherit (secrets.lan) prefix;
address = {
family = "inet"; address ="${secrets.lan.prefix}.1"; prefixLength = 24;
};
dhcp = {
start = 10;
end = 240;
hosts = { } // lib.optionalAttrs (builtins.pathExists ./static-leases.nix) (import ./static-leases.nix);
localDomain = "lan";
};
};
wan = {
interface = config.hardware.networkInterfaces.wan;
username = secrets.l2tp.name;
password = secrets.l2tp.password;
dhcp6.enable = true;
};
firewall = {
enable = true;
rules = secrets.firewallRules;
};
wireless.networks = {
"${secrets.ssid}" = {
interface = config.hardware.networkInterfaces.wlan;
hw_mode="g";
channel = "2";
ieee80211n = 1;
} // wirelessConfig;
"${secrets.ssid}5" = rec {
interface = config.hardware.networkInterfaces.wlan5;
hw_mode="a";
channel = 36;
ht_capab = "[HT40+]";
vht_oper_chwidth = 1;
vht_oper_centr_freq_seg0_idx = channel + 6;
ieee80211n = 1;
ieee80211ac = 1;
} // wirelessConfig;
};
};
services.ntp = svc.ntp.build {
@@ -106,95 +98,6 @@ in rec {
users.root = secrets.root;
services.dns =
let interface = services.int;
in svc.dnsmasq.build {
resolvconf = services.resolvconf;
inherit interface;
ranges = [
"${secrets.lan.prefix}.10,${secrets.lan.prefix}.240"
# ra-stateless: sends router advertisements with the O and A
# bits set, and provides a stateless DHCP service. The client
# will use a SLAAC address, and use DHCP for other
# configuration information.
"::,constructor:$(output ${interface} ifname),ra-stateless"
];
# You can add static addresses for the DHCP server here. I'm
# not putting my actual MAC addresses in a public git repo ...
hosts = { } // lib.optionalAttrs (builtins.pathExists ./static-leases.nix) (import ./static-leases.nix);
upstreams = [ "/${secrets.domainName}/" ];
domain = secrets.domainName;
};
services.wan = svc.pppoe.build {
interface = config.hardware.networkInterfaces.wan;
ppp-options = [
"debug" "+ipv6" "noauth"
"name" secrets.l2tp.name
"password" secrets.l2tp.password
];
};
services.resolvconf = oneshot rec {
dependencies = [ services.wan ];
name = "resolvconf";
up = ''
. ${serviceFns}
( in_outputs ${name}
echo "nameserver $(output ${services.wan} ns1)" > resolv.conf
echo "nameserver $(output ${services.wan} ns2)" >> resolv.conf
chmod 0444 resolv.conf
)
'';
};
filesystem =
let inherit (pkgs.pseudofile) dir symlink;
in dir {
etc = dir {
"resolv.conf" = symlink "${services.resolvconf}/.outputs/resolv.conf";
};
};
services.defaultroute4 = svc.network.route.build {
via = "$(output ${services.wan} address)";
target = "default";
dependencies = [ services.wan ];
};
services.defaultroute6 = svc.network.route.build {
via = "$(output ${services.wan} ipv6-peer-address)";
target = "default";
interface = services.wan;
};
services.firewall = svc.firewall.build {
ruleset =
let defaults = import ./demo-firewall.nix;
in lib.recursiveUpdate defaults secrets.firewallRules;
};
services.packet_forwarding = svc.network.forward.build { };
services.dhcp6c =
let client = svc.dhcp6c.client.build {
interface = services.wan;
};
in bundle {
name = "dhcp6c";
contents = [
(svc.dhcp6c.prefix.build {
inherit client;
interface = services.int;
})
(svc.dhcp6c.address.build {
inherit client;
interface = services.wan;
})
];
};
defaultProfile.packages = with pkgs; [
min-collect-garbage
nftables
+9 -10
View File
@@ -9,30 +9,29 @@
./busybox.nix
./dhcp6c
./dnsmasq
./outputs/ext4fs.nix
./firewall
./hardware.nix
./hostapd
./hostname.nix
./outputs/initramfs.nix
./outputs/jffs2.nix
./kernel
./outputs/kexecboot.nix
./mdevd.nix
./mount
./network
./ntp
./outputs.nix
./outputs/vmroot.nix
./outputs/ubimage.nix
./outputs/ext4fs.nix
./outputs/initramfs.nix
./outputs/jffs2.nix
./outputs/kexecboot.nix
./outputs/mtdimage.nix
./outputs/tftpboot.nix
./outputs/ubifs.nix
./outputs/ubimage.nix
./outputs/vmroot.nix
./ppp
./ramdisk.nix
./squashfs.nix
./ssh
./outputs/tftpboot.nix
./outputs/ubifs.nix
./ubifs.nix
./ubinize.nix
./users.nix
./vlan
./watchdog
+29 -3
View File
@@ -12,9 +12,6 @@ let
type_service = pkgs.liminix.lib.types.service;
in {
imports = [
./kernel # kernel is a separate module for doc purposes
];
options = {
defaultProfile = {
packages = mkOption {
@@ -29,6 +26,10 @@ in {
services = mkOption {
type = types.attrsOf type_service;
};
system.callService = mkOption {
type = types.functionTo (types.functionTo types.anything);
};
filesystem = mkOption {
type = types.anything;
description = ''
@@ -111,6 +112,31 @@ in {
"fw_devlink=off"
] ++ lib.optional (config.rootOptions != null) "rootflags=${config.rootOptions}";
system.callService = path : parameters :
let
typeChecked = caller: type: value:
let
inherit (lib) types mergeDefinitions;
defs = [{ file = caller; inherit value; }];
type' = types.submodule { options = type; };
in (mergeDefinitions [] type' defs).mergedValue;
cp = lib.callPackageWith(pkgs // { svc = config.system.service; });
pkg = cp path {};
checkTypes = t : p : typeChecked (builtins.toString path) t p;
in {
inherit parameters;
build = { dependencies ? [], ... } @ args :
let
s = pkg (checkTypes parameters
(builtins.removeAttrs args ["dependencies"]));
in s.overrideAttrs (o: {
dependencies = (builtins.map (d: d.name) dependencies) ++ o.dependencies;
buildInputs = dependencies ++ o.buildInputs;
});
};
users.root = {
uid = 0; gid= 0; gecos = "Root of all evaluation";
dir = "/home/root/";
+4 -2
View File
@@ -14,6 +14,8 @@ let
inherit (pkgs) liminix;
in
{
imports = [ ../ifwait ];
options = {
system.service.bridge = {
primary = mkOption { type = liminix.lib.types.serviceDefn; };
@@ -27,7 +29,7 @@ in
description = "bridge interface name to create";
};
};
members = liminix.callService ./members.nix {
members = config.system.callService ./members.nix {
primary = mkOption {
type = liminix.lib.types.interface;
description = "primary bridge interface";
@@ -47,5 +49,5 @@ in
# a better way to test for the existence of vlan config:
# maybe the module should set an `enabled` attribute?
BRIDGE_VLAN_FILTERING = "y";
};
};
}
+14 -7
View File
@@ -2,6 +2,7 @@
liminix
, ifwait
, lib
, svc
}:
{ members, primary } :
@@ -10,14 +11,20 @@ let
inherit (liminix.services) bundle oneshot;
inherit (lib) mkOption types;
addif = member :
oneshot {
name = "${primary.name}.member.${member.name}";
up = ''
dev=$(output ${member} ifname)
${ifwait}/bin/ifwait $dev running && ip link set dev $dev master $(output ${primary} ifname)
'';
down = "ip link set dev $(output ${member} ifname) nomaster";
# how do we get sight of services from here? maybe we need to
# implement ifwait as a regualr derivation instead of a
# servicedefinition
svc.ifwait.build {
state = "running";
interface = member;
dependencies = [ primary member ];
service = oneshot {
name = "${primary.name}.member.${member.name}";
up = ''
ip link set dev $(output ${member} ifname) master $(output ${primary} ifname)
'';
down = "ip link set dev $(output ${member} ifname) nomaster";
};
};
in bundle {
name = "${primary.name}.members";
+56
View File
@@ -0,0 +1,56 @@
{ config, pkgs, lib, ... }:
let
inherit (pkgs.liminix.services) oneshot;
svc = config.system.service;
in {
config = {
kernel.config = {
USB_NET_HUAWEI_CDC_NCM = "y";
USB_USBNET = "y";
USB_SERIAL = "y";
USB_SERIAL_OPTION = "y";
};
# https://www.0xf8.org/2017/01/flashing-a-huawei-e3372h-4g-lte-stick-from-hilink-to-stick-mode/
services.wwan = let
chat = lib.escapeShellArgs [
# Your usb modem thing might present as a tty that you run PPP
# over, or as a network device ("ndis" or "ncm"). The latter
# kind is to be preferred, at least in principle, because it's
# faster. This initialization sequence works for the Huawei
# E3372, and took much swearing: the error messages are *awful*
"" "AT"
"OK" "ATZ"
# create PDP context
"OK" "AT+CGDCONT=1,\"IP\",\"data.uk\""
# activate PDP context
"OK" "AT+CGACT=1,1"
# setup username and password per requirements of sim provider.
# (caret is special to chat, so needs escaping in AT commands)
"OK" "AT\\^AUTHDATA=1,2,\"1p\",\"one2one\",\"user\""
# start the thing (I am choosing to read this as "NDIS DialUP")
"OK" "AT\\^NDISDUP=1,1"
];
modemConfig = oneshot {
name = "modem-configure";
# this is currently only going to work if there is one
# modem only plugged in, it is plugged in already at boot,
# and nothing else is providing a USB tty.
# https://stackoverflow.com/questions/5477882/how-to-i-detect-whether-a-tty-belonging-to-a-gsm-3g-modem-is-a-data-or-control-p
up = ''
sleep 2
${pkgs.usb-modeswitch}/bin/usb_modeswitch -v 12d1 -p 14fe --huawei-new-mode
sleep 5
${pkgs.ppp}/bin/chat -s -v ${chat} 0<>/dev/ttyUSB0 1>&0
'';
down = "chat -v '' ATZ OK </dev/ttyUSB0 >&0";
};
in svc.network.link.build {
ifname = "wwan0";
dependencies = [ modemConfig ];
};
};
}
+2 -1
View File
@@ -3,8 +3,9 @@
, linotify
, anoia
, lua
, lualinux
}:
writeFennel "acquire-delegated-prefix" {
packages = [ linotify anoia lua.pkgs.luafilesystem ];
packages = [ linotify anoia lualinux ];
mainFunction = "run";
} ./acquire-delegated-prefix.fnl
+2 -14
View File
@@ -1,7 +1,8 @@
(local subject (require :acquire-wan-address))
(local { : view } (require :fennel))
(import-macros { : expect= } :anoia.assert)
(local { : merge : dup } (require :anoia))
;; nix-shell --run "cd modules/dhcp6c && fennelrepl acquire-wan-address-test.fnl"
(local a1
{
@@ -47,19 +48,6 @@
}
)
(macro expect [assertion]
(let [msg (.. "expectation failed: " (view assertion))]
`(when (not ,assertion)
(assert false ,msg))))
(macro expect= [actual expected]
`(let [ve# (view ,expected)
va# (view ,actual)]
(when (not (= ve# va#))
(assert false
(.. "\nexpected " ve# "\ngot " va#)
))))
(fn first-address []
(let [deleted
(subject.deletions
+2 -1
View File
@@ -2,9 +2,10 @@
writeFennel
, linotify
, anoia
, lualinux
, lua
}:
writeFennel "acquire-wan-address" {
packages = [ linotify anoia lua.pkgs.luafilesystem ];
packages = [ linotify anoia lualinux ];
mainFunction = "run";
} ./acquire-wan-address.fnl
+2 -2
View File
@@ -1,6 +1,6 @@
{
liminix
, dnsmasq
, dnsmasqSmall
, serviceFns
, lib
}:
@@ -27,7 +27,7 @@ longrun {
dependencies = [ interface ];
run = ''
. ${serviceFns}
${dnsmasq}/bin/dnsmasq \
${dnsmasqSmall}/bin/dnsmasq \
--user=${user} \
--domain=${domain} \
--group=${group} \
+12 -2
View File
@@ -56,8 +56,14 @@ in
config = {
system.service.firewall =
let svc = liminix.callService ./service.nix {
ruleset = mkOption {
extraRules = mkOption {
type = types.attrsOf types.attrs;
description = "firewall ruleset";
default = {};
};
rules = mkOption {
type = types.attrsOf types.attrs; # we could usefully tighten this a bit :-)
default = import ./default-rules.nix;
description = "firewall ruleset";
};
};
@@ -68,13 +74,17 @@ in
};
in svc.build args' ;
};
programs.busybox.applets = [
"insmod" "rmmod"
];
kernel.config = {
NETFILTER = "y";
NETFILTER_ADVANCED = "y";
NETFILTER_NETLINK = "m";
NF_CONNTRACK = "m";
NETLINK_DIAG = "y";
IP6_NF_IPTABLES= "m";
IP_NF_IPTABLES = "m";
IP_NF_NAT = "m";
+2 -2
View File
@@ -4,12 +4,12 @@
, firewallgen
, nftables
}:
{ ruleset }:
{ rules, extraRules }:
let
inherit (liminix.services) oneshot;
inherit (liminix.lib) typeChecked;
inherit (lib) mkOption types;
script = firewallgen "firewall.nft" ruleset;
script = firewallgen "firewall.nft" (lib.recursiveUpdate rules extraRules);
in oneshot {
name = "firewall";
up = script;
+1 -1
View File
@@ -67,7 +67,7 @@ in {
};
loadAddress = mkOption { type = types.ints.unsigned; default = null; };
entryPoint = mkOption { type = types.ints.unsigned; };
alignment = mkOption { type = types.ints.unsigned; default = null; description = "Alignment passed to `mkimage` for FIT"; };
alignment = mkOption { type = types.nullOr types.ints.unsigned; default = null; description = "Alignment passed to `mkimage` for FIT"; };
radios = mkOption {
description = ''
Kernel modules (from mac80211 package) required for the
+18
View File
@@ -0,0 +1,18 @@
{ config, pkgs, lib, ... } :
let
inherit (pkgs) liminix;
inherit (lib) mkOption types;
in {
options.system.service.ifwait =
mkOption { type = liminix.lib.types.serviceDefn; };
config.system.service.ifwait = config.system.callService ./ifwait.nix {
state = mkOption { type = types.str; };
interface = mkOption {
type = liminix.lib.types.interface;
};
service = mkOption {
type = liminix.lib.types.service;
};
};
}
+16
View File
@@ -0,0 +1,16 @@
{ ifwait, liminix } :
{
state
, interface
, service
}:
let
inherit (liminix.services) longrun;
in longrun {
name = "ifwait.${interface.name}";
buildInputs = [ service ];
isTrigger = true;
run = ''
${ifwait}/bin/ifwait -s ${service.name} $(output ${interface} ifname) ${state}
'';
}
+27
View File
@@ -0,0 +1,27 @@
{ config, pkgs, lib, ...} :
let inherit (pkgs.liminix.services) oneshot longrun;
in {
config = {
services = rec {
mdevd = longrun {
name = "mdevd";
notification-fd = 3;
run = "${pkgs.mdevd}/bin/mdevd -D 3 -b 200000 -O4";
};
devout = longrun {
name = "devout";
notification-fd = 10;
run = "${pkgs.devout}/bin/devout /run/devout.sock 4";
};
coldplug = oneshot {
name ="coldplug";
# would love to know what mdevd-coldplug/udevadm trigger does
# that this doesn't
up = ''
for i in $(find /sys -name uevent); do ( echo change > $i ) ; done
'';
dependencies = [devout mdevd];
};
};
};
}
+32 -21
View File
@@ -19,28 +19,39 @@ in {
type = liminix.lib.types.serviceDefn;
};
};
config.system.service = {
mount = liminix.callService ./service.nix {
device = mkOption {
type = types.str;
example = "/dev/sda1";
};
mountpoint = mkOption {
type = types.str;
example = "/mnt/media";
};
options = mkOption {
type = types.listOf types.str;
default = [];
example = ["noatime" "ro" "sync"];
};
fstype = mkOption {
type = types.str;
default = "auto";
example = "vfat";
};
imports = [ ../mdevd.nix ];
config.system.service.mount =
let svc = liminix.callService ./service.nix {
partlabel = mkOption {
type = types.str;
example = "my-usb-stick";
};
mountpoint = mkOption {
type = types.str;
example = "/mnt/media";
};
options = mkOption {
type = types.listOf types.str;
default = [];
example = ["noatime" "ro" "sync"];
};
fstype = mkOption {
type = types.str;
default = "auto";
example = "vfat";
};
};
in svc // {
build = args:
let args' = args // {
dependencies = (args.dependencies or []) ++ [
config.services.mdevd
config.services.devout
];
};
in svc.build args' ;
};
};
config.programs.busybox = {
applets = ["blkid" "findfs"];
options = {
+19 -11
View File
@@ -1,18 +1,26 @@
{
liminix
, uevent-watch
, lib
}:
{ device, mountpoint, options, fstype }:
{ partlabel, mountpoint, options, fstype }:
let
inherit (liminix.services) oneshot;
in oneshot {
name = "mount.${lib.escapeURL mountpoint}";
up = ''
while ! findfs ${device}; do
echo waiting for device ${device}
sleep 1
done
mount -t ${fstype} -o ${lib.concatStringsSep "," options} ${device} ${mountpoint}
inherit (liminix.services) longrun oneshot;
device = "/dev/disk/by-partlabel/${partlabel}";
options_string =
if options == [] then "" else "-o ${lib.concatStringsSep "," options}";
mount_service = oneshot {
name = "mount.${lib.escapeURL mountpoint}";
timeout-up = 3600;
up = "mount -t ${fstype} ${options_string} ${device} ${mountpoint}";
down = "umount ${mountpoint}";
};
in longrun {
name = "watch-mount.${lib.strings.sanitizeDerivationName mountpoint}";
isTrigger = true;
buildInputs = [ mount_service ];
run = ''
${uevent-watch}/bin/uevent-watch -s ${mount_service.name} -n ${device} partname=${partlabel} devtype=partition
'';
down = "umount ${mountpoint}";
}
+1 -1
View File
@@ -122,7 +122,7 @@ in {
fdtput -p -t lx dtb /reserved-memory/$node reg $ac_prefix $(hex $rootfsStart) $sz_prefix $(hex $rootfsSize)
cmd="liminix ${cmdline} mtdparts=phram0:''${rootfsSize}(rootfs) phram.phram=phram0,''${rootfsStart},''${rootfsSize},${toString config.hardware.flash.eraseBlockSize} root=/dev/mtdblock0";
fdtput -t s dtb /chosen bootargs "$cmd"
fdtput -t s dtb /chosen ${config.boot.commandLineDtbNode} "$cmd"
dtbSize=$(binsize ./dtb )
+15
View File
@@ -17,6 +17,9 @@ in {
system.service.pppoe = mkOption {
type = liminix.lib.types.serviceDefn;
};
system.service.l2tp = mkOption {
type = liminix.lib.types.serviceDefn;
};
};
config = {
system.service.pppoe = pkgs.liminix.callService ./pppoe.nix {
@@ -29,6 +32,16 @@ in {
description = "options supplied on ppp command line";
};
};
system.service.l2tp = pkgs.liminix.callService ./l2tp.nix {
lns = mkOption {
type = types.str;
description = "hostname or address of the L2TP network server";
};
ppp-options = mkOption {
type = types.listOf types.str;
description = "options supplied on ppp command line";
};
};
kernel = {
config = {
PPP = "y";
@@ -36,6 +49,8 @@ in {
PPP_DEFLATE = "y";
PPP_ASYNC = "y";
PPP_SYNC_TTY = "y";
PPPOL2TP = "y";
L2TP = "y";
};
};
};
+62
View File
@@ -0,0 +1,62 @@
{
liminix
, lib
, ppp
, pppoe
, writeAshScript
, writeText
, serviceFns
, xl2tpd
} :
{ lns, ppp-options }:
let
inherit (liminix.services) longrun;
name = "${lns}.l2tp";
ip-up = writeAshScript "ip-up" {} ''
. ${serviceFns}
(in_outputs ${name}
echo $1 > ifname
echo $2 > tty
echo $3 > speed
echo $4 > address
echo $5 > peer-address
echo $DNS1 > ns1
echo $DNS2 > ns2
)
echo >/proc/self/fd/10
'';
ip6-up = writeAshScript "ip6-up" {} ''
. ${serviceFns}
(in_outputs ${name}
echo $4 > ipv6-address
echo $5 > ipv6-peer-address
)
echo >/proc/self/fd/10
'';
ppp-options' = ppp-options ++ [
"ip-up-script" ip-up
"ipv6-up-script" ip6-up
"ipparam" name
"nodetach"
"usepeerdns"
"logfd" "2"
];
conf = writeText "xl2tpd.conf" ''
[lac upstream]
lns = ${lns}
require authentication = no
pppoptfile = ${writeText "ppp-options" ppp-options'}
autodial = yes
redial = yes
'';
control = "/run/xl2tpd/control-${name}";
in
longrun {
inherit name;
run = ''
mkdir -p /run/xl2tpd
touch ${control}
exec ${xl2tpd}/bin/xl2tpd -D -p /run/xl2tpd/${name}.pid -c ${conf} -C ${control}
'';
notification-fd = 10;
}
+178
View File
@@ -0,0 +1,178 @@
{ config, pkgs, lib, ... } :
let
svc = config.system.service;
cfg = config.profile.gateway;
inherit (lib) mkOption mkEnableOption mkIf mdDoc types optional optionals;
inherit (pkgs) liminix serviceFns;
inherit (liminix.services) bundle oneshot;
hostaps =
let
defaults = {
auth_algs = 1; # 1=wpa2, 2=wep, 3=both
wpa = 2; # 1=wpa, 2=wpa2, 3=both
wpa_key_mgmt = "WPA-PSK";
wpa_pairwise = "TKIP CCMP"; # auth for wpa (may not need this?)
rsn_pairwise = "CCMP"; # auth for wpa2
};
in lib.mapAttrs'
(name : value :
let
attrs = defaults // { ssid = name; } // value;
in lib.nameValuePair
"hostap-${name}"
(svc.hostapd.build {
interface = attrs.interface;
params = lib.filterAttrs (k: v: k != "interface") attrs;
}))
cfg.wireless.networks;
in {
options.profile.gateway = {
lan = {
interfaces = mkOption {
type = types.listOf liminix.lib.types.interface;
default = [];
};
address = mkOption {
type = types.attrs;
};
prefix = mkOption { type = types.str; };
dhcp = {
start = mkOption { type = types.int; };
end = mkOption { type = types.int; };
hosts = mkOption { type = types.attrs; };
localDomain = mkOption { type = types.str; };
};
};
firewall = {
enable = mkEnableOption "firewall";
rules = mkOption { type = types.attrsOf types.attrs; };
};
wan = {
interface = mkOption { type = liminix.lib.types.interface; };
username = mkOption { type = types.str; };
password = mkOption { type = types.str; };
dhcp6.enable = mkOption { type = types.bool; };
};
wireless = mkOption {
type = types.attrsOf types.anything;
};
};
imports = [
../wlan.nix
../network
../ppp
../dnsmasq
../dhcp6c
../firewall
../hostapd
../bridge
../ntp
../ssh
{ config.services = hostaps; }
];
config = {
services.int = svc.network.address.build ({
interface = svc.bridge.primary.build { ifname = "int"; };
} // cfg.lan.address);
services.bridge = svc.bridge.members.build {
primary = config.services.int;
members = cfg.lan.interfaces;
};
services.wan = svc.pppoe.build {
inherit (cfg.wan) interface;
ppp-options = [
"debug" "+ipv6" "noauth"
"name" cfg.wan.username
"password" cfg.wan.password
];
};
services.packet_forwarding = svc.network.forward.build { };
services.dhcp6c =
let
client = svc.dhcp6c.client.build {
interface = config.services.wan;
};
bundl = bundle {
name = "dhcp6c";
contents = [
(svc.dhcp6c.prefix.build {
inherit client;
interface = config.services.int;
})
(svc.dhcp6c.address.build {
inherit client;
interface = config.services.wan;
})
];
};
in mkIf cfg.wan.dhcp6.enable bundl;
services.dns =
let interface = config.services.int;
dcfg = cfg.lan.dhcp;
in svc.dnsmasq.build {
resolvconf = config.services.resolvconf;
inherit interface;
ranges = [
"${cfg.lan.prefix}.${toString dcfg.start},${cfg.lan.prefix}.${toString dcfg.end}"
# ra-stateless: sends router advertisements with the O and A
# bits set, and provides a stateless DHCP service. The client
# will use a SLAAC address, and use DHCP for other
# configuration information.
"::,constructor:$(output ${interface} ifname),ra-stateless"
];
hosts = dcfg.hosts;
upstreams = [ "/${dcfg.localDomain}/" ];
domain = dcfg.localDomain;
};
services.defaultroute4 = svc.network.route.build {
via = "$(output ${config.services.wan} address)";
target = "default";
dependencies = [ config.services.wan ];
};
services.defaultroute6 = svc.network.route.build {
via = "$(output ${config.services.wan} ipv6-peer-address)";
target = "default";
interface = config.services.wan;
};
services.firewall = mkIf cfg.firewall.enable
(svc.firewall.build {
extraRules = cfg.firewall.rules;
});
services.resolvconf = oneshot rec {
dependencies = [ config.services.wan ];
name = "resolvconf";
up = ''
. ${serviceFns}
( in_outputs ${name}
echo "nameserver $(output ${config.services.wan} ns1)" > resolv.conf
echo "nameserver $(output ${config.services.wan} ns2)" >> resolv.conf
chmod 0444 resolv.conf
)
'';
};
filesystem =
let inherit (pkgs.pseudofile) dir symlink;
in dir {
etc = dir {
"resolv.conf" = symlink "${config.services.resolvconf}/.outputs/resolv.conf";
};
};
};
}
+98
View File
@@ -0,0 +1,98 @@
{
config,
pkgs,
lib,
...
}: let
inherit (pkgs) liminix;
inherit (lib) mkEnableOption mkOption types isDerivation hasAttr ;
inherit (pkgs.liminix.services) oneshot longrun bundle target;
inherit (pkgs.pseudofile) dir symlink;
inherit (pkgs) serviceFns;
svc = config.system.service;
cfg = config.profile.wap;
hostaps =
let
defaults = {
auth_algs = 1; # 1=wpa2, 2=wep, 3=both
wpa = 2; # 1=wpa, 2=wpa2, 3=both
wpa_key_mgmt = "WPA-PSK";
wpa_pairwise = "TKIP CCMP"; # auth for wpa (may not need this?)
rsn_pairwise = "CCMP"; # auth for wpa2
};
in lib.mapAttrs'
(name : value :
let
attrs = defaults // { ssid = name; } // value;
in lib.nameValuePair
"hostap-${name}"
(svc.hostapd.build {
interface = attrs.interface;
params = lib.filterAttrs (k: v: k != "interface") attrs;
}))
cfg.wireless.networks;
in {
imports = [
../wlan.nix
../network
../hostapd
../bridge
{ config.services = hostaps; }
];
options.profile.wap = {
interfaces = mkOption {
type = types.listOf liminix.lib.types.interface;
default = [];
};
wireless = mkOption {
type = types.attrsOf types.anything;
};
};
config = {
services.int = svc.bridge.primary.build {
ifname = "int";
};
services.bridge = svc.bridge.members.build {
primary = config.services.int;
members = cfg.interfaces;
};
services.dhcpc = svc.network.dhcp.client.build {
interface = config.services.int;
dependencies = [ config.services.hostname ];
};
services.defaultroute4 = svc.network.route.build {
via = "$(output ${config.services.dhcpc} router)";
target = "default";
dependencies = [config.services.dhcpc];
};
services.resolvconf = oneshot rec {
dependencies = [ config.services.dhcpc ];
name = "resolvconf";
# CHECK: https://udhcp.busybox.net/README.udhcpc says
# 'A list of DNS server' but doesn't say what separates the
# list members. Assuming it's a space or other IFS character
up = ''
. ${serviceFns}
( in_outputs ${name}
for i in $(output ${config.services.dhcpc} dns); do
echo "nameserver $i" > resolv.conf
done
)
'';
};
filesystem = dir {
etc = dir {
"resolv.conf" = symlink "${config.services.resolvconf}/.outputs/resolv.conf";
};
};
};
}
+1 -1
View File
@@ -34,7 +34,7 @@ fi
### If your services are managed by s6-rc:
### (replace /run/service with your scandir)
s6-rc-init /run/service -d -c /etc/s6-rc/compiled
s6-rc-init -d -c /etc/s6-rc/compiled /run/service
### 2. Starting the wanted set of services
+1
View File
@@ -15,4 +15,5 @@ in oneshot rec {
)
'';
down = "ip link set down dev ${ifname}";
dependencies = [ primary ];
}
+2 -1
View File
@@ -1,6 +1,7 @@
{
liminix
, lib
, s6
}:
{ watched, headStart } :
let
@@ -8,5 +9,5 @@ let
in longrun {
name = "watchdog";
run =
"HEADSTART=${toString headStart} ${./gaspode.sh} ${lib.concatStringsSep " " (builtins.map (s: s.name) watched)}";
"PATH=${s6}/bin:$PATH HEADSTART=${toString headStart} ${./gaspode.sh} ${lib.concatStringsSep " " (builtins.map (s: s.name) watched)}";
}
+19 -1
View File
@@ -77,8 +77,24 @@ extraPkgs // {
};
# luarocks wants a cross-compiled cmake (which seems like a bug,
# we're never going to run luarocks on the device, but ...)
# but https://github.com/NixOS/nixpkgs/issues/284734
# so we do surgery on the cmake derivation until that's fixed
dnsmasq =
cmake = prev.cmake.overrideAttrs(o:
# don't override the build cmake or we'll have to rebuild
# half the known universe to no useful benefit
if final.stdenv.buildPlatform != final.stdenv.hostPlatform
then {
preConfigure =
builtins.replaceStrings
["$configureFlags"] ["$configureFlags $cmakeFlags"] o.preConfigure;
}
else {}
);
dnsmasqSmall =
let d = prev.dnsmasq.overrideAttrs(o: {
preBuild = ''
makeFlagsArray=("COPTS=")
@@ -255,6 +271,8 @@ extraPkgs // {
'';
};
libusb1 = prev.libusb1.override { enableUdev = false; };
util-linux-small = prev.util-linux.override {
ncursesSupport = false;
pamSupport = false;
+14
View File
@@ -0,0 +1,14 @@
default: fs.lua init.lua nl.lua svc.lua net/constants.lua
test:
ln -s . anoia
fennel test.fnl
fennel test-svc.fnl
net/constants.lua: net/constants.c
$(CC) -imacros sys/socket.h -imacros linux/netlink.h -E -P - < net/constants.c | sed 's/ *$$//g' | cat -s > net/constants.lua
%.lua: %.fnl
fennel --compile $< > $@
+21
View File
@@ -0,0 +1,21 @@
;; these are macros; this module should be imported
;; using import-macros
;; e.g. (import-macros { : expect= } :anoia.assert)
(fn expect [assertion]
(let [msg (.. "expectation failed: " (view assertion))]
`(when (not ,assertion)
(assert false ,msg))))
(fn expect= [actual expected]
`(let [view# (. (require :fennel) :view)
ve# (view# ,expected)
va# (view# ,actual)]
(when (not (= ve# va#))
(assert false
(.. "\nexpected " ve# "\ngot " va#)
))))
{ : expect : expect= }
+13 -8
View File
@@ -1,22 +1,27 @@
{
fennel
, stdenv
, linotify
, lua
, lualinux
, cpio
}:
let pname = "anoia";
in stdenv.mkDerivation {
inherit pname;
version = "0.1";
src = ./.;
nativeBuildInputs = [ fennel ];
buildInputs = with lua.pkgs; [ luafilesystem ];
buildPhase = ''
for f in *.fnl ; do
fennel --compile $f > `basename $f .fnl`.lua
done
'';
nativeBuildInputs = [ fennel cpio ];
buildInputs = with lua.pkgs; [ linotify lualinux ];
outputs = [ "out" "dev" ];
doCheck = true;
installPhase = ''
mkdir -p "$out/share/lua/${lua.luaversion}/${pname}"
cp *.lua "$out/share/lua/${lua.luaversion}/${pname}"
find . -name \*.lua | cpio -p -d "$out/share/lua/${lua.luaversion}/${pname}"
mkdir -p "$dev/share/lua/${lua.luaversion}/${pname}"
cp assert.fnl "$dev/share/lua/${lua.luaversion}/${pname}"
'';
}
+43 -10
View File
@@ -1,7 +1,30 @@
(local lfs (require :lfs))
(local ll (require :lualinux))
(local S_IFMT 0xf000)
(local S_IFSOCK 0xc000)
(local S_IFLNK 0xa000)
(local S_IFREG 0x8000)
(local S_IFBLK 0x6000)
(local S_IFDIR 0x4000)
(local S_IFCHR 0x2000)
(local S_IFIFO 0x1000)
(fn ifmt-bits [mode] (and mode (band mode 0xf000)))
(fn file-type [pathname]
(. {
S_IFDIR :directory
S_IFSOCK :socket
S_IFLNK :link
S_IFREG :file
S_IFBLK :block-device
S_IFCHR :character-device
S_IFIFO :fifo
}
(ifmt-bits (ll.lstat3 pathname))))
(fn directory? [pathname]
(= (lfs.symlinkattributes pathname :mode) "directory"))
(= (file-type pathname) :directory))
(fn mktree [pathname]
(if (or (= pathname "") (= pathname "/"))
@@ -10,27 +33,37 @@
(or (directory? pathname)
(let [parent (string.gsub pathname "/[^/]+/?$" "")]
(or (directory? parent) (mktree parent))
(assert (lfs.mkdir pathname)))))
(assert (ll.mkdir pathname)))))
(fn dir [name]
(let [dp (assert (ll.opendir name) name)]
(fn []
(match (ll.readdir dp)
(name type) (values name type)
(nil err) (do (if err (print err)) (ll.closedir dp) nil)))))
(fn rmtree [pathname]
(case (lfs.symlinkattributes pathname)
(case (file-type pathname)
nil true
{:mode "directory"}
:directory
(do
(each [f (lfs.dir pathname)]
(each [f (dir pathname)]
(when (not (or (= f ".") (= f "..")))
(rmtree ( .. pathname "/" f)))
(lfs.rmdir pathname)))
{:mode "file"}
(ll.rmdir pathname)))
:file
(os.remove pathname)
{:mode "link"}
:link
(os.remove pathname)
unknown
(error (.. "can't remove " pathname " of kind \"" unknown.mode "\""))))
(error (.. "can't remove " pathname " of mode \"" unknown "\""))))
{
: mktree
: rmtree
: directory?
: dir
: file-type
:symlink (fn [from to] (ll.symlink from to))
}
+22 -1
View File
@@ -1,3 +1,7 @@
(fn assoc [tbl k v]
(tset tbl k v)
tbl)
(fn merge [table1 table2]
(collect [k v (pairs table2) &into table1]
k v))
@@ -14,6 +18,12 @@
f (do (f:close) true)
_ false))
(fn basename [path]
(string.match path ".*/([^/]-)$"))
(fn dirname [path]
(string.match path "(.*)/[^/]-$"))
(fn system [s]
(match (os.execute s)
res (do (print (.. "Executed \"" s "\", exit code " (tostring res))) res)
@@ -62,4 +72,15 @@
(s:sub 1 (- (# s) pad))))
{ : merge : split : file-exists? : system : hash : base64url : dup }
{
: assoc
: base64url
: basename
: dirname
: dup
: file-exists?
: hash
: merge
: split
: system
}
+11
View File
@@ -0,0 +1,11 @@
#define MACRO(c) [#c] = c,
return {
MACRO(SOCK_STREAM)
MACRO(SOCK_DGRAM)
MACRO(SOCK_RAW)
MACRO(AF_LOCAL)
MACRO(AF_NETLINK)
MACRO(NETLINK_KOBJECT_UEVENT)
}
+15
View File
@@ -0,0 +1,15 @@
(local netlink (require :netlink))
; (local { : view } (require :fennel))
(fn events [groups]
(let [sock (netlink.socket)]
(coroutine.wrap
(fn []
(each [_ e (ipairs (sock:query groups))]
(coroutine.yield e))
(while (sock:poll)
(each [_ e (ipairs (sock:event))]
(coroutine.yield e)))))))
{ : events }
+6 -7
View File
@@ -1,7 +1,6 @@
(local inotify (require :inotify))
(local { : file-exists? } (require :anoia))
(local { : directory? } (require :anoia.fs))
(local lfs (require :lfs))
(local { : file-type : dir &as fs } (require :anoia.fs))
(fn read-line [name]
(with-open [f (assert (io.open name :r) (.. "can't open file " name))]
@@ -20,15 +19,15 @@
handle))
(fn read-value [pathname]
(case (lfs.symlinkattributes pathname)
(case (file-type pathname)
nil nil
{:mode "directory"}
(collect [f (lfs.dir pathname)]
:directory
(collect [f (fs.dir pathname)]
(when (not (or (= f ".") (= f "..")))
(values f (read-value ( .. pathname "/" f)))))
{:mode "file"}
:file
(read-line pathname)
{:mode "link"}
:link
(read-line pathname)
unknown
(error (.. "can't read " pathname " of kind \"" unknown.mode "\""))))
+7
View File
@@ -0,0 +1,7 @@
(local nl (require :anoia.nl))
(local { : view } (require :fennel))
(let [events (nl.events {:link true})]
(each [ev events]
(print "got one ")
(print (view ev))))
+1 -1
View File
@@ -1,4 +1,4 @@
(local svc (require :anoia.svc))
(local svc (require :svc))
(local { : view } (require :fennel))
(local ex (svc.open "./example-output"))
+6 -5
View File
@@ -1,9 +1,10 @@
(local { : hash : base64url } (require :anoia))
(local { : hash : base64url } (require :init))
(import-macros { : expect= } :assert)
(assert (= (hash "") 5381))
(expect= (hash "") 5381)
;; these examples from https://theartincode.stanis.me/008-djb2/
(assert (= (hash "Hello") 210676686969))
(assert (= (hash "Hello!") 6952330670010))
(expect= (hash "Hello") 210676686969)
(expect= (hash "Hello!") 6952330670010)
(assert (= (base64url "hello world") "aGVsbG8gd29ybGQ"))
(expect= (base64url "hello world") "aGVsbG8gd29ybGQ")
+6
View File
@@ -56,6 +56,7 @@ in {
# please keep the rest of this list alphabetised :-)
anoia = callPackage ./anoia {};
devout = callPackage ./devout {};
fennel = callPackage ./fennel {};
fennelrepl = callPackage ./fennelrepl {};
firewallgen = callPackage ./firewallgen {};
@@ -70,6 +71,7 @@ in {
levitate = callPackage ./levitate {};
libubootenv = callPackage ./libubootenv {};
linotify = callPackage ./linotify {};
lualinux = callPackage ./lualinux {};
# we need to build real lzma instead of using xz, because the lzma
# decoder in u-boot doesn't understand streaming lzma archives
@@ -82,6 +84,8 @@ in {
zyxel-bootconfig = callPackage ./zyxel-bootconfig {};
min-collect-garbage = callPackage ./min-collect-garbage {};
min-copy-closure = callPackage ./min-copy-closure {};
minisock = callPackage ./minisock {};
nellie = callPackage ./nellie {};
netlink-lua = callPackage ./netlink-lua {};
odhcp-script = callPackage ./odhcp-script {};
odhcp6c = callPackage ./odhcp6c {};
@@ -105,6 +109,8 @@ in {
swconfig = callPackage ./swconfig {};
systemconfig = callPackage ./systemconfig {};
tufted = callPackage ./tufted {};
uevent-watch = callPackage ./uevent-watch {};
usb-modeswitch = callPackage ./usb-modeswitch {};
writeAshScript = callPackage ./write-ash-script {};
writeFennel = callPackage ./write-fennel {};
writeFennelScript = callPackage ./write-fennel-script {};
+26
View File
@@ -0,0 +1,26 @@
{
lua
, nellie
, writeFennel
, anoia
, fennel
, stdenv
, fennelrepl
, lualinux
}:
stdenv.mkDerivation {
name = "devout";
src = ./.;
nativeBuildInputs = [ fennelrepl ];
postBuild = ''
LUA_CPATH=${lualinux}/lib/lua/5.3/?.so\;$LUA_CPATH \
fennelrepl ./test.fnl
'';
installPhase = ''
mkdir -p $out/bin
cp -p ${writeFennel "devout" {
packages = [fennel anoia nellie lualinux];
mainFunction = "run";
} ./devout.fnl} $out/bin/devout
'';
}
+173
View File
@@ -0,0 +1,173 @@
(local ll (require :lualinux))
(local {
: AF_LOCAL
: AF_NETLINK
: SOCK_STREAM
: SOCK_RAW
: NETLINK_KOBJECT_UEVENT
} (require :anoia.net.constants))
(local { : view } (require :fennel))
(fn trace [expr]
(do (print :TRACE (view expr)) expr))
(macro check-errno [expr]
(let [{ :view v } (require :fennel)]
`(case ,expr
val# val#
(nil err#) (error (string.format "%s failed: errno=%d" ,(v expr) err#)))))
(fn format-event [e]
(..
(string.format "%s@%s\0" e.action e.path)
(table.concat
(icollect [k v (pairs e.attributes)]
(string.format "%s=%s" (string.upper k) v ))
"\n")))
(fn event-matches? [e terms]
(accumulate [match? true
name value (pairs terms)]
(and match? (= value (. e.attributes name)))))
(fn parse-event [s]
(let [at (string.find s "@" 1 true)
(nl nxt) (string.find s "\0" 1 true)
attributes
(collect [k v (string.gmatch
(string.sub s (+ 1 nxt))
"(%g-)=(%g+)")]
(k:lower) v)]
{ : attributes
:path (string.sub s (+ at 1) (- nl 1))
:action (string.sub s 1 (- at 1))
:format format-event
:matches? event-matches?
}))
(fn find-in-database [db terms]
(accumulate [found []
_ e (pairs db)]
(if (e:matches? terms)
(doto found (table.insert e))
found)))
(fn record-event [db subscribers str]
(let [e (parse-event str)]
(match e.action
:add (tset db e.path e)
:change (tset db e.path e)
;; should we do something for bind?
:remove (tset db e.path nil)
)
(each [_ { : terms : callback } (pairs subscribers)]
(if (e:matches? terms) (callback e)))
e))
(fn database []
(let [db {}
subscribers []]
{
:find (fn [_ terms] (find-in-database db terms))
:add (fn [_ event-string] (when event-string (record-event db subscribers event-string)))
:at-path (fn [_ path] (. db path))
:subscribe (fn [_ id callback terms]
(let [past-events (find-in-database db terms)]
(each [_ e (pairs past-events)]
(callback e)))
(tset subscribers id {: callback : terms }))
:unsubscribe (fn [_ id] (tset subscribers id nil))
}))
;; grepped from kernel headers
(local POLLIN 0x0001)
(local POLLPRI 0x0002)
(local POLLOUT 0x0004)
(local POLLERR 0x0008)
(local POLLHUP 0x0010)
(local POLLNVAL 0x0020)
(fn unix-socket [name]
(let [addr (string.pack "=Hz" AF_LOCAL name)]
(case (ll.socket AF_LOCAL SOCK_STREAM 0)
fd (case (ll.bind fd addr)
0 (doto fd (ll.listen 32))
(nil err) (values nil err))
(nil err) (values nil err))))
(fn pollfds-for [fds]
(icollect [_ v (ipairs fds)]
(bor (lshift v 32) (lshift 1 16))))
(fn unpack-pollfds [pollfds]
(collect [_ v (ipairs pollfds)]
(let [fd (band (rshift v 32) 0xffffffff)
revent (band v 0xffff)]
(values fd (if (> revent 0) revent nil)))))
(fn parse-terms [str]
(collect [n (string.gmatch (str:gsub "\n+$" "") "([^ ]+)")]
(string.match n "(.-)=(.+)")))
(fn handle-client [db client]
(match (ll.read client)
"" (do
(db:unsubscribe client)
false)
s (do
(db:subscribe
client
(fn [e]
(ll.write client (format-event e)))
(parse-terms s))
true)
(nil err) (do (print err) false)))
(fn open-netlink [groups]
(match (ll.socket AF_NETLINK SOCK_RAW NETLINK_KOBJECT_UEVENT)
fd (doto fd (ll.bind (string.pack "I2I2I4I4" ; family pad pid groups
AF_NETLINK 0 0 groups)))
(nil errno) (values nil errno)))
(fn event-loop []
(let [fds {}]
{
:register #(tset fds $2 $3)
:feed (fn [_ revents]
(each [fd revent (pairs revents)]
(when (not ((. fds fd) fd))
(tset fds fd nil)
(ll.close fd))))
:fds #(icollect [fd _ (pairs fds)] fd)
:_tbl #(do fds) ;exposed for tests
}))
(fn run []
(let [[sockname nl-groups] arg
s (check-errno (unix-socket sockname))
db (database)
nl (check-errno (open-netlink nl-groups))
loop (event-loop)]
(loop:register
s
#(case
(ll.accept s)
(client addr)
(do
(loop:register client (partial handle-client db))
true)
(nil err)
(print (string.format "error accepting connection, errno=%d" err))))
(loop:register
nl
#(do (db:add (ll.read nl)) true))
(ll.write 10 "ready\n")
(while true
(let [pollfds (pollfds-for (loop:fds))]
(ll.poll pollfds 5000)
(loop:feed (unpack-pollfds pollfds))))))
{ : database : run : event-loop : parse-event }
+210
View File
@@ -0,0 +1,210 @@
(local { : database : event-loop : parse-event } (require :devout))
(local { : view } (require :fennel))
(local ll (require :lualinux))
(import-macros { : expect : expect= } :anoia.assert)
(var failed false)
(fn fail [d msg] (set failed true) (print :FAIL d (.. "\n" msg)))
(macro example [description & body]
(if (. body 1)
`(let [(ok?# err#) (xpcall (fn [] ,body) debug.traceback)]
(if ok?#
(print :PASS ,description)
(fail ,description err#)))
`(print :PENDING ,description)))
(local sda-uevent
"add@/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/block/sda\0ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/block/sda
SUBSYSTEM=block
MAJOR=8
MINOR=0
DEVNAME=sda
DEVTYPE=disk
DISKSEQ=2
SEQNUM=1527")
(local sdb1-insert
"add@/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1\0ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1
SUBSYSTEM=block
DEVNAME=/dev/sdb1
DEVTYPE=partition
DISKSEQ=33
PARTN=1
SEQNUM=82381
MAJOR=8
MINOR=17")
(local sdb1-remove
"remove@/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1\0ACTION=remove
DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1
SUBSYSTEM=block
DEVNAME=/dev/sdb1
DEVTYPE=partition
DISKSEQ=33
PARTN=1
SEQNUM=82386
MAJOR=8
MINOR=17")
(example
"I can parse an event"
(let [e (parse-event sdb1-insert)]
(expect= e.attributes.seqnum "82381")
(expect= e.attributes.devname "/dev/sdb1")
(expect= e.path "/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1")
(expect= e.action :add)
(expect= e (parse-event (e:format)))))
(example
"An event can match against terms"
(let [terms {:devname "foo" :partname "my-usbstick"}]
(expect= (: (parse-event "add@/\0SEQNUM=1") :matches? terms) false)
(expect= (: (parse-event "add@/\0DEVNAME=bill") :matches? terms) false)
(expect= (: (parse-event "add@/\0DEVNAME=foo\nPARTNAME=my-usbstick") :matches? terms) true)
(expect= (: (parse-event "add@/\0DEVNAME=foo\nPARTNAME=my-usbstick\nOTHERTHING=bar") :matches? terms) true)
))
(example
"given an empty database, searching it finds no entries"
(let [db (database)]
(expect= (db:find {:partname "boot"}) [])))
(example
"when I add a device, I can find it"
(let [db (database)]
(db:add sda-uevent)
(let [[m & more] (db:find {:devname "sda"})]
(expect= m.attributes.devname "sda")
(expect= m.attributes.major "8")
(expect= more []))))
(example
"when I add a device, I cannot find it with wrong terms"
(let [db (database)]
(db:add sda-uevent)
(expect= (db:find {:devname "sdb"}) [])))
(example
"when I add a device, I can retrieve it by path"
(let [db (database)]
(db:add sda-uevent)
(let [m (db:at-path "/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/block/sda")]
(expect= m.attributes.devname "sda")
(expect= m.attributes.major "8"))))
(example
"when I add and then remove a device, I cannot retrieve it by path"
(let [db (database)]
(db:add sdb1-insert)
(db:add sdb1-remove)
(expect= (db:at-path "/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1") nil)))
(example
"when I add and then remove a device, I cannot find it"
(let [db (database)]
(db:add sdb1-insert)
(db:add sda-uevent)
(db:add sdb1-remove)
(expect= (db:find {:devname "/dev/sdb1"}) [])))
(example
"when I search on multiple terms it uses all of them"
(let [db (database)]
(db:add sda-uevent)
(expect= (# (db:find {:devname "sda" :devtype "disk"})) 1)
(expect= (# (db:find {:devname "sda" :devtype "dosk"})) 0)))
;;; tests for indices
(example "when I add a device with $attributes major minor foo bar baz,
it is added to indices for foo bar baz but not major minor")
(example "a removed device can no longer be found by looking in any index")
(example "when I query with multiple attributes, the search is performed using the most specific attribute"
;; (= the attribute whose
;; value at this key has fewest elements)
)
;;; tests for subscriptions
(example
"I can subscribe to some search terms and be notified of matching events"
(var received [])
(let [db (database)
subscriber (fn [e] (table.insert received e))]
(db:subscribe :me subscriber {:devname "/dev/sdb1"})
(db:add sdb1-insert)
(db:add sda-uevent)
(db:add sdb1-remove)
(expect= (# received) 2)))
(example
"Subscribers get notifications of prior events for present devices"
(var received [])
(let [db (database)
subscriber (fn [e] (table.insert received e))]
(db:add sdb1-insert)
(db:add sda-uevent)
(db:subscribe :me subscriber {:devname "/dev/sdb1"})
(expect= (# received) 1)))
(example
"I can unsubscribe after subscribing"
(var received [])
(let [db (database)
subscriber (fn [e] (table.insert received e))]
(db:subscribe :me subscriber {:devname "/dev/sdb1"})
(db:unsubscribe :me)
(db:add sdb1-insert)
(db:add sda-uevent)
(db:add sdb1-remove)
(expect= (# received) 0)))
;;; test for event loop
(example
"I can register a fd with a callback"
(let [loop (event-loop)
cb #(print $1)]
(loop:register 3 cb)
(expect= (. (loop:_tbl) 3) cb)))
(example
"when the fd is ready, my callback is called"
(let [loop (event-loop)]
(var ran? false)
(loop:register 3 #(set ran? true))
(loop:feed {3 1})
(expect= ran? true)
))
(example
"when the callback returns true it remains registered"
(let [loop (event-loop)]
(loop:register 3 #true)
(loop:feed {3 1})
(expect (. (loop:_tbl) 3))
))
(fn new-fd []
(ll.open "/dev/zero" 0 0x1ff))
(example
"when the callback returns false it is unregistered and the fd is closed"
(let [loop (event-loop)
fd (new-fd)]
(expect (> fd 2))
(loop:register 3 #false)
(loop:feed {3 1})
(expect (not (. (loop:_tbl) 3)))
(assert (not (os.execute (string.format "test -e /dev/fd/%d" fd))))
))
(if failed (os.exit 1) (print "OK"))
+6 -1
View File
@@ -5,16 +5,20 @@
, lib
, luaPackages
, lua
, lualinux
, writeScriptBin
, linotify
, anoia
, netlink-lua
, fennel
}:
let packages = [
linotify
anoia
fennel
lua.pkgs.luafilesystem
lualinux
netlink-lua
lua.pkgs.readline
];
join = ps: builtins.concatStringsSep ";" ps;
luapath = join (builtins.map (f:
@@ -29,6 +33,7 @@ in writeScriptBin "fennelrepl" ''
package.cpath = ${lib.strings.escapeShellArg luacpath} .. ";" .. (package.cpath or "")
local fennel = require "fennel"
table.insert(package.loaders or package.searchers,1, fennel.searcher)
fennel['macro-path'] = "${anoia.dev}/share/lua/${lua.luaversion}/?.fnl;" .. fennel['macro-path']
local more_fennel = os.getenv("FENNEL_PATH")
if more_fennel then
+2 -1
View File
@@ -3,8 +3,9 @@
, netlink-lua
, writeFennelScript
, runCommand
, anoia
}:
runCommand "ifwait" {} ''
mkdir -p $out/bin
cp -p ${writeFennelScript "ifwait" [netlink-lua] ./ifwait.fnl} $out/bin/ifwait
cp -p ${writeFennelScript "ifwait" [anoia netlink-lua] ./ifwait.fnl} $out/bin/ifwait
''
+195
View File
@@ -0,0 +1,195 @@
{:event "newlink"
:hwaddr "00:00:00:00:00:00"
:index 1
:mtu 65536
:name "lo"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "50:3e:aa:08:df:52"
:index 2
:mtu 1500
:name "enp1s0"
:running "no"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "1c:1b:0d:9c:39:2d"
:index 3
:mtu 1500
:name "enp0s31f6"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "da:4d:53:c3:54:43"
:index 4
:mtu 1500
:name "vbridge0"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "00:28:f8:69:fa:14"
:index 6
:mtu 1500
:name "wlp4s0"
:running "no"
:stamp 857161382
:up "no"}
{:event "newlink"
:hwaddr "02:42:b1:e6:e5:bd"
:index 7
:mtu 1500
:name "br-7ddfef4820c5"
:running "no"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "02:42:8d:d4:36:34"
:index 8
:mtu 1500
:name "br-95da8b40a7cc"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "02:42:bc:cf:a8:5e"
:index 9
:mtu 1500
:name "docker0"
:running "no"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "b6:66:50:69:33:a6"
:index 11
:mtu 1500
:name "veth2ff6ec3"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "e6:94:c8:48:f3:97"
:index 13
:mtu 1500
:name "veth0913974"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "9a:87:d8:f2:c6:96"
:index 15
:mtu 1500
:name "veth0e74156"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "5e:d2:92:b9:5f:6d"
:index 17
:mtu 1500
:name "veth89a36b3"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "ca:88:3f:09:bc:51"
:index 19
:mtu 1500
:name "veth73c1e0b"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "b6:7d:5c:38:89:1d"
:index 21
:mtu 1500
:name "dummy0"
:running "no"
:stamp 857161382
:up "no"}
{:event "newlink"
:hwaddr "52:f0:46:da:0c:0c"
:index 22
:mtu 1500
:name "dummy1"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newneigh"
:hwaddr "00:22:61:3d:f7:54"
:index 4
:ip "192.168.8.140"
:probes 1
:stamp 857165355
:state "stale"}
{:event "delneigh"
:hwaddr "5c:60:ba:58:34:93"
:index 3
:stamp 857166891
:state "stale"}
{:event "newneigh"
:hwaddr "80:64:6f:9e:15:02"
:index 4
:ip "192.168.8.161"
:probes 1
:stamp 857172523
:state "stale"}
{:event "newneigh"
:hwaddr "e4:95:6e:42:c2:6c"
:index 3
:stamp 857174763
:state "reachable"}
{:event "newneigh"
:hwaddr "e4:b3:18:76:1b:23"
:index 4
:ip "2001:8b0:de3a:40de:4708:c700:4de2:9264"
:probes 1
:stamp 857175595
:state "stale"}
{:event "newneigh"
:hwaddr "80:64:6f:9e:10:c6"
:index 4
:ip "192.168.8.53"
:probes 1
:stamp 857176619
:state "stale"}
{:event "newneigh"
:hwaddr "80:64:6f:9e:15:02"
:index 4
:ip "192.168.8.161"
:probes 1
:stamp 857177643
:state "probe"}
{:event "newneigh"
:hwaddr "80:64:6f:9e:15:02"
:index 4
:ip "192.168.8.161"
:probes 1
:stamp 857177644
:state "reachable"}
{:event "newlink"
:hwaddr "b6:7d:5c:38:89:1d"
:index 21
:mtu 1500
:name "dummy0"
:running "yes"
:stamp 857178258
:up "yes"}
{:event "newlink"
:hwaddr "b6:7d:5c:38:89:1d"
:index 21
:mtu 1500
:name "dummy0"
:running "no"
:stamp 857181661
:up "no"}
{:event "newneigh"
:hwaddr "80:64:6f:9e:10:c6"
:index 4
:ip "192.168.8.53"
:probes 1
:stamp 857182251
:state "probe"}
+43 -31
View File
@@ -1,52 +1,64 @@
(local netlink (require :netlink))
(local sock (netlink.socket))
(local nl (require :anoia.nl))
(local { : assoc : system } (require :anoia))
; (local { : view} (require :fennel))
(fn assoc [tbl k v]
(tset tbl k v)
tbl)
(fn parse-args [args]
(match args
["-v" & rest] (assoc (parse-args rest) :verbose true)
["-t" timeout & rest] (assoc (parse-args rest) :timeout (tonumber timeout))
["-s" service & rest] (assoc (parse-args rest) :service service)
[linkname "up"] {:link linkname :expecting "up"}
[linkname "running"] {:link linkname :expecting "running"}
[linkname "present"] {:link linkname :expecting "present"}
[linkname nil] {:link linkname :expecting "present"}
_ nil))
(local parameters
(or
(parse-args arg)
(assert false (.. "Usage: " (. arg 0) " [-v] ifname [present|up|running]"))))
(fn event-matches? [params v]
(let [got
(match v
;; - up: Reflects the administrative state of the interface (IFF_UP)
;; - running: Reflects the operational state (IFF_RUNNING).
{:event "newlink" :name params.link :up :yes :running :yes}
{:present true :up true :running true}
(fn run-events [evs]
(each [_ v (ipairs evs)]
(let [got
(match v
;; - up: Reflects the administrative state of the interface (IFF_UP)
;; - running: Reflects the operational state (IFF_RUNNING).
{:event "newlink" :name parameters.link :up :yes :running :yes}
{:present true :up true :running true}
{:event "newlink" :name params.link :up :yes}
{:present :true :up true}
{:event "newlink" :name parameters.link :up :yes}
{:present :true :up true}
{:event "newlink" :name params.link}
{:present true }
{:event "newlink" :name parameters.link}
{:present true }
_
{})]
(not (not (. got params.expecting)))))
_
{})]
(when (. got parameters.expecting)
(os.exit 0)))))
(var up :unknown)
(fn toggle-service [service wanted?]
(when (not (= up wanted?))
(set up
(if wanted?
(pcall system (.. "s6-rc -b -u change " service))
(not (pcall system (.. "s6-rc -b -d change " service)))))
))
(fn run [args event-fn]
(set up :unknown)
(let [parameters
(assert (parse-args args)
(.. "Usage: ifwait [-v] ifname [present|up|running]"))]
(when parameters.verbose
(print (.. "ifwait: waiting for "
parameters.link " to be " parameters.expecting)))
(when parameters.verbose
(print (.. (. arg 0) ": waiting for "
parameters.link " to be " parameters.expecting)))
(if parameters.service
(each [e (event-fn)]
(if (= e.name parameters.link)
(toggle-service parameters.service (event-matches? parameters e))))
(each [e (event-fn)
&until (event-matches? parameters e)]
true))))
(run-events (sock:query {:link true}))
(when (not (= (. arg 0) "test"))
(run arg #(nl.events {:link true})))
(while (sock:poll) (run-events (sock:event)))
{ : run }
+117
View File
@@ -0,0 +1,117 @@
(local { : view &as fennel } (require :fennel))
(local anoia (require :anoia))
(import-macros { : expect= } :anoia.assert)
;; nix-shell --run "cd pkgs/ifwait && fennelrepl test-ifwait.fnl"
(var fake-system (fn [s] (print "executing " s)))
(tset anoia :system #(fake-system $1))
(fn event-generator [events]
(coroutine.wrap
(fn []
(each [_ e (ipairs events)] (coroutine.yield e)))))
(fn file-events [path]
(let [data (with-open [e (io.open path "r")] (e:read "*a"))
parse (fennel.parser data)]
(icollect [_ ast parse]
ast)))
(set _G.arg (doto [] (tset 0 "test")))
(local ifwait (require :ifwait))
(let [gen (event-generator (file-events "events-fixture"))]
(ifwait.run ["dummy0" "up"] #gen)
(match (pcall gen)
(true _) true
(false msg) (error "didn't detect dummy0 up event")))
(var upsies [])
(set fake-system
(fn [s]
(if (s:match "-u change addmember")
(table.insert upsies :u)
(s:match "-d change addmember")
(table.insert upsies :d))))
(fn newlink [name up running]
{:event "newlink"
:hwaddr "b6:7d:5c:38:89:1d"
:index (string.unpack ">i2" name)
:mtu 1500
: name
: running
:stamp 857161382
: up })
"when it gets events that don't match the interface, nothing happens"
(let [gen (-> [(newlink "eth1" "no" "no")] event-generator)]
(set upsies [])
(ifwait.run [ "-s" "addmember" "dummy0" "up"] #gen)
(expect= upsies []))
"when it gets an event that should start the service, the service starts"
(let [gen (->
[(newlink "dummy0" "no" "no")
(newlink "dummy0" "yes" "no")
(newlink "eth1" "no" "no")]
event-generator)]
(set upsies [])
(ifwait.run ["-s" "addmember" "dummy0" "up"] #gen)
(expect= upsies [:d :u]))
"when it gets an event that should stop the service, the service stops"
(let [gen (->
[(newlink "dummy0" "no" "no")
(newlink "dummy0" "yes" "no")
(newlink "dummy0" "no" "no")
]
event-generator)]
(set upsies [])
(ifwait.run ["-s" "addmember" "dummy0" "up"] #gen)
(expect= upsies [:d :u :d]))
"it does not call s6-rc again if the service is already in required state"
(let [gen (->
[(newlink "dummy0" "no" "no")
(newlink "dummy0" "yes" "no")
(newlink "dummy0" "yes" "yes")
(newlink "dummy0" "yes" "yes")
(newlink "dummy0" "yes" "no")
(newlink "dummy0" "no" "no")
]
event-generator)]
(set upsies [])
(ifwait.run ["-s" "addmember" "dummy0" "up"] #gen)
(expect= upsies [:d :u :d]))
"it handles an error return from s6-rc"
(set fake-system
(fn [s]
(if (s:match "-u change addmember")
(table.insert upsies :u)
(s:match "-d change addmember")
(table.insert upsies :d))
(error "false")
))
(let [gen (->
[(newlink "dummy0" "yes" "no")
(newlink "dummy0" "yes" "yes")
(newlink "dummy0" "yes" "yes")
(newlink "dummy0" "yes" "no")
(newlink "dummy0" "no" "no")
]
event-generator)]
(set upsies [])
(ifwait.run ["-s" "addmember" "dummy0" "up"] #gen)
(expect= upsies [:u :u :u :u]))
(print "OK")
+5 -3
View File
@@ -4,7 +4,7 @@
, systemconfig
, execline
, lib
, services ? null
, config ? {}
, liminix
, pseudofile
, pkgs
@@ -12,6 +12,7 @@
let
inherit (pseudofile) dir symlink;
inherit (liminix.services) oneshot;
paramConfig = config;
newRoot = "/run/maintenance";
sysconfig =
let
@@ -25,8 +26,8 @@ let
emptyenv chroot . /bin/init
'';
base = {...} : {
config = {
services = services // {
config = {
services = {
banner = oneshot {
name = "banner";
up = "cat /etc/banner > /dev/console";
@@ -60,6 +61,7 @@ let
../../modules/users.nix
../../modules/busybox.nix
base
({ ... } : paramConfig)
../../modules/s6
];
};
+1 -1
View File
@@ -11,7 +11,7 @@ test -n "$contents" && for d in $contents; do
touch $out/${name}/contents.d/$d
done
for i in run notification-fd up down consumer-for producer-for pipeline-name ; do
for i in timeout-up timeout-down run notification-fd up down finish consumer-for producer-for pipeline-name restart-on-upgrade; do
test -n "$(printenv $i)" && (echo "$(printenv $i)" > $out/${name}/$i)
done
+12 -4
View File
@@ -18,6 +18,7 @@ let
${commands}
'';
cleanupScript = name : ''
#!/bin/sh
if test -d ${prefix}/${name} ; then rm -rf ${prefix}/${name} ; fi
'';
service = {
@@ -26,20 +27,25 @@ let
, run ? null
, up ? null
, down ? null
, finish ? null
, outputs ? []
, notification-fd ? null
, producer-for ? null
, consumer-for ? null
, pipeline-name ? null
, timeout-up ? 30000 # milliseconds
, timeout-down ? 0
, dependencies ? []
, contents ? []
, buildInputs ? []
, isTrigger ? false
} @ args:
stdenvNoCC.mkDerivation {
# we use stdenvNoCC to avoid generating derivations with names
# like foo.service-mips-linux-musl
inherit name serviceType up down run notification-fd
producer-for consumer-for pipeline-name;
inherit name serviceType up down run finish notification-fd
producer-for consumer-for pipeline-name timeout-up timeout-down;
restart-on-upgrade = isTrigger;
buildInputs = buildInputs ++ dependencies ++ contents;
dependencies = builtins.map (d: d.name) dependencies;
contents = builtins.map (d: d.name) contents;
@@ -52,6 +58,7 @@ let
, outputs ? []
, notification-fd ? null
, dependencies ? []
, buildInputs ? []
, ...
} @ args:
let logger = service {
@@ -63,9 +70,10 @@ let
pipeline-name = "${name}-pipeline";
};
in service (args // {
buildInputs = [ logger ];
buildInputs = buildInputs ++ [ logger ];
serviceType = "longrun";
run = serviceScript "${run}\n${cleanupScript name}";
run = serviceScript run;
finish = cleanupScript name;
producer-for = "${name}-log";
});
+25
View File
@@ -0,0 +1,25 @@
{ lua, lib, fetchFromGitHub }:
let
pname = "lualinux";
src = fetchFromGitHub {
repo = "lualinux";
owner = "philanc";
rev = "1d4c962aad9cbe01c05df741b91e8b39c356362c";
hash = "sha256-+Ys4sERG+TI8nRzG38UP+KqbH0efspaX0j4IHCt56RI=";
};
in lua.pkgs.buildLuaPackage {
inherit pname;
version = "0.1"; # :shrug:
inherit src;
postPatch = ''
sed -i -e '/strip/d' Makefile
'';
makeFlags = [ "LUADIR=." "CC:=$(CC)" "STRIP=true" "lualinux.so" ];
installPhase = ''
mkdir -p "$out/lib/lua/${lua.luaversion}"
cp ${pname}.so "$out/lib/lua/${lua.luaversion}/"
'';
}
+23 -7
View File
@@ -1,12 +1,19 @@
#!/usr/bin/env bash
ssh_command=${SSH_COMMAND-ssh}
if [ "$1" = "--no-reboot" ] ; then
reboot="true"
shift
else
reboot="reboot"
fi
reboot="reboot"
case "$1" in
"--no-reboot")
unset reboot
shift
;;
"--fast")
reboot="soft"
shift
;;
esac
target_host=$1
shift
@@ -20,7 +27,16 @@ if toplevel=$(nix-build "$@" -A outputs.systemConfiguration --no-out-link); then
echo systemConfiguration $toplevel
min-copy-closure $target_host $toplevel
$ssh_command $target_host $toplevel/bin/install
$ssh_command $target_host "sync; source /etc/profile; reboot -f"
case "$reboot" in
reboot)
$ssh_command $target_host "sync; source /etc/profile; reboot"
;;
soft)
$ssh_command $target_host $toplevel/bin/restart-services
;;
*)
;;
esac
else
echo Rebuild failed
fi
+22
View File
@@ -0,0 +1,22 @@
{ lua, lib, fetchFromGitHub }:
let
pname = "minisock";
src = fetchFromGitHub {
repo = "minisock";
owner = "telent";
rev = "46e0470ff88c68f3a873dedbcf1dc351f4916b1a";
hash = "sha256-uTV5gpfEMvHMBgdu41Gy2uizc3K9bXtO5BiCY70cYUs=";
};
in lua.pkgs.buildLuaPackage {
inherit pname;
version = "0.1"; # :shrug:
inherit src;
makeFlags = [ "LUADIR=." "minisock.so" ];
installPhase = ''
mkdir -p "$out/lib/lua/${lua.luaversion}"
cp ${pname}.so "$out/lib/lua/${lua.luaversion}/"
'';
}
+28
View File
@@ -0,0 +1,28 @@
{ lua, lib, fetchpatch, fetchFromGitHub, stdenv }:
let pname = "nellie";
in lua.pkgs.buildLuaPackage {
inherit pname;
version = "0.1.1-1";
src = ./.;
buildPhase = "$CC -shared -l lua -o nellie.so nellie.c";
# for the checks to work you need to
# nix-build--option sandbox false
# otherwise the sandbox doesn't see any uevent messages
# doCheck = stdenv.hostPlatform == stdenv.buildPlatform;
checkPhase = ''
export LUA_CPATH=./?.so
lua test.lua
'';
installPhase = ''
mkdir -p "$out/lib/lua/${lua.luaversion}"
cp nellie.so "$out/lib/lua/${lua.luaversion}/"
'';
}
+90
View File
@@ -0,0 +1,90 @@
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include <linux/netlink.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int l_close_socket(lua_State *L) {
lua_getfield(L, 1, "fileno");
int fd = (int) lua_tointeger(L, -1);
close(fd);
return 0;
}
static int l_read_from_socket(lua_State *L) {
int length = 32;
if(lua_isnumber(L, 2))
length = lua_tointeger(L, 2);
lua_getfield(L, 1, "fileno");
int fd = (int) lua_tointeger(L, -1);
char *buf = (char *) malloc(length);
int bytes = recv(fd, buf, length, 0);
if(bytes > 0) {
lua_pushlstring(L, buf, bytes);
free(buf);
return 1;
} else {
free(buf);
return 0;
}
}
static int l_open_socket(lua_State *L) {
int netlink_fd = socket(AF_NETLINK, SOCK_RAW|SOCK_CLOEXEC, NETLINK_KOBJECT_UEVENT);
struct sockaddr_nl sa;
memset(&sa, 0, sizeof(sa));
sa.nl_family = AF_NETLINK;
sa.nl_pid = getpid();
if(lua_isnumber(L, 1)) {
sa.nl_groups = lua_tointeger(L, 1);
lua_pop(L, 1);
}
else {
sa.nl_groups = 4; /* group 4 is rebroadcasts from mdevd */
}
if(bind(netlink_fd, (struct sockaddr *) &sa, sizeof(sa))==0) {
lua_newtable(L);
lua_pushliteral(L, "fileno");
lua_pushinteger(L, netlink_fd);
lua_settable(L, 1);
lua_pushliteral(L, "read");
lua_pushcfunction(L, l_read_from_socket);
lua_settable(L, 1);
lua_pushliteral(L, "close");
lua_pushcfunction(L, l_close_socket);
lua_settable(L, 1);
return 1;
} else {
return 0;
}
}
static const struct luaL_Reg funcs [] = {
{"open", l_open_socket},
{NULL, NULL} /* sentinel */
};
/* "luaopen_" prefix is magic and tells lua to run this function
* when it dlopens the library
*/
int luaopen_nellie (lua_State *L) {
luaL_newlib(L, funcs);
return 1;
}
+6
View File
@@ -0,0 +1,6 @@
local nellie = require('nellie')
print('dfg')
local f = nellie.open(2)
print(string.byte(f:read(1000), 0, 60))
print("CLOSED", f:close())
+2 -1
View File
@@ -2,5 +2,6 @@
writeFennelScript
, anoia
, lua
, lualinux
}:
writeFennelScript "odhcpc-script" [anoia lua.pkgs.luafilesystem] ./odhcp6-script.fnl
writeFennelScript "odhcpc-script" [anoia lualinux] ./odhcp6-script.fnl
+22
View File
@@ -1,6 +1,7 @@
{
fetchFromGitHub
, writeShellScript
, pkgsBuildBuild
}:
let
src = fetchFromGitHub {
@@ -36,4 +37,25 @@ in {
applyPatches.ramips = doPatch "ramips";
applyPatches.mediatek = doPatch "mediatek"; # aarch64
applyPatches.mvebu = doPatch "mvebu"; # arm
applyPatches.rt2x00 = ''
PATH=${pkgsBuildBuild.patchutils}/bin:$PATH
for i in ${src}/package/kernel/mac80211/patches/rt2x00/6*.patch ; do
fixed=$(basename $i).fixed
sed '/depends on m/d' < $i | sed 's/CPTCFG_/CONFIG_/g' | recountdiff | filterdiff -x '*/local-symbols' > $fixed
case $fixed in
606-*)
;;
611-*)
filterdiff -x '*/rt2x00.h' < $fixed | patch --forward -p1
;;
601-*|607-*)
filterdiff -x '*/rt2x00_platform.h' < $fixed | patch --forward -p1
;;
*)
cat $fixed | patch --forward -p1
;;
esac
done
'';
}
+18 -2
View File
@@ -21,11 +21,27 @@ in stdenvNoCC.mkDerivation {
if test -d $i; then
for j in $i/* ; do
if test -f $j/type ; then
if test -e $j/restart-on-upgrade; then
flag=force-restart
else
unset flag
fi
case $(cat $j/type) in
longrun|oneshot)
# s6-rc-update only wants atomics in its
# restarts file
echo $(basename $j) " " ''${flag-$i} >> $out/hashes
;;
*)
;;
esac
srcs="$srcs $i"
fi
done
fi
done
s6-rc-compile $out/compiled $srcs
'';
}
s6-rc-db -c $out/compiled contents default
mv $out/hashes $out/compiled
'';
}
+3 -1
View File
@@ -6,6 +6,7 @@
{
writeText
, writeFennelScript
, lib
, s6-init-bin
, closureInfo
@@ -52,7 +53,7 @@ let
chown = if uid>0 || gid>0
then "\nCHOWN(${qpathname},${toString uid},${toString gid});\n"
else "";
in "${cmd} ${chown}";
in "unlink(${qpathname}); ${cmd} ${chown}";
in mapAttrsToList (makeFile prefix) attrset;
activateScript = attrset: writeText "makedevs.c" ''
#include "defs.h"
@@ -80,6 +81,7 @@ in attrset:
cp $closure/store-paths $out/etc/nix-store-paths
$STRIP --remove-section=.note --remove-section=.comment --strip-all makedevs -o $out/bin/activate
ln -s ${s6-init-bin}/bin/init $out/bin/init
cp -p ${writeFennelScript "restart-services" [] ./restart-services.fnl} $out/bin/restart-services
cat > $out/bin/install <<EOF
#!/bin/sh -e
prefix=\''${1-/}
+40
View File
@@ -0,0 +1,40 @@
(fn hashes-from-file [name]
(with-open [f (assert (io.open name :r) name)]
(accumulate [h []
l #(f:read "*l")]
(let [(name hash) (string.match l "([^%s]+) +([^%s]+)")]
(if name
(doto h (tset name hash))
h)))))
(fn write-restarts [old new]
(let [old-hashes (hashes-from-file old)
new-hashes (hashes-from-file new)]
(with-open [f (io.open "/tmp/restarts" :w)]
(each [n h (pairs old-hashes)]
(let [new (. new-hashes n)]
(when (or (= h "force-restart")
(= new "force-restart")
(not (= h new)))
(f:write (.. n " restart\n"))))))))
(fn exec [text command]
(io.write (.. text ": "))
(match (os.execute command)
res (print "[OK]")
(nil err) (error (.. "[FAILED " err "]"))))
(let [mypath (: (. arg 0) :match "(.*/)")
activate (.. mypath "activate /")
old-compiled "/run/s6-rc/compiled/"
new-compiled "/etc/s6-rc/compiled/"]
(exec "installing FHS files" activate)
(write-restarts (.. old-compiled "hashes") (.. new-compiled "hashes"))
(exec "updating service database"
(.. "s6-rc-update -f /tmp/restarts " new-compiled))
(exec "starting services" (.. "s6-rc -u -p change default"))
)
+12
View File
@@ -0,0 +1,12 @@
loopback /nix/store/3k7yp08465k8z8a1514slansj0hbrvbq-loopback
default /nix/store/5247rr0mcr17x9lkaqjiywxmn4q3ibgd-default
lan.link.a.10.3.0.1 /nix/store/99ic5bryzbx12ip4v8sh9cdrv15c8qwl-lan.link.a.10.3.0.1
lo.link.a.-1 /nix/store/a2yvj42ghcg8iyvi51zcmzmpwwkrx30l-lo.link.a.-1
sshd /nix/store/afgznmavi6s9dxkv4vnn9xwd7qz5kk35-sshd
lo.link /nix/store/dz0nvlf16lqv9yndas0sk0xqh7jvx8gm-lo.link
lo.link.a.127.0.0.1 /nix/store/jl018kbgxcnyb70s0cfw19q1zqa0pval-lo.link.a.127.0.0.1
lan.link /nix/store/lxvmhb7ax46s2akj9lsivcq494kmx0hm-lan.link
lan.link.a.10.3.0.1.dnsmasq-log /nix/store/rk8265ikd9lj3fpibyrw0kal32h0ksqg-lan.link.a.10.3.0.1.dnsmasq-log
hostname /nix/store/xfy0ymvnd0zv6wcd26h405f85hfkd8wq-hostname
sshd-log /nix/store/xj58nxvkacsjg3rvwb4bxcqnhaav4d3s-sshd-log
lan.link.a.10.3.0.1.dnsmasq /nix/store/z4l7h9pr4an9aq921cs9wrlvf9b8xr48-lan.link.a.10.3.0.1.dnsmasq
+12
View File
@@ -0,0 +1,12 @@
loopback /nix/store/3k7yp08465k8z8a1514slansj0hbrvbq-loopback
default /nix/store/5247rr0mcr17x9lkaqjiywxmn4q3ibgd-default
lan.link.a.10.3.0.1 /nix/store/99ic5bryzbx12ip4v8sh9cdrv15c8qwl-lan.link.a.10.3.0.1
lo.link.a.-1 /nix/store/a2yvj42ghcg8iyvi51zcmzmpwwkrx30l-lo.link.a.-1
sshd /nix/store/afgznmavi6s9dxkv4vnn9xwd7qz5kk35-sshd
lo.link /nax/store/dz0nvlf16lqv9yndas0sk0xqh7jvx8gm-lo.link
lo.link.a.127.0.0.1 /nix/store/jl018kbgxcnyb70s0cfw19q1zqa0pval-lo.link.a.127.0.0.1
lan.link /nix/store/lxvmhb7ax46s2akj9lsivcq494kmx0hm-lan.link
lan.link.a.10.3.0.1.dnsmasq-log /nix/store/rk8265ikd9lj3fpibyrw0kal32h0ksqg-lan.link.a.10.3.0.1.dnsmasq-log
hostname /nax/store/xfy0ymvnd0zv6wcd26h405f85hfkd8wq-hostname
sshd-log /nax/store/xj58nxvkacsjg3rvwb4bxcqnhaav4d3s-sshd-log
+27
View File
@@ -0,0 +1,27 @@
{
lua
, nellie
, lualinux
, writeFennel
, runCommand
, anoia
, fennel
, stdenv
, fennelrepl
}:
stdenv.mkDerivation {
name = "uevent-watch";
src = ./.;
nativeBuildInputs = [ fennelrepl ];
installPhase = ''
mkdir -p $out/bin
cp -p ${writeFennel "uevent-watch" {
packages = [fennel anoia nellie lualinux];
mainFunction = "run";
} ./watch.fnl} $out/bin/uevent-watch
'';
checkPhase = ''
fennelrepl ./test.fnl
'';
doCheck = true;
}
+25
View File
@@ -0,0 +1,25 @@
add@/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1
SUBSYSTEM=block
DEVNAME=/dev/sdb1
DEVTYPE=partition
DISKSEQ=33
PARTN=1
SEQNUM=82381
MAJOR=8
PARTNAME=backup-disk
MINOR=17
remove@/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1
ACTION=remove
DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1
SUBSYSTEM=block
DEVNAME=/dev/sdb1
DEVTYPE=partition
DISKSEQ=33
PARTN=1
SEQNUM=82386
PARTNAME=backup-disk
MAJOR=8
MINOR=17
+33
View File
@@ -0,0 +1,33 @@
(local watch (require :watch))
;; Events come from the devout socket as an initial summary line
;; followed by a NUL character followed by newline-separated key=value
;; pairs. For ease of editing we don't have NULs in events.txt,
;; so we need to massage it into shape here
(local events
(with-open [f (io.open "./events.txt" :r)]
(let [text (string.gsub (f:read "*a") "\n\n" "\0") ]
(icollect [n (string.gmatch text "([^\0]+)")]
(string.gsub n "\n" "\0" 1)))))
(fn next-event []
(var i 0)
(fn []
(let [i_ (+ 1 i)
e (. events i_)]
(set i i_)
e)))
;; this tests event parsing but not whether anything
;; happens as a result of processing them
(watch.run-with-fh
{
:read (next-event)
:write (fn [payload] )
}
["-s" "foo" "-n" (os.getenv "TMPDIR") "partname=backup-disk" ]
)
(print "OK")
+77
View File
@@ -0,0 +1,77 @@
(local { : assoc : system : dirname } (require :anoia))
(local { : mktree : rmtree : symlink } (require :anoia.fs))
(local { : AF_LOCAL : SOCK_STREAM } (require :anoia.net.constants))
(local ll (require :lualinux))
(local { : view } (require :fennel))
(fn parse-args [args]
(match args
["-s" service & rest] (assoc (parse-args rest) :service service)
["-n" path & rest] (assoc (parse-args rest) :linkname path)
matches { :matches (table.concat matches " ") }
_ nil))
(fn %% [fmt ...] (string.format fmt ...))
(var up :unknown)
(fn start-service [devname linkname service]
(match (symlink (.. "/dev/" devname ) linkname)
ok (pcall system (%% "s6-rc -b -u change %q" service))
(nil err) false))
(fn stop-service [linkname service]
(match (pcall system (%% "s6-rc -b -d change %q" linkname service))
ok (os.remove linkname)
(nil err) false))
(fn toggle-service [devname linkname service wanted?]
(when (not (= up wanted?))
(set up
(if wanted?
(start-service devname linkname service)
(not (stop-service linkname service))))))
(fn parse-uevent [s]
(when s
(let [(nl nxt) (string.find s "\0" 1 true)]
(collect [k v (string.gmatch
(string.sub s (+ 1 nxt))
"(%g-)=(%g+)")]
(k:lower) v))))
(fn run-with-fh [fh args]
(set up :unknown)
(let [parameters
(assert (parse-args args) (.. "can't parse args: " (table.concat args " ")))]
(mktree (dirname parameters.linkname))
(var finished? false)
(print "registering for events" (fh:write parameters.matches))
(while (not finished?)
(let [e (parse-uevent (fh:read))]
(when e
(let [wanted? (. {:add true :change true} e.action)]
(toggle-service e.devname parameters.linkname parameters.service wanted?)))
(set finished? (= e nil))
))))
(fn unix-connect [pathname]
(let [addr (string.pack "=Hz" AF_LOCAL pathname)]
(match (ll.socket AF_LOCAL SOCK_STREAM 0)
sock (doto sock (ll.connect addr))
(nil err) (error err))))
(fn run [args]
(let [fd (assert (unix-connect "/run/devout.sock"))
devout {
:read #(ll.read fd)
:write #(ll.write fd $2)
:close #(ll.close fd)
}]
(run-with-fh devout arg)))
{ : run : run-with-fh }
+39
View File
@@ -0,0 +1,39 @@
# usb modeswitch without udev, tcl, coreutils, bash dependencies
{ stdenv
, lib
, fetchurl
, pkg-config
, libusb1
}:
let
pname = "usb-modeswitch";
version = "2.6.0";
in stdenv.mkDerivation {
inherit pname version;
src = fetchurl {
url = "http://www.draisberghof.de/usb_modeswitch/${pname}-${version}.tar.bz2";
sha256 = "18wbbxc5cfsmikba0msdvd5qlaga27b32nhrzicyd9mdddp265f2";
};
preBuild = ''
makeFlagsArray+=(LIBS="$($PKG_CONFIG --libs --cflags libusb-1.0)")
'';
makeFlags = [
"PREFIX=$(out)"
"usb_modeswitch"
];
buildInputs = [ libusb1 ];
nativeBuildInputs = [ pkg-config ];
installPhase = ''
mkdir -p $out/bin
cp usb_modeswitch $out/bin
'';
meta = {
license = lib.licenses.gpl2;
maintainers = [];
};
}
+2
View File
@@ -8,4 +8,6 @@
min-copy-closure = import ./min-copy-closure/test.nix;
fennel = import ./fennel/test.nix;
tftpboot = import ./tftpboot/test.nix;
updown = import ./updown/test.nix;
inout = import ./inout/test.nix;
}
+55
View File
@@ -0,0 +1,55 @@
{ config, pkgs, lib, modulesPath, ... } :
let
inherit (pkgs.liminix.services) bundle oneshot longrun;
inherit (pkgs.pseudofile) dir symlink;
inherit (pkgs) serviceFns;
svc = config.system.service;
in rec {
imports = [
"${modulesPath}/dhcp6c"
"${modulesPath}/dnsmasq"
"${modulesPath}/firewall"
"${modulesPath}/hostapd"
"${modulesPath}/network"
"${modulesPath}/ssh"
"${modulesPath}/mount"
"${modulesPath}/mdevd.nix"
];
filesystem = dir { srv = dir {}; };
kernel = {
config = {
USB = "y";
USB_EHCI_HCD = "y";
USB_EHCI_HCD_PLATFORM = "y";
USB_OHCI_HCD = "y";
USB_OHCI_HCD_PLATFORM = "y";
USB_SUPPORT = "y";
USB_COMMON = "y";
USB_STORAGE = "y";
USB_STORAGE_DEBUG = "n";
USB_UAS = "y";
USB_ANNOUNCE_NEW_DEVICES = "y";
SCSI = "y";
BLK_DEV_SD = "y";
USB_PRINTER = "y";
MSDOS_PARTITION = "y";
EFI_PARTITION = "y";
EXT4_FS = "y";
EXT4_USE_FOR_EXT2 = "y";
FS_ENCRYPTION = "y";
};
};
rootfsType = "jffs2";
hostname = "inout";
services.mount_backup_disk = svc.mount.build {
partlabel = "backup-disk";
mountpoint = "/srv";
fstype = "ext4";
};
}
+55
View File
@@ -0,0 +1,55 @@
set timeout 10
set when [lindex $argv 0];
spawn socat -,echo=0,icanon=1 unix-connect:vm/monitor
set monitor_id $spawn_id
spawn socat unix-connect:vm/console -
set console_id $spawn_id
proc chat {instr outstr} {
expect {
$instr { send $outstr }
timeout { exit 1 }
}
}
proc adddevice { } {
global monitor_id console_id spawn_id
set spawn_id $monitor_id
chat "QEMU" "device_add usb-storage,bus=xhci.0,drive=usbstick\n"
chat "(qemu)" "version\r"
set spawn_id $console_id
expect {
"sda: sda1" { }
timeout { exit 1 }
}
}
if { $when eq "early" } {
adddevice
}
expect "BusyBox"
chat "#" "PS1=RE\\ADY_\\ ; stty -echo \r"
chat "READY_" "tail -f /run/uncaught-logs/current & \rs6-rc -b -a list\r"
chat "mount" "\r"
if { $when eq "late" } {
adddevice
}
send "\r"
set timeout 20
chat "READY_" "sleep 3; grep /srv /proc/mounts && hostname\r"
expect {
"inout" { }
timeout { exit 1 }
}
+37
View File
@@ -0,0 +1,37 @@
{
liminix
, nixpkgs
}:
let img = (import liminix {
device = import "${liminix}/devices/qemu/";
liminix-config = ./configuration.nix;
}).outputs.vmroot;
pkgs = import <nixpkgs> { overlays = [(import ../../overlay.nix)]; };
in pkgs.runCommand "check" {
nativeBuildInputs = with pkgs; [
expect
socat
e2fsprogs
util-linux # for sfdisk, fallocate
parted
] ;
} ''
mkdir vm
dd if=/dev/zero of=./vm/stick.e2fs bs=1M count=32
mkfs.ext2 -L backup-disk ./vm/stick.e2fs
dd if=/dev/zero of=./vm/stick.img bs=1M count=38
dd if=./vm/stick.e2fs of=./vm/stick.img bs=512 seek=34 conv=notrunc
parted -s ./vm/stick.img -- mklabel gpt mkpart backup-disk ext2 34s -0M
{
${img}/run.sh --background ./vm --flag -device --flag usb-ehci,id=xhci --flag -drive --flag if=none,id=usbstick,format=raw,file=$(pwd)/vm/stick.img
expect ${./script.expect} late
kill $(cat ./vm/pid)
${img}/run.sh --background ./vm --flag -device --flag usb-ehci,id=xhci --flag -drive --flag if=none,id=usbstick,format=raw,file=$(pwd)/vm/stick.img
expect ${./script.expect} early
} | tee $out
''
-4
View File
@@ -32,9 +32,5 @@ in {
};
rootfsType = "jffs2";
services.default = lib.mkForce (target {
name = "default";
contents = with config.services; [ loopback ntp defaultroute4 sshd dhcpv4 ];
});
};
}
+22 -7
View File
@@ -1,14 +1,26 @@
#!/usr/bin/env nix-shell
#! nix-shell -v -i bash -p expect socat
# This is a test for liminix-rebuild. It's not a CI test because
# liminix-rebuild calls nix-build so won't run inside a derivation,
# meaning you have to remember to run it manually when changing
# liminix-rebuild
# nix-shell -p expect socat --run "sh ./tests/min-copy-closure/test-liminix-rebuild.sh "
. tests/test-helpers.sh
set -e
while test -n "$1"; do
case $1 in
--fast)
FAST=true
;;
*)
;;
esac
shift
done
here=$(pwd)/tests/min-copy-closure
top=$(pwd)
@@ -31,7 +43,7 @@ mkdir ./vm
cat ${rootfs} > rootfs
truncate -s 24M rootfs
truncate -s 32M rootfs
resize2fs rootfs
dd if=rootfs of=disk-image bs=512 seek=4 conv=sync
@@ -50,9 +62,12 @@ echo "READY"
touch known_hosts
export SSH_COMMAND="ssh -o UserKnownHostsFile=${work}/known_hosts -o StrictHostKeyChecking=no -p 2022 -i ${here}/id"
(cd ${top} && liminix-rebuild root@localhost -I liminix-config=${here}/with-figlet.nix --arg device "import ./devices/qemu-armv7l")
if test -n "$FAST"; then
(cd ${top} && liminix-rebuild --fast root@localhost -I liminix-config=${here}/with-figlet.nix --argstr deviceName qemu-armv7l)
cd ${work} && expect $here/wait-for-soft-restart.expect
else
(cd ${top} && liminix-rebuild root@localhost -I liminix-config=${here}/with-figlet.nix --arg device "import ./devices/qemu-armv7l")
cd ${work} && expect $here/wait-for-reboot.expect
fi
ls -l vm
cd ${work} && expect $here/wait-for-reboot.expect
cd / ; rm -rf $work
@@ -0,0 +1,13 @@
proc chat {instr outstr} {
expect {
$instr { send $outstr }
timeout { exit 1 }
}
}
spawn socat unix-connect:vm/console -
send "exit\r"
chat "BusyBox" "\r"
chat "#" "stty -echo; type -p figlet\r"
chat "figlet-armv7l-unknown-linux" "s6-rc -a list |grep w\\inkle\r"
chat "winkle" "poweroff\r"
@@ -5,4 +5,9 @@ send "\r\n"
expect {
"# " { send "hostname\r\n" };
}
expect "(none)"
expect {
"(none)" {}
"liminix" {}
timeout { exit(1) }
}
+7
View File
@@ -4,4 +4,11 @@
defaultProfile.packages = with pkgs; [
figlet
];
services.ripvanwinkle = pkgs.liminix.services.longrun {
name = "winkle";
run = ''
echo SLEEPING > /dev/console
sleep 3600
'';
};
}
+55
View File
@@ -0,0 +1,55 @@
{ config, pkgs, lib, ... } :
let
inherit (pkgs.liminix.services) bundle oneshot longrun;
inherit (pkgs) serviceFns;
# EDIT: you can pick your preferred RFC1918 address space
# for NATted connections, if you don't like this one.
ipv4LocalNet = "10.8.0";
svc = config.system.service;
in rec {
imports = [
../../modules/bridge
../../modules/dhcp6c
../../modules/dnsmasq
../../modules/firewall
../../modules/hostapd
../../modules/network
../../modules/ssh
../../modules/vlan
../../modules/wlan.nix
];
rootfsType = "jffs2";
hostname = "updown";
services.int = svc.network.address.build {
interface = svc.bridge.primary.build { ifname = "int"; };
family = "inet"; address = "${ipv4LocalNet}.1"; prefixLength = 16;
};
services.bridge = svc.bridge.members.build {
primary = services.int;
members = with config.hardware.networkInterfaces;
[ lan ];
};
services.sshd = svc.ssh.build { };
# users.root = {
# # EDIT: choose a root password and then use
# # "mkpasswd -m sha512crypt" to determine the hash.
# # It should start wirh $6$.
# passwd = "$6$6HG7WALLQQY1LQDE$428cnouMJ7wVmyK9.dF1uWs7t0z9ztgp3MHvN5bbeo0M4Kqg/u2ThjoSHIjCEJQlnVpDOaEKcOjXAlIClHWN21";
# openssh.authorizedKeys.keys = [
# # EDIT: you can add your ssh pubkey here
# # "ssh-rsa AAAAB3NzaC1....H6hKd user@example.com";
# ];
# };
defaultProfile.packages = with pkgs; [
min-collect-garbage
# strace
# ethtool
tcpdump
];
}
+64
View File
@@ -0,0 +1,64 @@
set timeout 10
spawn socat unix-connect:vm/monitor -
set monitor_id $spawn_id
expect "(qemu)"
send "set_link virtio-net-pci.1 off\n"
expect "(qemu)"
send "set_link virtio-net-pci.0 off\n"
expect "(qemu)"
send "c\r\n"
spawn socat unix-connect:vm/console -
set console_id $spawn_id
expect "BusyBox"
expect "#" { send "PS1=RE\\ADY_\\ ; stty -echo \r" }
expect "READY_" { send "s6-rc -b -a list\r" } ; # -b waits for s6-rc lock
expect "READY_" { send "ls /sys/class/net/lan/master\r" }
expect {
"No such file or directory" { }
timeout { exit 1 }
}
expect "READY_" { send "cat /sys/class/net/lan/operstate\r" }
expect {
"down" { }
"up" { exit 1 }
}
expect "READY_" { send "s6-rc -a -u change\r" }
expect {
"unable to take locks" { exit 1 }
"READY_" { send "\r" }
}
set spawn_id $monitor_id
send "\r"
expect "(qemu)"
send "set_link virtio-net-pci.1 on\n"
expect "(qemu)"
send "set_link virtio-net-pci.0 on\n"
expect "(qemu)"
set spawn_id $console_id
expect "entered forwarding state"
send "\r"
expect "READY_" { send "cat /sys/class/net/lan/operstate\r" }
expect {
"down" { exit 1 }
"up" { }
}
expect "READY_" { send "cat /sys/class/net/lan/master/uevent\r" }
expect {
"INTERFACE=int" { }
timeout { exit 1 }
}
expect "READY_" { send "s6-rc listall int.link.a.10.8.0.1.member.lan.link ; hostname\r" }
expect {
"updown" {}
timeout { exit 1 }
}
+19
View File
@@ -0,0 +1,19 @@
{
liminix
, nixpkgs
}:
let img = (import liminix {
device = import "${liminix}/devices/qemu/";
liminix-config = ./configuration.nix;
}).outputs.vmroot;
pkgs = import <nixpkgs> { overlays = [(import ../../overlay.nix)]; };
in pkgs.runCommand "check" {
nativeBuildInputs = with pkgs; [
expect
socat
] ;
} ''
mkdir vm
${img}/run.sh --flag -S --background ./vm
expect ${./script.expect} | tee $out
''
+1 -1
View File
@@ -16,7 +16,7 @@ in rec {
in svc.network.dhcp.client.build { interface = iface; };
services.defaultroute4 = svc.network.route.build {
via = "$(output ${services.dhcpv4} address)";
via = "$(output ${services.dhcpv4} ip)";
target = "default";
dependencies = [ services.dhcpv4 ];
};