Compare commits
47 Commits
6ae0267df2
...
capabiliti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8fc58bd9f | ||
|
|
ba6a8936f4 | ||
|
|
0daf1b1ae4 | ||
|
|
8320e69ccc | ||
|
|
0a82ebcb52 | ||
|
|
715673f4b5 | ||
|
|
2848fa1a69 | ||
|
|
ab66901132 | ||
|
|
0bb33e2ae8 | ||
|
|
6ae03bee7a | ||
|
|
4fab9e7759 | ||
|
|
cfba731eae | ||
|
|
bd1faa7647 | ||
|
|
3def6ed054 | ||
|
|
8b448cc312 | ||
|
|
df53492c98 | ||
|
|
f5ddd27c7e | ||
|
|
23ac917f5b | ||
|
|
485405edac | ||
|
|
45b2b940f6 | ||
|
|
67382a6dbe | ||
|
|
b513356080 | ||
|
|
cbfacd97a6 | ||
|
|
1921f918ee | ||
|
|
9e3ab11076 | ||
|
|
9c9a33515b | ||
|
|
52cab9f145 | ||
|
|
dc8c88b368 | ||
|
|
91c487c959 | ||
|
|
d1929f390f | ||
|
|
2c805ed001 | ||
|
|
d588a72286 | ||
|
|
50ceea79c7 | ||
|
|
d57f04bf53 | ||
|
|
4950fd73f6 | ||
|
|
57f559232c | ||
|
|
2f0aac76a0 | ||
|
|
9a9519186b | ||
|
|
5675dbb2eb | ||
|
|
ab29b81133 | ||
|
|
19d09ffb32 | ||
|
|
8143336564 | ||
|
|
2979dc9b26 | ||
|
|
ab76ef531d | ||
|
|
2e390edb12 | ||
|
|
d145a5b689 | ||
|
|
a0558bc6a0 |
23
.github/workflows/push.yml
vendored
23
.github/workflows/push.yml
vendored
@@ -7,8 +7,14 @@ on:
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: build-and-test
|
||||
runs-on: ubuntu-latest
|
||||
name: CI
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- macos-latest
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
@@ -21,14 +27,23 @@ jobs:
|
||||
id: go
|
||||
|
||||
- name: Ensure all files were formatted as per gofmt
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
[ "$(gofmt -l $(find . -name '*.go') 2>&1)" = "" ]
|
||||
|
||||
- name: Build and run tests
|
||||
- name: Build
|
||||
run: |
|
||||
go install -mod=mod ./cmd/...
|
||||
|
||||
- name: install qemu
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: sudo apt update && sudo apt-get install qemu-system-x86
|
||||
|
||||
- name: Run tests
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
# TestRelativeParentDir verifies breakglass.authorized_keys
|
||||
# is correctly included, and the gok CLI only creates that
|
||||
# file when it finds SSH keys.
|
||||
run: |
|
||||
mkdir -p ~/.ssh && echo dummy > ~/.ssh/id_ed25519.pub
|
||||
go install -mod=mod ./cmd/...
|
||||
go test -mod=mod -v ./...
|
||||
|
||||
78
flake.lock
generated
Normal file
78
flake.lock
generated
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1763049705,
|
||||
"narHash": "sha256-A5LS0AJZ1yDPTa2fHxufZN++n8MCmtgrJDtxFxrH4S8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3acb677ea67d4c6218f33de0db0955f116b7588c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-25.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rpi-eeprom-tools": "rpi-eeprom-tools"
|
||||
}
|
||||
},
|
||||
"rpi-eeprom-tools": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1758789610,
|
||||
"narHash": "sha256-1aGazeN4Ou5GRMCRETie4I0E08mlAQFWp6cGURB1NIM=",
|
||||
"owner": "info-beamer",
|
||||
"repo": "rpi-eeprom-tools",
|
||||
"rev": "9ae4d618a0a51789fbde8e9d667881a321cf6721",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "info-beamer",
|
||||
"repo": "rpi-eeprom-tools",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
66
flake.nix
Normal file
66
flake.nix
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
rpi-eeprom-tools = {
|
||||
url = "github:info-beamer/rpi-eeprom-tools";
|
||||
flake = false;
|
||||
};
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
rpi-eeprom-tools,
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
|
||||
# Python environment with pycryptodome for pi-eeprom-tools
|
||||
pythonEnv = pkgs.python312.withPackages (
|
||||
ps: with ps; [
|
||||
pycryptodome
|
||||
]
|
||||
);
|
||||
|
||||
# Helper to create a wrapper for a pi-eeprom-* script
|
||||
mkPiEepromTool =
|
||||
name:
|
||||
pkgs.writeShellApplication {
|
||||
inherit name;
|
||||
runtimeInputs = [
|
||||
pythonEnv
|
||||
];
|
||||
text = ''
|
||||
exec ${pythonEnv.interpreter} ${rpi-eeprom-tools}/${name} "$@"
|
||||
'';
|
||||
};
|
||||
|
||||
# All the pi-eeprom tools
|
||||
pi-eeprom-extract = mkPiEepromTool "pi-eeprom-extract";
|
||||
pi-eeprom-ls = mkPiEepromTool "pi-eeprom-ls";
|
||||
pi-eeprom-recompress = mkPiEepromTool "pi-eeprom-recompress";
|
||||
pi-eeprom-update = mkPiEepromTool "pi-eeprom-update";
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
go
|
||||
raspberrypi-eeprom
|
||||
pi-eeprom-extract
|
||||
pi-eeprom-ls
|
||||
pi-eeprom-recompress
|
||||
pi-eeprom-update
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
echo "Nix-based gokrazy/tools dev env (go: $(go version))"
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
25
go.mod
25
go.mod
@@ -1,24 +1,27 @@
|
||||
module github.com/gokrazy/tools
|
||||
|
||||
go 1.24
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.6
|
||||
|
||||
require (
|
||||
github.com/breml/rootcerts v0.2.20
|
||||
github.com/anatol/vmtest v0.0.0-20250627153117-302402d269a6
|
||||
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0
|
||||
github.com/gokrazy/gokapi v0.0.0-20250222080418-e140e9c461d8
|
||||
github.com/gokrazy/internal v0.0.0-20250520205945-c2e4e2b4f611
|
||||
github.com/gokrazy/updater v0.0.0-20230215172637-813ccc7f21e2
|
||||
github.com/gokrazy/gokapi v0.0.0-20251205165548-0927bab199d4
|
||||
github.com/gokrazy/internal v0.0.0-20251208203110-3c1aa9087c82
|
||||
github.com/gokrazy/updater v0.0.0-20250705135802-db129c40879c
|
||||
github.com/google/renameio/v2 v2.0.0
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
golang.org/x/mod v0.23.0
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/sys v0.28.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.6
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250911151450-96dc232fbd79
|
||||
golang.org/x/mod v0.24.0
|
||||
golang.org/x/sync v0.14.0
|
||||
golang.org/x/sys v0.33.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/antihax/optional v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
)
|
||||
|
||||
52
go.sum
52
go.sum
@@ -1,38 +1,46 @@
|
||||
github.com/anatol/vmtest v0.0.0-20250627153117-302402d269a6 h1:zdaWj/ncXyzpPH3YqACvJXMrJxkkILrnWbjHojHBctc=
|
||||
github.com/anatol/vmtest v0.0.0-20250627153117-302402d269a6/go.mod h1:m5pN88x7ZnEDGXZldwg7RCX+EikR9qz/iSI2GzXq++Y=
|
||||
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/breml/rootcerts v0.2.20 h1:koth1lShwiiDp3VOX6/4qKEZ87S7HgDKsnDr47XEIq0=
|
||||
github.com/breml/rootcerts v0.2.20/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0 h1:C7t6eeMaEQVy6e8CarIhscYQlNmw5e3G36y7l7Y21Ao=
|
||||
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0/go.mod h1:56wL82FO0bfMU5RvfXoIwSOP2ggqqxT+tAfNEIyxuHw=
|
||||
github.com/gokrazy/gokapi v0.0.0-20250222080418-e140e9c461d8 h1:BvyzTtbpz1GCGD35Z3G/ZR0nK0j3Fh+dRCCso+w3RKE=
|
||||
github.com/gokrazy/gokapi v0.0.0-20250222080418-e140e9c461d8/go.mod h1:rVItujrJo0NpYZhFR5dYdzLDqMoMCtjEZkdxoCRDo+o=
|
||||
github.com/gokrazy/internal v0.0.0-20250520205945-c2e4e2b4f611 h1:BcmhkIKeIsw5xGheRGOCj97zjevG+VImWiP2/XGF2Gg=
|
||||
github.com/gokrazy/internal v0.0.0-20250520205945-c2e4e2b4f611/go.mod h1:dQY4EMkD4L5ZjYJ0SPtpgYbV7MIUMCxNIXiOfnZ6jP4=
|
||||
github.com/gokrazy/updater v0.0.0-20230215172637-813ccc7f21e2 h1:kBY5R1tSf+EYZ+QaSrofLaVJtBqYsVNVBWkdMq3Smcg=
|
||||
github.com/gokrazy/updater v0.0.0-20230215172637-813ccc7f21e2/go.mod h1:PYOvzGOL4nlBmuxu7IyKQTFLaxr61+WPRNRzVtuYOHw=
|
||||
github.com/gokrazy/gokapi v0.0.0-20251205165548-0927bab199d4 h1:XFo3EqnHUbmAySp7zqms8ee/tU8bM9k+YzT7L4o5CcQ=
|
||||
github.com/gokrazy/gokapi v0.0.0-20251205165548-0927bab199d4/go.mod h1:+StofDb/2cMb7vbA2znaNolgp9SadTYeyRIFtdhH1KQ=
|
||||
github.com/gokrazy/internal v0.0.0-20251208203110-3c1aa9087c82 h1:4ghNfD9NaZLpFrqQiBF6mPVFeMYXJSky38ubVA4ic2E=
|
||||
github.com/gokrazy/internal v0.0.0-20251208203110-3c1aa9087c82/go.mod h1:dQY4EMkD4L5ZjYJ0SPtpgYbV7MIUMCxNIXiOfnZ6jP4=
|
||||
github.com/gokrazy/updater v0.0.0-20250705135802-db129c40879c h1:j4/v9FR/cOy6nog5rmXUtauBsOU3mm+rTPn5IENUbmg=
|
||||
github.com/gokrazy/updater v0.0.0-20250705135802-db129c40879c/go.mod h1:EtAn+BPibqnAHnYGj3FW5e284xNsiOOMOL2dJiwu7H4=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
|
||||
github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250911151450-96dc232fbd79 h1:WZWglxfb13JCTbJyKY1pk0V94spHxJzMAQW29INytRQ=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250911151450-96dc232fbd79/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -17,7 +17,7 @@ type Context struct {
|
||||
}
|
||||
|
||||
func (c Context) Execute(ctx context.Context) error {
|
||||
root := gok.RootCmd
|
||||
root := gok.RootCmd()
|
||||
if r := c.Stdin; r != nil {
|
||||
root.SetIn(r)
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ func TestGokRun(t *testing.T) {
|
||||
|
||||
// Testing the root command because individual cobra commands cannot be
|
||||
// executed directly.
|
||||
root := gok.RootCmd
|
||||
root := gok.RootCmd()
|
||||
root.SetContext(ctx)
|
||||
logOutputFound := make(chan bool)
|
||||
rd, wr := io.Pipe()
|
||||
262
integration/gokupdate/gokupdate_test.go
Normal file
262
integration/gokupdate/gokupdate_test.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package gokupdate_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gokrazy/internal/config"
|
||||
"github.com/gokrazy/internal/tlsflag"
|
||||
"github.com/gokrazy/tools/gok"
|
||||
"github.com/gokrazy/tools/internal/packer"
|
||||
)
|
||||
|
||||
type gokrazyTestInstance struct {
|
||||
name string
|
||||
configDir string
|
||||
}
|
||||
|
||||
func (inst *gokrazyTestInstance) configPath() string {
|
||||
return "gokrazy/" + inst.name + "/config.json"
|
||||
}
|
||||
|
||||
func (inst *gokrazyTestInstance) readConfig(t *testing.T) config.Struct {
|
||||
b, err := os.ReadFile(inst.configPath())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cfg config.Struct
|
||||
if err := json.Unmarshal(b, &cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (inst *gokrazyTestInstance) writeConfig(t *testing.T, cfg config.Struct) {
|
||||
t.Helper()
|
||||
t.Logf("Writing updated cfg = %+v", cfg)
|
||||
b, err := cfg.FormatForFile()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(inst.configPath(), b, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeGokrazyInstance(t *testing.T, name string) *gokrazyTestInstance {
|
||||
t.Helper()
|
||||
|
||||
// Redirect os.UserConfigDir() to a temporary directory under our
|
||||
// control. gokrazy always uses a path under os.UserConfigDir().
|
||||
var configDir string
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
configHomeDir := t.TempDir()
|
||||
os.Setenv("XDG_CONFIG_HOME", configHomeDir)
|
||||
// where linux looks:
|
||||
configDir = filepath.Join(configHomeDir, "gokrazy")
|
||||
|
||||
case "darwin":
|
||||
homeDir := t.TempDir()
|
||||
os.Setenv("HOME", homeDir)
|
||||
// where darwin looks:
|
||||
configDir = filepath.Join(homeDir, "Library", "Application Support", "gokrazy")
|
||||
|
||||
default:
|
||||
t.Fatalf("GOOS=%s unsupported", runtime.GOOS)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return &gokrazyTestInstance{
|
||||
name: name,
|
||||
configDir: configDir,
|
||||
}
|
||||
}
|
||||
|
||||
func TestGokUpdate(t *testing.T) {
|
||||
// Run this whole test in a throw-away temporary directory to not litter the
|
||||
// gokrazy/tools repository working copy.
|
||||
t.Chdir(t.TempDir())
|
||||
|
||||
// create a new instance
|
||||
const (
|
||||
instanceName = "hello"
|
||||
hostname = "localhost"
|
||||
)
|
||||
ti := writeGokrazyInstance(t, instanceName)
|
||||
|
||||
c := gok.Context{
|
||||
Args: []string{
|
||||
"--parent_dir", "gokrazy",
|
||||
"-i", instanceName,
|
||||
"new",
|
||||
},
|
||||
}
|
||||
t.Logf("running %q", append([]string{"<gok>"}, c.Args...))
|
||||
if err := c.Execute(context.Background()); err != nil {
|
||||
t.Fatalf("%v: %v", c.Args, err)
|
||||
}
|
||||
|
||||
// and update the (default) instance config for our test
|
||||
{
|
||||
cfg := ti.readConfig(t)
|
||||
|
||||
// use generic kernel, enable serial console
|
||||
// TODO: use arm64 kernel when running on arm64
|
||||
kernelPackage := "github.com/gokrazy/kernel.amd64"
|
||||
cfg.KernelPackage = &kernelPackage
|
||||
cfg.FirmwarePackage = &kernelPackage
|
||||
cfg.SerialConsole = "ttyS0,115200"
|
||||
cfg.Environment = []string{"GOOS=linux", "GOARCH=amd64"}
|
||||
|
||||
cfg.Hostname = hostname
|
||||
cfg.Update.Hostname = hostname
|
||||
cfg.Update.HTTPPort = "9080"
|
||||
cfg.Update.HTTPSPort = "9443"
|
||||
t.Logf("Updated cfg.Update = %+v", cfg.Update)
|
||||
|
||||
ti.writeConfig(t, cfg)
|
||||
}
|
||||
|
||||
t.Logf("booting gokrazy instance in a VM")
|
||||
qemu := Run(t, nil)
|
||||
defer qemu.Kill()
|
||||
// TODO: kill the test if this qemu process dies for any reason
|
||||
// test by setting an aggressive QemuOptions.Timeout
|
||||
|
||||
// wait for this instance to become healthy
|
||||
//
|
||||
// TODO: include the actual build timestamp once gok overwrite returns it.
|
||||
if err := qemu.ConsoleExpect("gokrazy build timestamp "); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("gokrazy VM booted up, waiting for network reachability")
|
||||
// poll for reachability over the network
|
||||
for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(1 * time.Second) {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 1*time.Second)
|
||||
defer cancel()
|
||||
req, err := http.NewRequest("GET", "http://localhost:9080", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Logf("VM not yet reachable: %v", err)
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
t.Logf("gokrazy VM became reachable over the network")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: make 'gok update' not change directory?
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// verify overwrite works (i.e. locates extrafiles)
|
||||
fakeBuildTimestamp := "fake-update-1"
|
||||
ctx := context.WithValue(context.Background(), packer.BuildTimestampOverride, fakeBuildTimestamp)
|
||||
c = gok.Context{
|
||||
Args: []string{
|
||||
"--parent_dir", "gokrazy",
|
||||
"-i", instanceName,
|
||||
"update",
|
||||
},
|
||||
}
|
||||
t.Logf("running %q", append([]string{"<gok>"}, c.Args...))
|
||||
if err := c.Execute(ctx); err != nil {
|
||||
t.Fatalf("%v: %v", c.Args, err)
|
||||
}
|
||||
|
||||
if err := os.Chdir(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// change to use self-signed TLS certificates
|
||||
t.Logf("Setting Update.UseTLS = self-signed")
|
||||
|
||||
{
|
||||
cfg := ti.readConfig(t)
|
||||
|
||||
cfg.Update.UseTLS = "self-signed"
|
||||
t.Logf("Updated cfg.Update = %+v", cfg.Update)
|
||||
|
||||
ti.writeConfig(t, cfg)
|
||||
}
|
||||
|
||||
fakeBuildTimestamp = "fake-update-2"
|
||||
ctx = context.WithValue(context.Background(), packer.BuildTimestampOverride, fakeBuildTimestamp)
|
||||
c = gok.Context{
|
||||
Args: []string{
|
||||
"--parent_dir", "gokrazy",
|
||||
"-i", instanceName,
|
||||
"update",
|
||||
"--insecure", // only on first update after enabling self-signed TLS
|
||||
},
|
||||
}
|
||||
t.Logf("running %q", append([]string{"<gok>"}, c.Args...))
|
||||
if err := c.Execute(ctx); err != nil {
|
||||
t.Fatalf("%v: %v", c.Args, err)
|
||||
}
|
||||
if err := os.Chdir(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("first update succeeded, doing another update without --insecure")
|
||||
fakeBuildTimestamp = "fake-update-3"
|
||||
ctx = context.WithValue(context.Background(), packer.BuildTimestampOverride, fakeBuildTimestamp)
|
||||
c = gok.Context{
|
||||
Args: []string{
|
||||
"--parent_dir", "gokrazy",
|
||||
"-i", instanceName,
|
||||
"update",
|
||||
},
|
||||
}
|
||||
t.Logf("running %q", append([]string{"<gok>"}, c.Args...))
|
||||
if err := c.Execute(ctx); err != nil {
|
||||
t.Fatalf("%v: %v", c.Args, err)
|
||||
}
|
||||
if err := os.Chdir(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("second update succeeded, doing another update after deleting the certificates (with --insecure)")
|
||||
certPath, keyPath, err := tlsflag.CertificatePathsFor("self-signed", hostname)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Remove(certPath); err != nil {
|
||||
t.Fatalf("deleting certificate: %v", err)
|
||||
}
|
||||
if err := os.Remove(keyPath); err != nil {
|
||||
t.Fatalf("deleting certificate: %v", err)
|
||||
}
|
||||
fakeBuildTimestamp = "fake-update-4"
|
||||
ctx = context.WithValue(context.Background(), packer.BuildTimestampOverride, fakeBuildTimestamp)
|
||||
c = gok.Context{
|
||||
Args: []string{
|
||||
"--parent_dir", "gokrazy",
|
||||
"-i", instanceName,
|
||||
"update",
|
||||
"--insecure", // because we deleted the certificate files
|
||||
},
|
||||
}
|
||||
t.Logf("running %q", append([]string{"<gok>"}, c.Args...))
|
||||
if err := c.Execute(ctx); err != nil {
|
||||
t.Fatalf("%v: %v", c.Args, err)
|
||||
}
|
||||
|
||||
}
|
||||
97
integration/gokupdate/integration_test.go
Normal file
97
integration/gokupdate/integration_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package gokupdate_test
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/anatol/vmtest"
|
||||
)
|
||||
|
||||
func effectiveGOARCH() string {
|
||||
goarch := os.Getenv("GOARCH")
|
||||
if goarch != "" {
|
||||
return goarch
|
||||
}
|
||||
return runtime.GOARCH
|
||||
}
|
||||
|
||||
// TODO: move to a gokrazy/internal/integrationtest package
|
||||
func Run(t *testing.T, qemuArgs []string) *vmtest.Qemu {
|
||||
tempdir := t.TempDir()
|
||||
diskImage := filepath.Join(tempdir, "gokrazy.img")
|
||||
// diskImage := "/tmp/gokrazy.img" // for debugging
|
||||
|
||||
// TODO: use in-process gok overwrite
|
||||
packer := exec.Command("gok",
|
||||
"overwrite",
|
||||
"--instance=hello",
|
||||
"--parent_dir=gokrazy",
|
||||
"--full="+diskImage,
|
||||
"--target_storage_bytes="+strconv.Itoa(2*1024*1024*1024))
|
||||
packer.Env = append(os.Environ(), "GOARCH=amd64")
|
||||
packer.Stdout = os.Stdout
|
||||
packer.Stderr = os.Stderr
|
||||
log.Printf("%s", packer.Args)
|
||||
if err := packer.Run(); err != nil {
|
||||
t.Fatalf("%s: %v", packer.Args, err)
|
||||
}
|
||||
|
||||
// Chosen to match internal/gok/vmrun.go
|
||||
qemuArgs = append(qemuArgs,
|
||||
//"-enable-kvm",
|
||||
//"-cpu", "host",
|
||||
"-nodefaults",
|
||||
"-m", "1024",
|
||||
// required! system gets stuck without -smp
|
||||
"-smp", strconv.Itoa(max(runtime.NumCPU(), 2)),
|
||||
"-device", "e1000,netdev=net0",
|
||||
"-netdev", "user,id=net0,hostfwd=tcp::9080-:9080,hostfwd=tcp::9022-:22,hostfwd=tcp::9443-:9443",
|
||||
// Use -drive instead of vmtest.QemuOptions.Disks because the latter
|
||||
// results in wiring up the devices using SCSI in a way that the
|
||||
// router7 kernel config does not support.
|
||||
// TODO: update kernel config and switch to Disks:
|
||||
"-boot", "order=d",
|
||||
"-drive", "file="+diskImage+",format=raw",
|
||||
)
|
||||
|
||||
// Do not use hardware acceleration on GitHub Actions,
|
||||
// where there is no nested KVM available (by default).
|
||||
if os.Getenv("GITHUB_ACTIONS") != "true" {
|
||||
goarch := effectiveGOARCH()
|
||||
if goarch == runtime.GOARCH {
|
||||
// Hardware acceleration (in both cases) is only available for the
|
||||
// native architecture, e.g. arm64 for M1 MacBooks.
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
qemuArgs = append(qemuArgs, "-accel", "kvm")
|
||||
case "darwin":
|
||||
qemuArgs = append(qemuArgs, "-accel", "hvf")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
opts := vmtest.QemuOptions{
|
||||
Architecture: vmtest.QEMU_X86_64,
|
||||
OperatingSystem: vmtest.OS_LINUX,
|
||||
Params: qemuArgs,
|
||||
// Disks: []vmtest.QemuDisk{
|
||||
// {
|
||||
// Path: diskImage,
|
||||
// Format: "raw",
|
||||
// },
|
||||
// },
|
||||
Verbose: testing.Verbose(),
|
||||
Timeout: 30 * time.Minute,
|
||||
}
|
||||
qemu, err := vmtest.NewQemu(&opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return qemu
|
||||
}
|
||||
@@ -15,9 +15,10 @@ import (
|
||||
func TestNonModuleFiles(t *testing.T) {
|
||||
// Run this whole test in a throw-away temporary directory to not litter the
|
||||
// gokrazy/tools repository working copy.
|
||||
parent := t.TempDir()
|
||||
t.Chdir(t.TempDir())
|
||||
|
||||
// create a new instance
|
||||
parent := t.TempDir()
|
||||
c := gok.Context{
|
||||
Args: []string{
|
||||
"--parent_dir=" + parent,
|
||||
|
||||
@@ -13,22 +13,7 @@ import (
|
||||
func TestRelativeParentDir(t *testing.T) {
|
||||
// Run this whole test in a throw-away temporary directory to not litter the
|
||||
// gokrazy/tools repository working copy.
|
||||
|
||||
// TODO(go1.24): use t.Chdir()
|
||||
oldwd, err := os.Open(".")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chdir(t.TempDir()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
err := oldwd.Chdir()
|
||||
oldwd.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
t.Chdir(t.TempDir())
|
||||
|
||||
// create a new instance
|
||||
c := gok.Context{
|
||||
|
||||
398
internal/cap/License
Normal file
398
internal/cap/License
Normal file
@@ -0,0 +1,398 @@
|
||||
/* SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0-only */
|
||||
|
||||
Unless otherwise *explicitly* stated, the following text describes the
|
||||
licensed conditions under which the contents of this libcap/cap release
|
||||
may be used and distributed.
|
||||
|
||||
The licensed conditions are one or the other of these two Licenses:
|
||||
|
||||
- BSD 3-clause
|
||||
- GPL v2.0
|
||||
|
||||
-------------------------------------------------------------------------
|
||||
BSD 3-clause:
|
||||
-------------
|
||||
|
||||
Redistribution and use in source and binary forms of libcap/cap, with
|
||||
or without modification, are permitted provided that the following
|
||||
conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain any existing copyright
|
||||
notice, and this entire permission notice in its entirety,
|
||||
including the disclaimer of warranties.
|
||||
|
||||
2. Redistributions in binary form must reproduce all prior and current
|
||||
copyright notices, this list of conditions, and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
3. The name of any author may not be used to endorse or promote
|
||||
products derived from this software without their specific prior
|
||||
written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
||||
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
||||
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
|
||||
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGE.
|
||||
|
||||
-------------------------------------------------------------------------
|
||||
GPL v2.0:
|
||||
---------
|
||||
|
||||
ALTERNATIVELY, this product may be distributed under the terms of the
|
||||
GNU General Public License (v2.0 - see below), in which case the
|
||||
provisions of the GNU GPL are required INSTEAD OF the above
|
||||
restrictions. (This clause is necessary due to a potential conflict
|
||||
between the GNU GPL and the restrictions contained in a BSD-style
|
||||
copyright.)
|
||||
|
||||
-------------------------
|
||||
Full text of gpl-2.0.txt:
|
||||
-------------------------
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
13
internal/cap/README
Normal file
13
internal/cap/README
Normal file
@@ -0,0 +1,13 @@
|
||||
This package is modified from the libcap project to allow
|
||||
cross-platform marshalling to/from the kernel vfs format to write squashfs
|
||||
xattr values and for parsing the text format used with the setcap command
|
||||
|
||||
Package cap is the libcap API for Linux Capabilities written in
|
||||
Go. The official release announcement site for libcap is:
|
||||
|
||||
https://sites.google.com/site/fullycapable/
|
||||
|
||||
Like libcap, the cap package is distributed with a "you choose"
|
||||
License. Specifically: BSD 3-clause, or GPL2. See the License file.
|
||||
|
||||
Andrew G. Morgan <morgan@kernel.org>
|
||||
124
internal/cap/cap.go
Normal file
124
internal/cap/cap.go
Normal file
@@ -0,0 +1,124 @@
|
||||
// Package cap
|
||||
// Copyright (c) 2019-21 Andrew G. Morgan <morgan@kernel.org>
|
||||
//
|
||||
// The cap and psx packages are licensed with a (you choose) BSD
|
||||
// 3-clause or GPL2. See LICENSE file for details.
|
||||
// [the Fully Capable site]: https://sites.google.com/site/fullycapable/
|
||||
package cap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Value is the type of a single capability (or permission) bit.
|
||||
type Value uint
|
||||
|
||||
// Flag is the type of one of the three Value dimensions held in a
|
||||
// Set. It is also used in the (*IAB).Fill() method for changing the
|
||||
// Bounding and Ambient Vectors.
|
||||
type Flag uint
|
||||
|
||||
// Effective, Permitted, Inheritable are the three Flags of Values
|
||||
// held in a Set.
|
||||
const (
|
||||
Effective Flag = iota
|
||||
Permitted
|
||||
Inheritable
|
||||
)
|
||||
|
||||
// String identifies a Flag value by its conventional "e", "p" or "i"
|
||||
// string abbreviation.
|
||||
func (f Flag) String() string {
|
||||
switch f {
|
||||
case Effective:
|
||||
return "e"
|
||||
case Permitted:
|
||||
return "p"
|
||||
case Inheritable:
|
||||
return "i"
|
||||
default:
|
||||
return "<Error>"
|
||||
}
|
||||
}
|
||||
|
||||
// data holds a 32-bit slice of the compressed bitmaps of capability
|
||||
// sets as understood by the kernel.
|
||||
type data [Inheritable + 1]uint32
|
||||
|
||||
// Set is an opaque capabilities container for a set of system
|
||||
// capbilities. It holds individually addressable capability Value's
|
||||
// for the three capability Flag's. See GetFlag() and SetFlag() for
|
||||
// how to adjust them individually, and Clear() and ClearFlag() for
|
||||
// how to do bulk operations.
|
||||
//
|
||||
// For admin tasks associated with managing namespace specific file
|
||||
// capabilities, Set can also support a namespace-root-UID value which
|
||||
// defaults to zero. See GetNSOwner() and SetNSOwner().
|
||||
type Set struct {
|
||||
// mu protects all other members of a Set.
|
||||
mu sync.RWMutex
|
||||
|
||||
// flat holds Flag Value bitmaps for all capabilities
|
||||
// associated with this Set.
|
||||
flat []data
|
||||
|
||||
// Linux specific
|
||||
nsRoot int
|
||||
}
|
||||
|
||||
// Various known kernel magic values.
|
||||
const (
|
||||
kv1 = 0x19980330 // First iteration of process capabilities (32 bits).
|
||||
kv2 = 0x20071026 // First iteration of process and file capabilities (64 bits) - deprecated.
|
||||
kv3 = 0x20080522 // Most recently supported process and file capabilities (64 bits).
|
||||
)
|
||||
|
||||
var (
|
||||
// startUp protects setting of the following values: magic,
|
||||
// words, maxValues.
|
||||
startUp sync.Once
|
||||
|
||||
// magic holds the preferred magic number for the kernel ABI.
|
||||
magic uint32
|
||||
|
||||
// words holds the number of uint32's associated with each
|
||||
// capability Flag for this session.
|
||||
words int
|
||||
|
||||
// maxValues holds the number of bit values that are named by
|
||||
// the running kernel. This is generally expected to match
|
||||
// ValueCount which is autogenerated at packaging time.
|
||||
maxValues uint
|
||||
)
|
||||
|
||||
type header struct {
|
||||
magic uint32
|
||||
pid int32
|
||||
}
|
||||
|
||||
// defines from uapi/linux/prctl.h
|
||||
const (
|
||||
prCapBSetRead = 23
|
||||
prCapBSetDrop = 24
|
||||
)
|
||||
|
||||
// NewSet returns an empty capability set.
|
||||
func NewSet() *Set {
|
||||
startUp.Do(cInit)
|
||||
return &Set{
|
||||
flat: make([]data, words),
|
||||
}
|
||||
}
|
||||
|
||||
// ErrBadSet indicates a nil pointer was used for a *Set, or the
|
||||
// request of the Set is invalid in some way.
|
||||
var ErrBadSet = errors.New("bad capability set")
|
||||
|
||||
// good confirms that c looks valid.
|
||||
func (c *Set) good() error {
|
||||
if c == nil || len(c.flat) == 0 {
|
||||
return ErrBadSet
|
||||
}
|
||||
return nil
|
||||
}
|
||||
47
internal/cap/cinit_linux.go
Normal file
47
internal/cap/cinit_linux.go
Normal file
@@ -0,0 +1,47 @@
|
||||
//go:build linux
|
||||
|
||||
package cap
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// cInit performs the lazy identification of the capability vintage of
|
||||
// the running system.
|
||||
func cInit() {
|
||||
h := &header{
|
||||
magic: kv3,
|
||||
}
|
||||
_, _, _ = syscall.RawSyscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(h)), uintptr(0), 0)
|
||||
|
||||
magic = h.magic
|
||||
switch magic {
|
||||
case kv1:
|
||||
words = 1
|
||||
case kv2, kv3:
|
||||
words = 2
|
||||
default:
|
||||
// Fall back to a known good version.
|
||||
magic = kv3
|
||||
words = 2
|
||||
}
|
||||
// Use the bounding set to evaluate which capabilities exist.
|
||||
maxValues = uint(sort.Search(32*words, func(n int) bool {
|
||||
_, err := GetBound(Value(n))
|
||||
return err != nil
|
||||
}))
|
||||
if maxValues == 0 {
|
||||
// Fall back to using the largest value defined at build time.
|
||||
maxValues = NamedCount
|
||||
}
|
||||
}
|
||||
func GetBound(val Value) (bool, error) {
|
||||
r, _, err := syscall.RawSyscall(syscall.SYS_PRCTL, prCapBSetRead, uintptr(val), 0)
|
||||
if err != 0 {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return int(r) > 0, nil
|
||||
}
|
||||
12
internal/cap/cinit_other.go
Normal file
12
internal/cap/cinit_other.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build !linux
|
||||
|
||||
package cap
|
||||
|
||||
// cInit performs the lazy identification of the capability vintage of
|
||||
// the running system.
|
||||
func cInit() {
|
||||
if maxValues == 0 {
|
||||
// Fall back to using the largest value defined at build time.
|
||||
maxValues = NamedCount
|
||||
}
|
||||
}
|
||||
174
internal/cap/file.go
Normal file
174
internal/cap/file.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package cap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// uapi/linux/xattr.h defined.
|
||||
var (
|
||||
xattrNameCaps, _ = syscall.BytePtrFromString("security.capability")
|
||||
)
|
||||
|
||||
// uapi/linux/capability.h defined.
|
||||
const (
|
||||
vfsCapRevisionMask = uint32(0xff000000)
|
||||
vfsCapFlagsMask = ^vfsCapRevisionMask
|
||||
vfsCapFlagsEffective = uint32(1)
|
||||
|
||||
vfsCapRevision1 = uint32(0x01000000)
|
||||
vfsCapRevision2 = uint32(0x02000000)
|
||||
vfsCapRevision3 = uint32(0x03000000)
|
||||
)
|
||||
|
||||
// Data types stored in little-endian order.
|
||||
|
||||
type vfsCaps1 struct {
|
||||
MagicEtc uint32
|
||||
Data [1]struct {
|
||||
Permitted, Inheritable uint32
|
||||
}
|
||||
}
|
||||
|
||||
type vfsCaps2 struct {
|
||||
MagicEtc uint32
|
||||
Data [2]struct {
|
||||
Permitted, Inheritable uint32
|
||||
}
|
||||
}
|
||||
|
||||
type vfsCaps3 struct {
|
||||
MagicEtc uint32
|
||||
Data [2]struct {
|
||||
Permitted, Inheritable uint32
|
||||
}
|
||||
RootID uint32
|
||||
}
|
||||
|
||||
// ErrBadSize indicates the loaded file capability has
|
||||
// an invalid number of bytes in it.
|
||||
var ErrBadSize = errors.New("filecap bad size")
|
||||
|
||||
// ErrBadMagic indicates that the kernel preferred magic number for
|
||||
// capability Set values is not supported by this package. This
|
||||
// generally implies you are using an exceptionally old
|
||||
// "../libcap/cap" package. An upgrade is needed, or failing that see
|
||||
// [the Fully Capable site] for the way to report or review a bug.
|
||||
//
|
||||
// [the Fully Capable site]: https://sites.google.com/site/fullycapable/
|
||||
var ErrBadMagic = errors.New("unsupported magic")
|
||||
|
||||
// ErrBadPath indicates a failed attempt to set a file capability on
|
||||
// an irregular (non-executable) file.
|
||||
var ErrBadPath = errors.New("file is not a regular executable")
|
||||
|
||||
// ErrOutOfRange indicates an erroneous value for MinExtFlagSize.
|
||||
var ErrOutOfRange = errors.New("flag length invalid for export")
|
||||
|
||||
// DigestFileCap unpacks a file capability and returns it in a *Set
|
||||
// form.
|
||||
func DigestFileCap(d []byte) (*Set, error) {
|
||||
var (
|
||||
err error
|
||||
raw1 vfsCaps1
|
||||
raw2 vfsCaps2
|
||||
raw3 vfsCaps3
|
||||
)
|
||||
sz := len(d)
|
||||
if sz < binary.Size(raw1) || sz > binary.Size(raw3) {
|
||||
return nil, ErrBadSize
|
||||
}
|
||||
b := bytes.NewReader(d)
|
||||
var magicEtc uint32
|
||||
if err = binary.Read(b, binary.LittleEndian, &magicEtc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := NewSet()
|
||||
b.Seek(0, io.SeekStart)
|
||||
switch magicEtc & vfsCapRevisionMask {
|
||||
case vfsCapRevision1:
|
||||
if err = binary.Read(b, binary.LittleEndian, &raw1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := raw1.Data[0]
|
||||
c.flat[0][Permitted] = data.Permitted
|
||||
c.flat[0][Inheritable] = data.Inheritable
|
||||
if raw1.MagicEtc&vfsCapFlagsMask == vfsCapFlagsEffective {
|
||||
c.flat[0][Effective] = data.Inheritable | data.Permitted
|
||||
}
|
||||
case vfsCapRevision2:
|
||||
if err = binary.Read(b, binary.LittleEndian, &raw2); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, data := range raw2.Data {
|
||||
c.flat[i][Permitted] = data.Permitted
|
||||
c.flat[i][Inheritable] = data.Inheritable
|
||||
if raw2.MagicEtc&vfsCapFlagsMask == vfsCapFlagsEffective {
|
||||
c.flat[i][Effective] = data.Inheritable | data.Permitted
|
||||
}
|
||||
}
|
||||
case vfsCapRevision3:
|
||||
if err = binary.Read(b, binary.LittleEndian, &raw3); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, data := range raw3.Data {
|
||||
c.flat[i][Permitted] = data.Permitted
|
||||
c.flat[i][Inheritable] = data.Inheritable
|
||||
if raw3.MagicEtc&vfsCapFlagsMask == vfsCapFlagsEffective {
|
||||
c.flat[i][Effective] = data.Inheritable | data.Permitted
|
||||
}
|
||||
}
|
||||
c.nsRoot = int(raw3.RootID)
|
||||
default:
|
||||
return nil, ErrBadMagic
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// PackFileCap transforms a system capability into a VFS form. Because
|
||||
// of the way Linux stores capabilities in the file extended
|
||||
// attributes, the process is a little lossy with respect to effective
|
||||
// bits.
|
||||
func (c *Set) PackFileCap() ([]byte, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
var magic uint32
|
||||
switch words {
|
||||
case 1:
|
||||
if c.nsRoot != 0 {
|
||||
return nil, ErrBadSet // nsRoot not supported for single DWORD caps.
|
||||
}
|
||||
magic = vfsCapRevision1
|
||||
case 2:
|
||||
if c.nsRoot == 0 {
|
||||
magic = vfsCapRevision2
|
||||
break
|
||||
}
|
||||
magic = vfsCapRevision3
|
||||
}
|
||||
if magic == 0 {
|
||||
return nil, ErrBadSize
|
||||
}
|
||||
eff := uint32(0)
|
||||
for _, f := range c.flat {
|
||||
eff |= (f[Permitted] | f[Inheritable]) & f[Effective]
|
||||
}
|
||||
if eff != 0 {
|
||||
magic |= vfsCapFlagsEffective
|
||||
}
|
||||
b := new(bytes.Buffer)
|
||||
binary.Write(b, binary.LittleEndian, magic)
|
||||
for _, f := range c.flat {
|
||||
binary.Write(b, binary.LittleEndian, f[Permitted])
|
||||
binary.Write(b, binary.LittleEndian, f[Inheritable])
|
||||
}
|
||||
if c.nsRoot != 0 {
|
||||
binary.Write(b, binary.LittleEndian, uint32(c.nsRoot))
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
134
internal/cap/flags.go
Normal file
134
internal/cap/flags.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package cap
|
||||
|
||||
import "errors"
|
||||
|
||||
// GetFlag determines if the requested Value is enabled in the
|
||||
// specified Flag of the capability Set.
|
||||
func (c *Set) GetFlag(vec Flag, val Value) (bool, error) {
|
||||
if err := c.good(); err != nil {
|
||||
// Checked this first, because otherwise we are sure
|
||||
// cInit has been called.
|
||||
return false, err
|
||||
}
|
||||
offset, mask, err := bitOf(vec, val)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.flat[offset][vec]&mask != 0, nil
|
||||
}
|
||||
|
||||
// SetFlag sets the requested bits to the indicated enable state. This
|
||||
// function does not perform any security checks, so values can be set
|
||||
// out-of-order. Only when the Set is used to SetProc() etc., will the
|
||||
// bits be checked for validity and permission by the kernel. If the
|
||||
// function returns an error, the Set will not be modified.
|
||||
func (c *Set) SetFlag(vec Flag, enable bool, val ...Value) error {
|
||||
if err := c.good(); err != nil {
|
||||
// Checked this first, because otherwise we are sure
|
||||
// cInit has been called.
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
// Make a backup.
|
||||
replace := make([]uint32, words)
|
||||
for i := range replace {
|
||||
replace[i] = c.flat[i][vec]
|
||||
}
|
||||
var err error
|
||||
for _, v := range val {
|
||||
offset, mask, err2 := bitOf(vec, v)
|
||||
if err2 != nil {
|
||||
err = err2
|
||||
break
|
||||
}
|
||||
if enable {
|
||||
c.flat[offset][vec] |= mask
|
||||
} else {
|
||||
c.flat[offset][vec] &= ^mask
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
// Clean up.
|
||||
for i, bits := range replace {
|
||||
c.flat[i][vec] = bits
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear fully clears a capability set.
|
||||
func (c *Set) Clear() error {
|
||||
if err := c.good(); err != nil {
|
||||
return err
|
||||
}
|
||||
// startUp.Do(cInit) is not called here because c cannot be
|
||||
// initialized except via this package and doing that will
|
||||
// perform that call at least once (sic).
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.flat = make([]data, words)
|
||||
c.nsRoot = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
// ErrBadValue indicates a bad capability value was specified.
|
||||
var ErrBadValue = errors.New("bad capability value")
|
||||
|
||||
// bitOf converts from a Value into the offset and mask for a specific
|
||||
// Value bit in the compressed (kernel ABI) representation of a
|
||||
// capabilities. If the requested bit is unsupported, an error is
|
||||
// returned.
|
||||
func bitOf(vec Flag, val Value) (uint, uint32, error) {
|
||||
if vec > Inheritable || val > Value(words*32) {
|
||||
return 0, 0, ErrBadValue
|
||||
}
|
||||
u := uint(val)
|
||||
return u / 32, uint32(1) << (u % 32), nil
|
||||
}
|
||||
|
||||
// allMask returns the mask of valid bits in the all mask for index.
|
||||
func allMask(index uint) (mask uint32) {
|
||||
if maxValues == 0 {
|
||||
panic("uninitialized package")
|
||||
}
|
||||
base := 32 * uint(index)
|
||||
if maxValues <= base {
|
||||
return
|
||||
}
|
||||
if maxValues >= 32+base {
|
||||
mask = ^mask
|
||||
return
|
||||
}
|
||||
mask = uint32((uint64(1) << (maxValues % 32)) - 1)
|
||||
return
|
||||
}
|
||||
|
||||
// forceFlag sets 'all' capability values (supported by the kernel) of
|
||||
// a specified Flag to enable.
|
||||
func (c *Set) forceFlag(vec Flag, enable bool) error {
|
||||
if err := c.good(); err != nil {
|
||||
return err
|
||||
}
|
||||
if vec > Inheritable {
|
||||
return ErrBadSet
|
||||
}
|
||||
m := uint32(0)
|
||||
if enable {
|
||||
m = ^m
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
for i := range c.flat {
|
||||
c.flat[i][vec] = m & allMask(uint(i))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearFlag clears all the Values associated with the specified Flag.
|
||||
func (c *Set) ClearFlag(vec Flag) error {
|
||||
return c.forceFlag(vec, false)
|
||||
}
|
||||
440
internal/cap/names.go
Normal file
440
internal/cap/names.go
Normal file
@@ -0,0 +1,440 @@
|
||||
package cap
|
||||
|
||||
/* ** DO NOT EDIT THIS FILE. IT WAS AUTO-GENERATED BY LIBCAP'S GO BUILDER (mknames.go) ** */
|
||||
|
||||
// NamedCount holds the number of capability values, with official
|
||||
// names, known at the time this libcap/cap version was released. The
|
||||
// "../libcap/cap" package is fully able to manipulate higher numbered
|
||||
// capability values by numerical value. However, if you find
|
||||
// cap.NamedCount < cap.MaxBits(), it is probably time to upgrade this
|
||||
// package on your system.
|
||||
//
|
||||
// FWIW the userspace tool '/sbin/capsh' also contains a runtime check
|
||||
// for the condition that libcap is behind the running kernel in this
|
||||
// way.
|
||||
const NamedCount = 41
|
||||
|
||||
// CHOWN etc., are the named capability values of the Linux
|
||||
// kernel. The canonical source for each name is the
|
||||
// "uapi/linux/capabilities.h" file. Some values may not be available
|
||||
// (yet) where the kernel is older. The actual number of capabities
|
||||
// supported by the running kernel can be obtained using the
|
||||
// cap.MaxBits() function.
|
||||
const (
|
||||
// CHOWN allows a process to arbitrarily change the user and
|
||||
// group ownership of a file.
|
||||
CHOWN Value = iota
|
||||
|
||||
// DAC_OVERRIDE allows a process to override of all Discretionary
|
||||
// Access Control (DAC) access, including ACL execute
|
||||
// access. That is read, write or execute files that the
|
||||
// process would otherwise not have access to. This
|
||||
// excludes DAC access covered by cap.LINUX_IMMUTABLE.
|
||||
DAC_OVERRIDE
|
||||
|
||||
// DAC_READ_SEARCH allows a process to override all DAC restrictions
|
||||
// limiting the read and search of files and
|
||||
// directories. This excludes DAC access covered by
|
||||
// cap.LINUX_IMMUTABLE.
|
||||
DAC_READ_SEARCH
|
||||
|
||||
// FOWNER allows a process to perform operations on files, even
|
||||
// where file owner ID should otherwise need be equal to
|
||||
// the UID, except where cap.FSETID is applicable. It
|
||||
// doesn't override MAC and DAC restrictions.
|
||||
//
|
||||
// This capability permits the deletion of a file owned
|
||||
// by another UID in a directory protected by the sticky
|
||||
// (t) bit.
|
||||
FOWNER
|
||||
|
||||
// FSETID allows a process to set the S_ISUID and S_ISUID bits of
|
||||
// the file permissions, even when the process' effective
|
||||
// UID or GID/supplementary GIDs do not match that of the
|
||||
// file.
|
||||
FSETID
|
||||
|
||||
// KILL allows a process to send a kill(2) signal to any other
|
||||
// process - overriding the limitation that there be a
|
||||
// [E]UID match between source and target process.
|
||||
KILL
|
||||
|
||||
// SETGID allows a process to freely manipulate its own GIDs:
|
||||
// - arbitrarily set the GID, EGID, REGID, RESGID values
|
||||
// - arbitrarily set the supplementary GIDs
|
||||
// - allows the forging of GID credentials passed over a
|
||||
// socket
|
||||
SETGID
|
||||
|
||||
// SETUID allows a process to freely manipulate its own UIDs:
|
||||
// - arbitrarily set the UID, EUID, REUID and RESUID
|
||||
// values
|
||||
// - allows the forging of UID credentials passed over a
|
||||
// socket
|
||||
SETUID
|
||||
|
||||
// SETPCAP allows a process to freely manipulate its inheritable
|
||||
// capabilities.
|
||||
//
|
||||
// Linux supports the POSIX.1e Inheritable set, the POXIX.1e (X
|
||||
// vector) known in Linux as the Bounding vector, as well as
|
||||
// the Linux extension Ambient vector.
|
||||
//
|
||||
// This capability permits dropping bits from the Bounding
|
||||
// vector (ie. raising B bits in the libcap IAB
|
||||
// representation). It also permits the process to raise
|
||||
// Ambient vector bits that are both raised in the Permitted
|
||||
// and Inheritable sets of the process. This capability cannot
|
||||
// be used to raise Permitted bits, Effective bits beyond those
|
||||
// already present in the process' permitted set, or
|
||||
// Inheritable bits beyond those present in the Bounding
|
||||
// vector.
|
||||
//
|
||||
// [Historical note: prior to the advent of file capabilities
|
||||
// (2008), this capability was suppressed by default, as its
|
||||
// unsuppressed behavior was not auditable: it could
|
||||
// asynchronously grant its own Permitted capabilities to and
|
||||
// remove capabilities from other processes arbitrarily. The
|
||||
// former leads to undefined behavior, and the latter is better
|
||||
// served by the kill system call.]
|
||||
SETPCAP
|
||||
|
||||
// LINUX_IMMUTABLE allows a process to modify the S_IMMUTABLE and
|
||||
// S_APPEND file attributes.
|
||||
LINUX_IMMUTABLE
|
||||
|
||||
// NET_BIND_SERVICE allows a process to bind to privileged ports:
|
||||
// - TCP/UDP sockets below 1024
|
||||
// - ATM VCIs below 32
|
||||
NET_BIND_SERVICE
|
||||
|
||||
// NET_BROADCAST allows a process to broadcast to the network and to
|
||||
// listen to multicast.
|
||||
NET_BROADCAST
|
||||
|
||||
// NET_ADMIN allows a process to perform network configuration
|
||||
// operations:
|
||||
// - interface configuration
|
||||
// - administration of IP firewall, masquerading and
|
||||
// accounting
|
||||
// - setting debug options on sockets
|
||||
// - modification of routing tables
|
||||
// - setting arbitrary process, and process group
|
||||
// ownership on sockets
|
||||
// - binding to any address for transparent proxying
|
||||
// (this is also allowed via cap.NET_RAW)
|
||||
// - setting TOS (Type of service)
|
||||
// - setting promiscuous mode
|
||||
// - clearing driver statistics
|
||||
// - multicasing
|
||||
// - read/write of device-specific registers
|
||||
// - activation of ATM control sockets
|
||||
NET_ADMIN
|
||||
|
||||
// NET_RAW allows a process to use raw networking:
|
||||
// - RAW sockets
|
||||
// - PACKET sockets
|
||||
// - binding to any address for transparent proxying
|
||||
// (also permitted via cap.NET_ADMIN)
|
||||
NET_RAW
|
||||
|
||||
// IPC_LOCK allows a process to lock shared memory segments for IPC
|
||||
// purposes. Also enables mlock and mlockall system
|
||||
// calls.
|
||||
IPC_LOCK
|
||||
|
||||
// IPC_OWNER allows a process to override IPC ownership checks.
|
||||
IPC_OWNER
|
||||
|
||||
// SYS_MODULE allows a process to initiate the loading and unloading
|
||||
// of kernel modules. This capability can effectively
|
||||
// modify kernel without limit.
|
||||
SYS_MODULE
|
||||
|
||||
// SYS_RAWIO allows a process to perform raw IO:
|
||||
// - permit ioper/iopl access
|
||||
// - permit sending USB messages to any device via
|
||||
// /dev/bus/usb
|
||||
SYS_RAWIO
|
||||
|
||||
// SYS_CHROOT allows a process to perform a chroot syscall to change
|
||||
// the effective root of the process' file system:
|
||||
// redirect to directory "/" to some other location.
|
||||
SYS_CHROOT
|
||||
|
||||
// SYS_PTRACE allows a process to perform a ptrace() of any other
|
||||
// process.
|
||||
SYS_PTRACE
|
||||
|
||||
// SYS_PACCT allows a process to configure process accounting.
|
||||
SYS_PACCT
|
||||
|
||||
// SYS_ADMIN allows a process to perform a somewhat arbitrary
|
||||
// grab-bag of privileged operations. Over time, this
|
||||
// capability should weaken as specific capabilities are
|
||||
// created for subsets of cap.SYS_ADMINs functionality:
|
||||
// - configuration of the secure attention key
|
||||
// - administration of the random device
|
||||
// - examination and configuration of disk quotas
|
||||
// - setting the domainname
|
||||
// - setting the hostname
|
||||
// - calling bdflush()
|
||||
// - mount() and umount(), setting up new SMB connection
|
||||
// - some autofs root ioctls
|
||||
// - nfsservctl
|
||||
// - VM86_REQUEST_IRQ
|
||||
// - to read/write pci config on alpha
|
||||
// - irix_prctl on mips (setstacksize)
|
||||
// - flushing all cache on m68k (sys_cacheflush)
|
||||
// - removing semaphores
|
||||
// - Used instead of cap.CHOWN to "chown" IPC message
|
||||
// queues, semaphores and shared memory
|
||||
// - locking/unlocking of shared memory segment
|
||||
// - turning swap on/off
|
||||
// - forged pids on socket credentials passing
|
||||
// - setting readahead and flushing buffers on block
|
||||
// devices
|
||||
// - setting geometry in floppy driver
|
||||
// - turning DMA on/off in xd driver
|
||||
// - administration of md devices (mostly the above, but
|
||||
// some extra ioctls)
|
||||
// - tuning the ide driver
|
||||
// - access to the nvram device
|
||||
// - administration of apm_bios, serial and bttv (TV)
|
||||
// device
|
||||
// - manufacturer commands in isdn CAPI support driver
|
||||
// - reading non-standardized portions of PCI
|
||||
// configuration space
|
||||
// - DDI debug ioctl on sbpcd driver
|
||||
// - setting up serial ports
|
||||
// - sending raw qic-117 commands
|
||||
// - enabling/disabling tagged queuing on SCSI
|
||||
// controllers and sending arbitrary SCSI commands
|
||||
// - setting encryption key on loopback filesystem
|
||||
// - setting zone reclaim policy
|
||||
SYS_ADMIN
|
||||
|
||||
// SYS_BOOT allows a process to initiate a reboot of the system.
|
||||
SYS_BOOT
|
||||
|
||||
// SYS_NICE allows a process to maipulate the execution priorities
|
||||
// of arbitrary processes:
|
||||
// - those involving different UIDs
|
||||
// - setting their CPU affinity
|
||||
// - alter the FIFO vs. round-robin (realtime)
|
||||
// scheduling for itself and other processes.
|
||||
SYS_NICE
|
||||
|
||||
// SYS_RESOURCE allows a process to adjust resource related parameters
|
||||
// of processes and the system:
|
||||
// - set and override resource limits
|
||||
// - override quota limits
|
||||
// - override the reserved space on ext2 filesystem
|
||||
// (this can also be achieved via cap.FSETID)
|
||||
// - modify the data journaling mode on ext3 filesystem,
|
||||
// which uses journaling resources
|
||||
// - override size restrictions on IPC message queues
|
||||
// - configure more than 64Hz interrupts from the
|
||||
// real-time clock
|
||||
// - override the maximum number of consoles for console
|
||||
// allocation
|
||||
// - override the maximum number of keymaps
|
||||
SYS_RESOURCE
|
||||
|
||||
// SYS_TIME allows a process to perform time manipulation of clocks:
|
||||
// - alter the system clock
|
||||
// - enable irix_stime on MIPS
|
||||
// - set the real-time clock
|
||||
SYS_TIME
|
||||
|
||||
// SYS_TTY_CONFIG allows a process to manipulate tty devices:
|
||||
// - configure tty devices
|
||||
// - perform vhangup() of a tty
|
||||
SYS_TTY_CONFIG
|
||||
|
||||
// MKNOD allows a process to perform privileged operations with
|
||||
// the mknod() system call.
|
||||
MKNOD
|
||||
|
||||
// LEASE allows a process to take leases on files.
|
||||
LEASE
|
||||
|
||||
// AUDIT_WRITE allows a process to write to the audit log via a
|
||||
// unicast netlink socket.
|
||||
AUDIT_WRITE
|
||||
|
||||
// AUDIT_CONTROL allows a process to configure audit logging via a
|
||||
// unicast netlink socket.
|
||||
AUDIT_CONTROL
|
||||
|
||||
// SETFCAP allows a process to set capabilities on files.
|
||||
// Permits a process to uid_map the uid=0 of the
|
||||
// parent user namespace into that of the child
|
||||
// namespace. Also, permits a process to override
|
||||
// securebits locks through user namespace
|
||||
// creation.
|
||||
SETFCAP
|
||||
|
||||
// MAC_OVERRIDE allows a process to override Manditory Access Control
|
||||
// (MAC) access. Not all kernels are configured with a MAC
|
||||
// mechanism, but this is the capability reserved for
|
||||
// overriding them.
|
||||
MAC_OVERRIDE
|
||||
|
||||
// MAC_ADMIN allows a process to configure the Mandatory Access
|
||||
// Control (MAC) policy. Not all kernels are configured
|
||||
// with a MAC enabled, but if they are this capability is
|
||||
// reserved for code to perform administration tasks.
|
||||
MAC_ADMIN
|
||||
|
||||
// SYSLOG allows a process to configure the kernel's syslog
|
||||
// (printk) behavior.
|
||||
SYSLOG
|
||||
|
||||
// WAKE_ALARM allows a process to trigger something that can wake the
|
||||
// system up.
|
||||
WAKE_ALARM
|
||||
|
||||
// BLOCK_SUSPEND allows a process to block system suspends - prevent the
|
||||
// system from entering a lower power state.
|
||||
BLOCK_SUSPEND
|
||||
|
||||
// AUDIT_READ allows a process to read the audit log via a multicast
|
||||
// netlink socket.
|
||||
AUDIT_READ
|
||||
|
||||
// PERFMON allows a process to enable observability of privileged
|
||||
// operations related to performance. The mechanisms
|
||||
// include perf_events, i915_perf and other kernel
|
||||
// subsystems.
|
||||
PERFMON
|
||||
|
||||
// BPF allows a process to manipulate aspects of the kernel
|
||||
// enhanced Berkeley Packet Filter (BPF) system. This is
|
||||
// an execution subsystem of the kernel, that manages BPF
|
||||
// programs. cap.BPF permits a process to:
|
||||
// - create all types of BPF maps
|
||||
// - advanced verifier features:
|
||||
// - indirect variable access
|
||||
// - bounded loops
|
||||
// - BPF to BPF function calls
|
||||
// - scalar precision tracking
|
||||
// - larger complexity limits
|
||||
// - dead code elimination
|
||||
// - potentially other features
|
||||
//
|
||||
// Other capabilities can be used together with cap.BFP to
|
||||
// further manipulate the BPF system:
|
||||
// - cap.PERFMON relaxes the verifier checks as follows:
|
||||
// - BPF programs can use pointer-to-integer
|
||||
// conversions
|
||||
// - speculation attack hardening measures can be
|
||||
// bypassed
|
||||
// - bpf_probe_read to read arbitrary kernel memory is
|
||||
// permitted
|
||||
// - bpf_trace_printk to print the content of kernel
|
||||
// memory
|
||||
// - cap.SYS_ADMIN permits the following:
|
||||
// - use of bpf_probe_write_user
|
||||
// - iteration over the system-wide loaded programs,
|
||||
// maps, links BTFs and convert their IDs to file
|
||||
// descriptors.
|
||||
// - cap.PERFMON is required to load tracing programs.
|
||||
// - cap.NET_ADMIN is required to load networking
|
||||
// programs.
|
||||
BPF
|
||||
|
||||
// CHECKPOINT_RESTORE allows a process to perform checkpoint
|
||||
// and restore operations. Also permits
|
||||
// explicit PID control via clone3() and
|
||||
// also writing to ns_last_pid.
|
||||
CHECKPOINT_RESTORE
|
||||
)
|
||||
|
||||
var names = map[Value]string{
|
||||
CHOWN: "cap_chown",
|
||||
DAC_OVERRIDE: "cap_dac_override",
|
||||
DAC_READ_SEARCH: "cap_dac_read_search",
|
||||
FOWNER: "cap_fowner",
|
||||
FSETID: "cap_fsetid",
|
||||
KILL: "cap_kill",
|
||||
SETGID: "cap_setgid",
|
||||
SETUID: "cap_setuid",
|
||||
SETPCAP: "cap_setpcap",
|
||||
LINUX_IMMUTABLE: "cap_linux_immutable",
|
||||
NET_BIND_SERVICE: "cap_net_bind_service",
|
||||
NET_BROADCAST: "cap_net_broadcast",
|
||||
NET_ADMIN: "cap_net_admin",
|
||||
NET_RAW: "cap_net_raw",
|
||||
IPC_LOCK: "cap_ipc_lock",
|
||||
IPC_OWNER: "cap_ipc_owner",
|
||||
SYS_MODULE: "cap_sys_module",
|
||||
SYS_RAWIO: "cap_sys_rawio",
|
||||
SYS_CHROOT: "cap_sys_chroot",
|
||||
SYS_PTRACE: "cap_sys_ptrace",
|
||||
SYS_PACCT: "cap_sys_pacct",
|
||||
SYS_ADMIN: "cap_sys_admin",
|
||||
SYS_BOOT: "cap_sys_boot",
|
||||
SYS_NICE: "cap_sys_nice",
|
||||
SYS_RESOURCE: "cap_sys_resource",
|
||||
SYS_TIME: "cap_sys_time",
|
||||
SYS_TTY_CONFIG: "cap_sys_tty_config",
|
||||
MKNOD: "cap_mknod",
|
||||
LEASE: "cap_lease",
|
||||
AUDIT_WRITE: "cap_audit_write",
|
||||
AUDIT_CONTROL: "cap_audit_control",
|
||||
SETFCAP: "cap_setfcap",
|
||||
MAC_OVERRIDE: "cap_mac_override",
|
||||
MAC_ADMIN: "cap_mac_admin",
|
||||
SYSLOG: "cap_syslog",
|
||||
WAKE_ALARM: "cap_wake_alarm",
|
||||
BLOCK_SUSPEND: "cap_block_suspend",
|
||||
AUDIT_READ: "cap_audit_read",
|
||||
PERFMON: "cap_perfmon",
|
||||
BPF: "cap_bpf",
|
||||
CHECKPOINT_RESTORE: "cap_checkpoint_restore",
|
||||
}
|
||||
|
||||
var bits = map[string]Value{
|
||||
"cap_chown": CHOWN,
|
||||
"cap_dac_override": DAC_OVERRIDE,
|
||||
"cap_dac_read_search": DAC_READ_SEARCH,
|
||||
"cap_fowner": FOWNER,
|
||||
"cap_fsetid": FSETID,
|
||||
"cap_kill": KILL,
|
||||
"cap_setgid": SETGID,
|
||||
"cap_setuid": SETUID,
|
||||
"cap_setpcap": SETPCAP,
|
||||
"cap_linux_immutable": LINUX_IMMUTABLE,
|
||||
"cap_net_bind_service": NET_BIND_SERVICE,
|
||||
"cap_net_broadcast": NET_BROADCAST,
|
||||
"cap_net_admin": NET_ADMIN,
|
||||
"cap_net_raw": NET_RAW,
|
||||
"cap_ipc_lock": IPC_LOCK,
|
||||
"cap_ipc_owner": IPC_OWNER,
|
||||
"cap_sys_module": SYS_MODULE,
|
||||
"cap_sys_rawio": SYS_RAWIO,
|
||||
"cap_sys_chroot": SYS_CHROOT,
|
||||
"cap_sys_ptrace": SYS_PTRACE,
|
||||
"cap_sys_pacct": SYS_PACCT,
|
||||
"cap_sys_admin": SYS_ADMIN,
|
||||
"cap_sys_boot": SYS_BOOT,
|
||||
"cap_sys_nice": SYS_NICE,
|
||||
"cap_sys_resource": SYS_RESOURCE,
|
||||
"cap_sys_time": SYS_TIME,
|
||||
"cap_sys_tty_config": SYS_TTY_CONFIG,
|
||||
"cap_mknod": MKNOD,
|
||||
"cap_lease": LEASE,
|
||||
"cap_audit_write": AUDIT_WRITE,
|
||||
"cap_audit_control": AUDIT_CONTROL,
|
||||
"cap_setfcap": SETFCAP,
|
||||
"cap_mac_override": MAC_OVERRIDE,
|
||||
"cap_mac_admin": MAC_ADMIN,
|
||||
"cap_syslog": SYSLOG,
|
||||
"cap_wake_alarm": WAKE_ALARM,
|
||||
"cap_block_suspend": BLOCK_SUSPEND,
|
||||
"cap_audit_read": AUDIT_READ,
|
||||
"cap_perfmon": PERFMON,
|
||||
"cap_bpf": BPF,
|
||||
"cap_checkpoint_restore": CHECKPOINT_RESTORE,
|
||||
}
|
||||
328
internal/cap/text.go
Normal file
328
internal/cap/text.go
Normal file
@@ -0,0 +1,328 @@
|
||||
package cap
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// String converts a capability Value into its canonical text
|
||||
// representation.
|
||||
func (v Value) String() string {
|
||||
name, ok := names[v]
|
||||
if ok {
|
||||
return name
|
||||
}
|
||||
// Un-named capabilities are referred to numerically (in decimal).
|
||||
return strconv.Itoa(int(v))
|
||||
}
|
||||
|
||||
// FromName converts a named capability Value to its binary
|
||||
// representation.
|
||||
func FromName(name string) (Value, error) {
|
||||
startUp.Do(cInit)
|
||||
v, ok := bits[name]
|
||||
if ok {
|
||||
if v >= Value(words*32) {
|
||||
return 0, ErrBadValue
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
i, err := strconv.Atoi(name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if i >= 0 && i < int(words*32) {
|
||||
return Value(i), nil
|
||||
}
|
||||
return 0, ErrBadValue
|
||||
}
|
||||
|
||||
const (
|
||||
eBin uint = (1 << Effective)
|
||||
pBin = (1 << Permitted)
|
||||
iBin = (1 << Inheritable)
|
||||
)
|
||||
|
||||
var combos = []string{"", "e", "p", "ep", "i", "ei", "ip", "eip"}
|
||||
|
||||
// histo generates a histogram of flag state combinations.
|
||||
// Note: c is locked by or private to the caller.
|
||||
func (c *Set) histo(bins []int, patterns []uint, from, limit Value) uint {
|
||||
for v := from; v < limit; v++ {
|
||||
b := uint(v & 31)
|
||||
u, bit, err := bitOf(0, v)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
x := uint((c.flat[u][Effective]&bit)>>b) * eBin
|
||||
x |= uint((c.flat[u][Permitted]&bit)>>b) * pBin
|
||||
x |= uint((c.flat[u][Inheritable]&bit)>>b) * iBin
|
||||
bins[x]++
|
||||
patterns[uint(v)] = x
|
||||
}
|
||||
// Note, in the loop, we use >= to pick the smallest value for
|
||||
// m with the highest bin value. That is ties break towards
|
||||
// m=0.
|
||||
m := uint(7)
|
||||
for t := m; t > 0; {
|
||||
t--
|
||||
if bins[t] >= bins[m] {
|
||||
m = t
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// String converts a full capability Set into a single short readable
|
||||
// string representation (which may contain spaces). See the
|
||||
// cap.FromText() function for an explanation of its return values.
|
||||
//
|
||||
// Note (*cap.Set).String() may evolve to generate more compact
|
||||
// strings representing the a given Set over time, but it should
|
||||
// maintain compatibility with the libcap:cap_to_text() function for
|
||||
// any given release. Further, it will always be an inverse of
|
||||
// cap.FromText().
|
||||
func (c *Set) String() string {
|
||||
if err := c.good(); err != nil {
|
||||
return "<invalid>"
|
||||
}
|
||||
bins := make([]int, 8)
|
||||
patterns := make([]uint, maxValues)
|
||||
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
// Note, in order to have a *Set pointer, startUp.Do(cInit)
|
||||
// must have been called which sets maxValues.
|
||||
m := c.histo(bins, patterns, 0, Value(maxValues))
|
||||
|
||||
// Background state is the most popular of the named bits.
|
||||
vs := []string{"=" + combos[m]}
|
||||
for i := uint(8); i > 0; {
|
||||
i--
|
||||
if i == m || bins[i] == 0 {
|
||||
continue
|
||||
}
|
||||
var list []string
|
||||
for j, p := range patterns {
|
||||
if p != i {
|
||||
continue
|
||||
}
|
||||
list = append(list, Value(j).String())
|
||||
}
|
||||
x := strings.Join(list, ",")
|
||||
var y, z string
|
||||
if cf := i & ^m; cf != 0 {
|
||||
op := "+"
|
||||
if len(vs) == 1 && vs[0] == "=" {
|
||||
// Special case "= foo+..." == "foo=...".
|
||||
// Prefer because it
|
||||
vs = nil
|
||||
op = "="
|
||||
}
|
||||
y = op + combos[cf]
|
||||
}
|
||||
if cf := m & ^i; cf != 0 {
|
||||
z = "-" + combos[cf]
|
||||
}
|
||||
vs = append(vs, x+y+z)
|
||||
}
|
||||
|
||||
// The unnamed bits can only add to the above named ones since
|
||||
// unnamed ones are always defaulted to lowered.
|
||||
uBins := make([]int, 8)
|
||||
uPatterns := make([]uint, 32*words)
|
||||
c.histo(uBins, uPatterns, Value(maxValues), 32*Value(words))
|
||||
for i := uint(7); i > 0; i-- {
|
||||
if uBins[i] == 0 {
|
||||
continue
|
||||
}
|
||||
var list []string
|
||||
for j, p := range uPatterns {
|
||||
if p != i {
|
||||
continue
|
||||
}
|
||||
list = append(list, Value(j).String())
|
||||
}
|
||||
vs = append(vs, strings.Join(list, ",")+"+"+combos[i])
|
||||
}
|
||||
|
||||
return strings.Join(vs, " ")
|
||||
}
|
||||
|
||||
// ErrBadText is returned if the text for a capability set cannot be parsed.
|
||||
var ErrBadText = errors.New("bad text")
|
||||
|
||||
// FromText converts the canonical text representation for a Set into
|
||||
// a freshly allocated Set.
|
||||
//
|
||||
// The format follows the following pattern: a set of space separated
|
||||
// sequences. Each sequence applies over the previous sequence to
|
||||
// build up a Set. The format of a sequence is:
|
||||
//
|
||||
// [comma list of cap_values][[ops][flags]]*
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// "all=ep"
|
||||
// "cap_chown,cap_setuid=ip cap_setuid+e"
|
||||
// "=p cap_setpcap-p+i"
|
||||
//
|
||||
// Here "all" refers to all named capabilities known to the hosting
|
||||
// kernel, and "all" is assumed if no capabilities are listed before
|
||||
// an "=".
|
||||
//
|
||||
// The ops values, "=", "+" and "-" imply "reset and raise", "raise"
|
||||
// and "lower" respectively. The "e", "i" and "p" characters
|
||||
// correspond to the capabilities of the corresponding Flag: "e"
|
||||
// (Effective); "i" (Inheritable); "p" (Permitted).
|
||||
//
|
||||
// This syntax is overspecified and there are many ways of building
|
||||
// the same final Set state. Any sequence that includes a '=' resets
|
||||
// the accumulated state of all Flags ignoring earlier sequences. On
|
||||
// each of the following lines we give three or more examples of ways
|
||||
// to specify a common Set. The last entry on each line is the one
|
||||
// generated by (*cap.Set).String() from that Set.
|
||||
//
|
||||
// "=p all+ei" "all=pie" "=pi all+e" "=eip"
|
||||
//
|
||||
// "cap_setuid=p cap_chown=i" "cap_chown=ip-p" "cap_chown=i"
|
||||
//
|
||||
// "cap_chown=-p" "all=" "cap_setuid=pie-pie" "="
|
||||
//
|
||||
// Note: FromText() is tested at release time to completely match the
|
||||
// import ability of the libcap:cap_from_text() function.
|
||||
func FromText(text string) (*Set, error) {
|
||||
text = strings.ToLower(text)
|
||||
c := NewSet()
|
||||
scanner := bufio.NewScanner(strings.NewReader(text))
|
||||
scanner.Split(bufio.ScanWords)
|
||||
chunks := 0
|
||||
for scanner.Scan() {
|
||||
chunks++
|
||||
|
||||
// Parsing for xxx([-+=][eip]+)+
|
||||
t := scanner.Text()
|
||||
i := strings.IndexAny(t, "=+-")
|
||||
if i < 0 {
|
||||
return nil, fmt.Errorf("%w: needs one of [=+-]", ErrBadText)
|
||||
}
|
||||
var vs []Value
|
||||
sep := t[i]
|
||||
if vals := t[:i]; vals == "all" {
|
||||
for v := Value(0); v < Value(maxValues); v++ {
|
||||
vs = append(vs, v)
|
||||
}
|
||||
} else if vals != "" {
|
||||
for name := range strings.SplitSeq(vals, ",") {
|
||||
v, err := FromName(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrBadText, err)
|
||||
}
|
||||
vs = append(vs, v)
|
||||
}
|
||||
} else if sep != '=' {
|
||||
if vals == "" {
|
||||
// Only "=" supports ""=="all".
|
||||
return nil, ErrBadText
|
||||
}
|
||||
} else if j := i + 1; j+1 < len(t) {
|
||||
switch t[j] {
|
||||
case '+':
|
||||
sep = 'P'
|
||||
i++
|
||||
case '-':
|
||||
sep = 'M'
|
||||
i++
|
||||
}
|
||||
}
|
||||
i++
|
||||
|
||||
// There are 5 ways to set: =, =+, =-, +, -. We call
|
||||
// the 2nd and 3rd of these 'P' and 'M'.
|
||||
|
||||
for {
|
||||
// read [eip]+ setting flags.
|
||||
var fE, fP, fI bool
|
||||
for ok := true; ok && i < len(t); i++ {
|
||||
switch t[i] {
|
||||
case 'e':
|
||||
fE = true
|
||||
case 'i':
|
||||
fI = true
|
||||
case 'p':
|
||||
fP = true
|
||||
default:
|
||||
ok = false
|
||||
}
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !(fE || fI || fP) {
|
||||
if sep != '=' {
|
||||
return nil, fmt.Errorf("%w: needs one of [eip]", ErrBadText)
|
||||
}
|
||||
}
|
||||
|
||||
switch sep {
|
||||
case '=', 'P', 'M', '+':
|
||||
if sep != '+' {
|
||||
c.Clear()
|
||||
if sep == 'M' {
|
||||
break
|
||||
}
|
||||
}
|
||||
if keep := len(vs) == 0; keep {
|
||||
if sep != '=' {
|
||||
return nil, fmt.Errorf("%w: expected an '='", ErrBadText)
|
||||
}
|
||||
c.forceFlag(Effective, fE)
|
||||
c.forceFlag(Permitted, fP)
|
||||
c.forceFlag(Inheritable, fI)
|
||||
break
|
||||
}
|
||||
// =, + and P for specific values are left.
|
||||
if fE {
|
||||
c.SetFlag(Effective, true, vs...)
|
||||
}
|
||||
if fP {
|
||||
c.SetFlag(Permitted, true, vs...)
|
||||
}
|
||||
if fI {
|
||||
c.SetFlag(Inheritable, true, vs...)
|
||||
}
|
||||
case '-':
|
||||
if fE {
|
||||
c.SetFlag(Effective, false, vs...)
|
||||
}
|
||||
if fP {
|
||||
c.SetFlag(Permitted, false, vs...)
|
||||
}
|
||||
if fI {
|
||||
c.SetFlag(Inheritable, false, vs...)
|
||||
}
|
||||
}
|
||||
|
||||
if i == len(t) {
|
||||
break
|
||||
}
|
||||
|
||||
switch t[i] {
|
||||
case '+', '-':
|
||||
sep = t[i]
|
||||
i++
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: expected '+' or '-'", ErrBadText)
|
||||
}
|
||||
}
|
||||
}
|
||||
if chunks == 0 {
|
||||
return nil, fmt.Errorf("%w: No capabilities found", ErrBadText)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
129
internal/eeprom/eeprom.go
Normal file
129
internal/eeprom/eeprom.go
Normal file
@@ -0,0 +1,129 @@
|
||||
// Package eeprom implements the Raspberry Pi EEPROM update file format.
|
||||
package eeprom
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Other implementations:
|
||||
//
|
||||
// - https://github.com/raspberrypi/rpi-eeprom (Python)
|
||||
// - https://github.com/info-beamer/rpi-eeprom-tools (Python)
|
||||
|
||||
const (
|
||||
MAGIC = 0x55aaf00f
|
||||
MAGIC_MASK = 0xfffff00f
|
||||
FILE_MAGIC = 0x55aaf11f // id for modifiable files
|
||||
FILENAME_LEN = 12
|
||||
FILENAME_PADDING = 4
|
||||
|
||||
chunkHeaderLen = 4 + 4 // magic number + 32 bit offset
|
||||
)
|
||||
|
||||
type Section struct {
|
||||
img []byte
|
||||
Magic uint32
|
||||
Offset int
|
||||
Length int
|
||||
Filename string
|
||||
}
|
||||
|
||||
func FileSection(offset int, name string, contents []byte) *Section {
|
||||
if len(name) > FILENAME_LEN {
|
||||
panic(fmt.Sprintf("BUG: file name %s exceeds max FILENAME_LEN = %d", name, FILENAME_LEN))
|
||||
}
|
||||
|
||||
img := bytes.Repeat([]byte{0}, offset+chunkHeaderLen)
|
||||
img = append(img, []byte(name)...)
|
||||
img = append(img, bytes.Repeat([]byte{0}, FILENAME_PADDING+FILENAME_LEN-len(name))...)
|
||||
img = append(img, contents...)
|
||||
return &Section{
|
||||
img: img,
|
||||
Magic: FILE_MAGIC,
|
||||
Offset: offset,
|
||||
Length: len(contents) + FILENAME_LEN + FILENAME_PADDING,
|
||||
Filename: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Section) WithoutImg() Section {
|
||||
without := *s
|
||||
without.img = nil
|
||||
return without
|
||||
}
|
||||
|
||||
func (s *Section) RawContent() []byte {
|
||||
offset := s.Offset + chunkHeaderLen
|
||||
length := s.Length
|
||||
return s.img[offset : offset+length]
|
||||
}
|
||||
|
||||
func (s *Section) FileContent() []byte {
|
||||
offset := s.Offset + chunkHeaderLen
|
||||
length := s.Length
|
||||
if s.Magic == FILE_MAGIC {
|
||||
const fileSkip = FILENAME_LEN + FILENAME_PADDING
|
||||
offset += fileSkip
|
||||
length -= fileSkip
|
||||
}
|
||||
return s.img[offset : offset+length]
|
||||
}
|
||||
|
||||
func Analyze(img []byte) ([]*Section, error) {
|
||||
// See https://github.com/raspberrypi/rpi-eeprom/blob/f38dbcb72341a3c3c3e66f1e10d58f8985cb0528/rpi-eeprom-config#L267
|
||||
|
||||
if len(img) != 512*1024 &&
|
||||
len(img) != 2*1024*1024 {
|
||||
return nil, fmt.Errorf("unexpected EEPROM size: got %d, want 512KB or 2MB", len(img))
|
||||
}
|
||||
|
||||
var sections []*Section
|
||||
for offset := 0; offset+chunkHeaderLen < len(img); {
|
||||
magic := binary.BigEndian.Uint32(img[offset : offset+4])
|
||||
length := binary.BigEndian.Uint32(img[offset+4 : offset+8])
|
||||
if magic == 0 || magic == 0xffffffff {
|
||||
break // end of file
|
||||
}
|
||||
if magic&MAGIC_MASK != MAGIC {
|
||||
return nil, fmt.Errorf("EEPROM is corrupted: %x & %x != %x", magic, MAGIC_MASK, MAGIC)
|
||||
}
|
||||
sect := Section{
|
||||
img: img,
|
||||
Magic: magic,
|
||||
Offset: offset,
|
||||
Length: int(length),
|
||||
}
|
||||
if magic == FILE_MAGIC {
|
||||
sect.Filename = string(img[offset+8 : offset+8+FILENAME_LEN])
|
||||
sect.Filename = strings.ReplaceAll(sect.Filename, "\x00", "")
|
||||
}
|
||||
sections = append(sections, §)
|
||||
offset += chunkHeaderLen + int(length)
|
||||
offset = (offset + 7) &^ 7
|
||||
}
|
||||
if len(sections) == 0 {
|
||||
return nil, fmt.Errorf("invalid EEPROM: no sections found")
|
||||
}
|
||||
if sections[len(sections)-1].Filename != "bootconf.txt" {
|
||||
// “by convention bootconf.txt is the last section”, from:
|
||||
// https://github.com/raspberrypi/rpi-eeprom/blob/f38dbcb72341a3c3c3e66f1e10d58f8985cb0528/rpi-eeprom-config#L373
|
||||
return nil, fmt.Errorf("invalid EEPROM: bootconf.txt not the last section")
|
||||
}
|
||||
return sections, nil
|
||||
}
|
||||
|
||||
func Assemble(sections []*Section) []byte {
|
||||
output := bytes.Repeat([]byte{0xff}, len(sections[0].img))
|
||||
offset := 0
|
||||
for _, sect := range sections {
|
||||
binary.BigEndian.PutUint32(output[offset:], sect.Magic)
|
||||
binary.BigEndian.PutUint32(output[offset+4:], uint32(sect.Length))
|
||||
copy(output[offset+8:], sect.RawContent())
|
||||
offset += chunkHeaderLen + sect.Length
|
||||
offset = (offset + 7) &^ 7
|
||||
}
|
||||
return output
|
||||
}
|
||||
110
internal/eeprom/eeprom_test.go
Normal file
110
internal/eeprom/eeprom_test.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package eeprom_test
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gokrazy/tools/internal/eeprom"
|
||||
)
|
||||
|
||||
func testAgainst(t *testing.T, verify func(eepromPath, needle string), pieeprom, bootUartNeedle string) {
|
||||
verify(pieeprom, bootUartNeedle)
|
||||
|
||||
// Read + Write the EEPROM with our own code,
|
||||
// verify that bootconf.txt can still be displayed.
|
||||
img, err := os.ReadFile(pieeprom)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sections, err := eeprom.Analyze(img)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reassembleAndVerify := func(needle string) {
|
||||
reassembled := filepath.Join(t.TempDir(), "reassembled.bin")
|
||||
if err := os.WriteFile(reassembled, eeprom.Assemble(sections), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verify(reassembled, needle)
|
||||
}
|
||||
reassembleAndVerify(bootUartNeedle)
|
||||
|
||||
bc := sections[len(sections)-1] // guaranteed to be bootconf.txt
|
||||
|
||||
// Re-create the section with the existing contents.
|
||||
sections[len(sections)-1] = eeprom.FileSection(bc.Offset, bc.Filename, bc.FileContent())
|
||||
reassembleAndVerify(bootUartNeedle)
|
||||
|
||||
// Change the bootconf.txt contents.
|
||||
sections[len(sections)-1] = eeprom.FileSection(bc.Offset, bc.Filename, []byte("hello world"))
|
||||
reassembleAndVerify("hello world")
|
||||
}
|
||||
|
||||
func TestAgainst(t *testing.T) {
|
||||
const fn = "testdata/pieeprom-2025-10-17.bin"
|
||||
f, err := os.Open(fn + ".gz")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
gz, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bin, err := io.ReadAll(gz)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pieeprom := filepath.Join(t.TempDir(), filepath.Base(fn))
|
||||
if err := os.WriteFile(pieeprom, bin, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("Infobeamer", func(t *testing.T) {
|
||||
lsBin, err := exec.LookPath("pi-eeprom-ls")
|
||||
if err != nil {
|
||||
t.Skipf("pi-eeprom-ls not available (%v)", err)
|
||||
}
|
||||
|
||||
verify := func(eepromPath, needle string) {
|
||||
ls := exec.Command(lsBin, eepromPath)
|
||||
ls.Stderr = os.Stderr
|
||||
lsOut, err := ls.Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(lsOut), needle) {
|
||||
t.Errorf("infobeamer pi-eeprom-ls output did not contain %q: \n%s", needle, string(lsOut))
|
||||
}
|
||||
}
|
||||
const bootUartNeedle = "-[ bootconf.txt ]------------------\n[all]\nBOOT_UART=1"
|
||||
testAgainst(t, verify, pieeprom, bootUartNeedle)
|
||||
})
|
||||
|
||||
t.Run("Pi", func(t *testing.T) {
|
||||
configBin, err := exec.LookPath("rpi-eeprom-config")
|
||||
if err != nil {
|
||||
t.Skipf("rpi-eeprom-config not available (%v)", err)
|
||||
}
|
||||
|
||||
verify := func(eepromPath, needle string) {
|
||||
extract := exec.Command(configBin, eepromPath)
|
||||
extract.Dir = t.TempDir()
|
||||
extract.Stderr = os.Stderr
|
||||
extractOut, err := extract.Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(extractOut), needle) {
|
||||
t.Errorf("rpi-eeprom-config output did not contain %q: \n%s", needle, string(extractOut))
|
||||
}
|
||||
}
|
||||
const bootUartNeedle = "[all]\nBOOT_UART=1"
|
||||
testAgainst(t, verify, pieeprom, bootUartNeedle)
|
||||
})
|
||||
}
|
||||
BIN
internal/eeprom/testdata/pieeprom-2025-10-17.bin.gz
vendored
Normal file
BIN
internal/eeprom/testdata/pieeprom-2025-10-17.bin.gz
vendored
Normal file
Binary file not shown.
@@ -18,12 +18,13 @@ import (
|
||||
)
|
||||
|
||||
// addCmd is gok add.
|
||||
var addCmd = &cobra.Command{
|
||||
GroupID: "edit",
|
||||
Use: "add [flags] importpath[@version]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Add a Go package to a gokrazy instance",
|
||||
Long: `Add a Go package to a gokrazy instance.
|
||||
func addCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
GroupID: "edit",
|
||||
Use: "add [flags] importpath[@version]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Add a Go package to a gokrazy instance",
|
||||
Long: `Add a Go package to a gokrazy instance.
|
||||
|
||||
This command creates the required build directory, runs go get, and adds
|
||||
the specified package to the gokrazy instance configuration (Packages field).
|
||||
@@ -42,26 +43,25 @@ Examples:
|
||||
% gok -i scan2drive add /home/michael/projects/scanui/cmd/scanui
|
||||
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flags().NArg() != 1 {
|
||||
fmt.Fprint(os.Stderr, `expected Go package name, name@version, or path
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flags().NArg() != 1 {
|
||||
fmt.Fprint(os.Stderr, `expected Go package name, name@version, or path
|
||||
|
||||
`)
|
||||
return cmd.Usage()
|
||||
}
|
||||
return cmd.Usage()
|
||||
}
|
||||
|
||||
return addImpl.run(cmd.Context(), args[0], cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
return addImpl.run(cmd.Context(), args[0], cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
}
|
||||
instanceflag.RegisterPflags(cmd.Flags())
|
||||
return cmd
|
||||
}
|
||||
|
||||
type addImplConfig struct{}
|
||||
|
||||
var addImpl addImplConfig
|
||||
|
||||
func init() {
|
||||
instanceflag.RegisterPflags(addCmd.Flags())
|
||||
}
|
||||
|
||||
type packageInfo struct {
|
||||
// Dir is the directory on the local disk containing the package sources,
|
||||
// e.g. /home/michael/projects/stapelberg/localmod/cmd/sup.
|
||||
|
||||
@@ -13,32 +13,32 @@ import (
|
||||
)
|
||||
|
||||
// editCmd is gok edit.
|
||||
var editCmd = &cobra.Command{
|
||||
GroupID: "edit",
|
||||
Use: "edit",
|
||||
Short: "Edit a gokrazy instance configuration interactively",
|
||||
Long: `Edit a gokrazy instance configuration interactively.
|
||||
func editCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
GroupID: "edit",
|
||||
Use: "edit",
|
||||
Short: "Edit a gokrazy instance configuration interactively",
|
||||
Long: `Edit a gokrazy instance configuration interactively.
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flags().NArg() > 0 {
|
||||
fmt.Fprint(os.Stderr, `positional arguments are not supported
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flags().NArg() > 0 {
|
||||
fmt.Fprint(os.Stderr, `positional arguments are not supported
|
||||
|
||||
`)
|
||||
return cmd.Usage()
|
||||
}
|
||||
return cmd.Usage()
|
||||
}
|
||||
|
||||
return editImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
return editImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
}
|
||||
instanceflag.RegisterPflags(cmd.Flags())
|
||||
return cmd
|
||||
}
|
||||
|
||||
type editImplConfig struct{}
|
||||
|
||||
var editImpl editImplConfig
|
||||
|
||||
func init() {
|
||||
instanceflag.RegisterPflags(editCmd.Flags())
|
||||
}
|
||||
|
||||
func (r *editImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||
parentDir := instanceflag.ParentDir()
|
||||
instance := instanceflag.Instance()
|
||||
|
||||
@@ -11,17 +11,17 @@ import (
|
||||
|
||||
"github.com/gokrazy/internal/config"
|
||||
"github.com/gokrazy/internal/instanceflag"
|
||||
"github.com/gokrazy/internal/updateflag"
|
||||
"github.com/gokrazy/tools/packer"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// getCmd is gok get.
|
||||
var getCmd = &cobra.Command{
|
||||
GroupID: "edit",
|
||||
Use: "get",
|
||||
Short: "Update the version of your Go program(s) using `go get`",
|
||||
Long: "gok get runs `go get` to update the version of the specified Go programs" + `
|
||||
func getCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
GroupID: "edit",
|
||||
Use: "get",
|
||||
Short: "Update the version of your Go program(s) using `go get`",
|
||||
Long: "gok get runs `go get` to update the version of the specified Go programs" + `
|
||||
in your gokrazy instance.
|
||||
|
||||
Examples:
|
||||
@@ -34,9 +34,13 @@ Examples:
|
||||
# Update only gokrazy system packages
|
||||
% gok -i scanner get gokrazy
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return getImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return getImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVarP(&getImpl.updateAll, "update_all", "u", false, "update all installed packages and gokrazy system packages")
|
||||
instanceflag.RegisterPflags(cmd.Flags())
|
||||
return cmd
|
||||
}
|
||||
|
||||
type getImplConfig struct {
|
||||
@@ -45,11 +49,6 @@ type getImplConfig struct {
|
||||
|
||||
var getImpl getImplConfig
|
||||
|
||||
func init() {
|
||||
getCmd.Flags().BoolVarP(&getImpl.updateAll, "update_all", "u", false, "update all installed packages and gokrazy system packages")
|
||||
instanceflag.RegisterPflags(getCmd.Flags())
|
||||
}
|
||||
|
||||
func getGokrazySystemPackages(cfg *config.Struct) []string {
|
||||
pkgs := append([]string{}, cfg.GokrazyPackagesOrDefault()...)
|
||||
pkgs = append(pkgs, packer.InitDeps(cfg.InternalCompatibilityFlags.InitPkg)...)
|
||||
@@ -78,8 +77,6 @@ func (r *getImplConfig) run(ctx context.Context, args []string, stdout, stderr i
|
||||
return err
|
||||
}
|
||||
|
||||
updateflag.SetUpdate("yes")
|
||||
|
||||
packages := args
|
||||
if r.updateAll {
|
||||
if len(packages) > 0 {
|
||||
|
||||
@@ -20,15 +20,20 @@ import (
|
||||
)
|
||||
|
||||
// logsCmd is gok logs.
|
||||
var logsCmd = &cobra.Command{
|
||||
GroupID: "runtime",
|
||||
Use: "logs",
|
||||
Short: "Stream logs from a running gokrazy service",
|
||||
Long: `Display the most recent 100 log lines from stdout and stderr each,
|
||||
func logsCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
GroupID: "runtime",
|
||||
Use: "logs",
|
||||
Short: "Stream logs from a running gokrazy service",
|
||||
Long: `Display the most recent 100 log lines from stdout and stderr each,
|
||||
and any new lines the gokrazy service produces (cancel any time with Ctrl-C)`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return logsImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return logsImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&logsImpl.service, "service", "s", "", "gokrazy service to fetch logs for")
|
||||
instanceflag.RegisterPflags(cmd.Flags())
|
||||
return cmd
|
||||
}
|
||||
|
||||
type logsImplConfig struct {
|
||||
@@ -37,11 +42,6 @@ type logsImplConfig struct {
|
||||
|
||||
var logsImpl logsImplConfig
|
||||
|
||||
func init() {
|
||||
logsCmd.Flags().StringVarP(&logsImpl.service, "service", "s", "", "gokrazy service to fetch logs for")
|
||||
instanceflag.RegisterPflags(logsCmd.Flags())
|
||||
}
|
||||
|
||||
func (l *logsImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||
cfg, err := config.ApplyInstanceFlag()
|
||||
if err != nil {
|
||||
@@ -53,13 +53,11 @@ func (l *logsImplConfig) run(ctx context.Context, args []string, stdout, stderr
|
||||
}
|
||||
}
|
||||
|
||||
updateflag.SetUpdate("yes")
|
||||
|
||||
if l.service == "" {
|
||||
return fmt.Errorf("the -service flag is empty, but required")
|
||||
}
|
||||
|
||||
httpClient, _, logsUrl, err := httpclient.For(cfg)
|
||||
httpClient, _, logsUrl, err := httpclient.For(updateflag.Value{Update: "yes"}, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -16,25 +16,30 @@ import (
|
||||
)
|
||||
|
||||
// newCmd is gok new.
|
||||
var newCmd = &cobra.Command{
|
||||
GroupID: "edit",
|
||||
Use: "new",
|
||||
Short: "Create a new gokrazy instance",
|
||||
Long: `Create a new gokrazy instance.
|
||||
func newCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
GroupID: "edit",
|
||||
Use: "new",
|
||||
Short: "Create a new gokrazy instance",
|
||||
Long: `Create a new gokrazy instance.
|
||||
|
||||
If you are unfamiliar with gokrazy, please follow:
|
||||
https://gokrazy.org/quickstart/
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flags().NArg() > 0 {
|
||||
fmt.Fprint(os.Stderr, `positional arguments are not supported
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flags().NArg() > 0 {
|
||||
fmt.Fprint(os.Stderr, `positional arguments are not supported
|
||||
|
||||
`)
|
||||
return cmd.Usage()
|
||||
}
|
||||
return cmd.Usage()
|
||||
}
|
||||
|
||||
return newImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
return newImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
}
|
||||
instanceflag.RegisterPflags(cmd.Flags())
|
||||
cmd.Flags().BoolVarP(&newImpl.empty, "empty", "", false, "create an empty gokrazy instance, without the default packages")
|
||||
return cmd
|
||||
}
|
||||
|
||||
type newImplConfig struct {
|
||||
@@ -43,11 +48,6 @@ type newImplConfig struct {
|
||||
|
||||
var newImpl newImplConfig
|
||||
|
||||
func init() {
|
||||
instanceflag.RegisterPflags(newCmd.Flags())
|
||||
newCmd.Flags().BoolVarP(&newImpl.empty, "empty", "", false, "create an empty gokrazy instance, without the default packages")
|
||||
}
|
||||
|
||||
func (r *newImplConfig) createBreakglassAuthorizedKeys(authorizedPath string, matches []string) error {
|
||||
f, err := os.OpenFile(authorizedPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
|
||||
if err != nil {
|
||||
|
||||
@@ -15,11 +15,12 @@ import (
|
||||
)
|
||||
|
||||
// overwriteCmd is gok overwrite.
|
||||
var overwriteCmd = &cobra.Command{
|
||||
GroupID: "deploy",
|
||||
Use: "overwrite",
|
||||
Short: "Build and deploy a gokrazy instance to a storage device",
|
||||
Long: `Build and deploy a gokrazy instance to a storage device.
|
||||
func overwriteCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
GroupID: "deploy",
|
||||
Use: "overwrite",
|
||||
Short: "Build and deploy a gokrazy instance to a storage device",
|
||||
Long: `Build and deploy a gokrazy instance to a storage device.
|
||||
|
||||
You typically need to use the gok overwrite command only once,
|
||||
when first deploying your gokrazy instance. Afterwards, you can
|
||||
@@ -29,16 +30,27 @@ Examples:
|
||||
# Overwrite the contents of the SD card sdx with gokrazy instance scan2drive:
|
||||
% gok -i scan2drive overwrite --full=/dev/sdx
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flags().NArg() > 0 {
|
||||
fmt.Fprint(os.Stderr, `positional arguments are not supported
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flags().NArg() > 0 {
|
||||
fmt.Fprint(os.Stderr, `positional arguments are not supported
|
||||
|
||||
`)
|
||||
return cmd.Usage()
|
||||
}
|
||||
return cmd.Usage()
|
||||
}
|
||||
|
||||
return overwriteImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
return overwriteImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
}
|
||||
instanceflag.RegisterPflags(cmd.Flags())
|
||||
cmd.Flags().StringVarP(&overwriteImpl.full, "full", "", "", "write a full gokrazy device image to the specified device (e.g. /dev/sdx) or path (e.g. /tmp/gokrazy.img)")
|
||||
cmd.Flags().StringVarP(&overwriteImpl.gaf, "gaf", "", "", "write a .gaf (gokrazy archive format) file to the specified path (e.g. /tmp/gokrazy.gaf)")
|
||||
cmd.Flags().StringVarP(&overwriteImpl.boot, "boot", "", "", "write the gokrazy boot file system to the specified partition (e.g. /dev/sdx1) or path (e.g. /tmp/boot.fat)")
|
||||
cmd.Flags().StringVarP(&overwriteImpl.root, "root", "", "", "write the gokrazy root file system to the specified partition (e.g. /dev/sdx2) or path (e.g. /tmp/root.squashfs)")
|
||||
cmd.Flags().StringVarP(&overwriteImpl.mbr, "mbr", "", "", "write the gokrazy master boot record (MBR) to the specified device (e.g. /dev/sdx) or path (e.g. /tmp/mbr.img). only effective if -boot is specified, too")
|
||||
cmd.Flags().StringVarP(&overwriteImpl.sudo, "sudo", "", "", "Whether to elevate privileges using sudo when required (one of auto, always, never, default auto)")
|
||||
cmd.Flags().IntVarP(&overwriteImpl.targetStorageBytes, "target_storage_bytes", "", 0, "Number of bytes which the target storage device (SD card) has. Required for using -full=<file>")
|
||||
cmd.Flags().StringVarP(&overwriteImpl.traceFile, "trace_file", "", "", "If non-empty, write a Go runtime/trace to this file (for performance analysis)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
type overwriteImplConfig struct {
|
||||
@@ -56,18 +68,6 @@ type overwriteImplConfig struct {
|
||||
|
||||
var overwriteImpl overwriteImplConfig
|
||||
|
||||
func init() {
|
||||
instanceflag.RegisterPflags(overwriteCmd.Flags())
|
||||
overwriteCmd.Flags().StringVarP(&overwriteImpl.full, "full", "", "", "write a full gokrazy device image to the specified device (e.g. /dev/sdx) or path (e.g. /tmp/gokrazy.img)")
|
||||
overwriteCmd.Flags().StringVarP(&overwriteImpl.gaf, "gaf", "", "", "write a .gaf (gokrazy archive format) file to the specified path (e.g. /tmp/gokrazy.gaf)")
|
||||
overwriteCmd.Flags().StringVarP(&overwriteImpl.boot, "boot", "", "", "write the gokrazy boot file system to the specified partition (e.g. /dev/sdx1) or path (e.g. /tmp/boot.fat)")
|
||||
overwriteCmd.Flags().StringVarP(&overwriteImpl.root, "root", "", "", "write the gokrazy root file system to the specified partition (e.g. /dev/sdx2) or path (e.g. /tmp/root.squashfs)")
|
||||
overwriteCmd.Flags().StringVarP(&overwriteImpl.mbr, "mbr", "", "", "write the gokrazy master boot record (MBR) to the specified device (e.g. /dev/sdx) or path (e.g. /tmp/mbr.img). only effective if -boot is specified, too")
|
||||
overwriteCmd.Flags().StringVarP(&overwriteImpl.sudo, "sudo", "", "", "Whether to elevate privileges using sudo when required (one of auto, always, never, default auto)")
|
||||
overwriteCmd.Flags().IntVarP(&overwriteImpl.targetStorageBytes, "target_storage_bytes", "", 0, "Number of bytes which the target storage device (SD card) has. Required for using -full=<file>")
|
||||
overwriteCmd.Flags().StringVarP(&overwriteImpl.traceFile, "trace_file", "", "", "If non-empty, write a Go runtime/trace to this file (for performance analysis)")
|
||||
}
|
||||
|
||||
func (r *overwriteImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||
if r.traceFile != "" {
|
||||
out, err := os.Create(r.traceFile)
|
||||
@@ -151,7 +151,7 @@ func (r *overwriteImplConfig) run(ctx context.Context, args []string, stdout, st
|
||||
Output: &output,
|
||||
}
|
||||
|
||||
pack.Main("gokrazy gok")
|
||||
pack.Main(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,18 +14,22 @@ import (
|
||||
)
|
||||
|
||||
// psCmd is gok ps.
|
||||
var psCmd = &cobra.Command{
|
||||
GroupID: "runtime",
|
||||
Use: "ps",
|
||||
Short: "list processes of a running gokrazy instance",
|
||||
Long: `gok ps
|
||||
func psCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
GroupID: "runtime",
|
||||
Use: "ps",
|
||||
Short: "list processes of a running gokrazy instance",
|
||||
Long: `gok ps
|
||||
|
||||
Examples:
|
||||
% gok -i scan2drive ps
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return psImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return psImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
}
|
||||
instanceflag.RegisterPflags(cmd.Flags())
|
||||
return cmd
|
||||
}
|
||||
|
||||
type psImplConfig struct {
|
||||
@@ -33,10 +37,6 @@ type psImplConfig struct {
|
||||
|
||||
var psImpl psImplConfig
|
||||
|
||||
func init() {
|
||||
instanceflag.RegisterPflags(psCmd.Flags())
|
||||
}
|
||||
|
||||
func (r *psImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||
cfg, err := config.ApplyInstanceFlag()
|
||||
if err != nil {
|
||||
|
||||
@@ -15,11 +15,12 @@ import (
|
||||
)
|
||||
|
||||
// pushCmd is gok push.
|
||||
var pushCmd = &cobra.Command{
|
||||
GroupID: "server",
|
||||
Use: "push",
|
||||
Short: "Push a gokrazy image to a remote GUS server",
|
||||
Long: `gok push pushes a local gaf (gokrazy archive format) file to a remote server.
|
||||
func pushCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
GroupID: "server",
|
||||
Use: "push",
|
||||
Short: "Push a gokrazy image to a remote GUS server",
|
||||
Long: `gok push pushes a local gaf (gokrazy archive format) file to a remote server.
|
||||
|
||||
When the --json flag is specified, the server response is printed to stdout.
|
||||
|
||||
@@ -28,9 +29,15 @@ Examples:
|
||||
% gok push --gaf /tmp/gokrazy.gaf --server https://gus.gokrazy.org
|
||||
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return pushImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return pushImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&pushImpl.gafPath, "gaf", "", "", "path to the .gaf (gokrazy archive format) file to push to GUS (e.g. /tmp/gokrazy.gaf); build using gok overwrite --gaf")
|
||||
cmd.Flags().StringVarP(&pushImpl.server, "server", "", "", "HTTP(S) URL to the server to push to")
|
||||
cmd.Flags().BoolVarP(&pushImpl.json, "json", "", false, "print server JSON response directly to stdout")
|
||||
instanceflag.RegisterPflags(cmd.Flags())
|
||||
return cmd
|
||||
}
|
||||
|
||||
type pushConfig struct {
|
||||
@@ -41,13 +48,6 @@ type pushConfig struct {
|
||||
|
||||
var pushImpl pushConfig
|
||||
|
||||
func init() {
|
||||
pushCmd.Flags().StringVarP(&pushImpl.gafPath, "gaf", "", "", "path to the .gaf (gokrazy archive format) file to push to GUS (e.g. /tmp/gokrazy.gaf); build using gok overwrite --gaf")
|
||||
pushCmd.Flags().StringVarP(&pushImpl.server, "server", "", "", "HTTP(S) URL to the server to push to")
|
||||
pushCmd.Flags().BoolVarP(&pushImpl.json, "json", "", false, "print server JSON response directly to stdout")
|
||||
instanceflag.RegisterPflags(pushCmd.Flags())
|
||||
}
|
||||
|
||||
func (r *pushConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||
// TODO: use an io.Reader that allows us to indicate progress
|
||||
body, err := os.Open(r.gafPath)
|
||||
|
||||
@@ -9,10 +9,11 @@ import (
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "gok",
|
||||
Short: "top-level CLI entry point for all things gokrazy",
|
||||
Long: `The gok tool is your main entrypoint to gokrazy and allows you to:
|
||||
func RootCmd() *cobra.Command {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "gok",
|
||||
Short: "top-level CLI entry point for all things gokrazy",
|
||||
Long: `The gok tool is your main entrypoint to gokrazy and allows you to:
|
||||
|
||||
1. Create new gokrazy instances (gok new),
|
||||
2. Deploy gokrazy instances to storage devices like SD cards (gok overwrite),
|
||||
@@ -22,59 +23,58 @@ var RootCmd = &cobra.Command{
|
||||
If you are unfamiliar with gokrazy, please follow:
|
||||
https://gokrazy.org/quickstart/
|
||||
`,
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
versionVal, err := cmd.Flags().GetBool("version")
|
||||
if err != nil {
|
||||
return fmt.Errorf("BUG: version flag declared as non-bool")
|
||||
}
|
||||
if versionVal {
|
||||
fmt.Println(version.Read())
|
||||
return nil
|
||||
}
|
||||
return pflag.ErrHelp
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddGroup(&cobra.Group{
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
versionVal, err := cmd.Flags().GetBool("version")
|
||||
if err != nil {
|
||||
return fmt.Errorf("BUG: version flag declared as non-bool")
|
||||
}
|
||||
if versionVal {
|
||||
fmt.Println(version.Read())
|
||||
return nil
|
||||
}
|
||||
return pflag.ErrHelp
|
||||
},
|
||||
}
|
||||
rootCmd.AddGroup(&cobra.Group{
|
||||
ID: "edit",
|
||||
Title: "Commands to create and edit a gokrazy instance:",
|
||||
})
|
||||
RootCmd.AddGroup(&cobra.Group{
|
||||
rootCmd.AddGroup(&cobra.Group{
|
||||
ID: "deploy",
|
||||
Title: "Commands to deploy and update a gokrazy instance:",
|
||||
})
|
||||
RootCmd.AddGroup(&cobra.Group{
|
||||
rootCmd.AddGroup(&cobra.Group{
|
||||
ID: "runtime",
|
||||
Title: "Commands to work with a running gokrazy instance:",
|
||||
})
|
||||
RootCmd.AddGroup(&cobra.Group{
|
||||
rootCmd.AddGroup(&cobra.Group{
|
||||
ID: "server",
|
||||
Title: "Commands to work with a remote GUS server:",
|
||||
})
|
||||
RootCmd.AddGroup(&cobra.Group{
|
||||
rootCmd.AddGroup(&cobra.Group{
|
||||
ID: "vm",
|
||||
Title: "Commands to work with Virtual Machines (VMs):",
|
||||
})
|
||||
RootCmd.Flags().Bool("version", false, "print gok version")
|
||||
rootCmd.Flags().Bool("version", false, "print gok version")
|
||||
// Only defined so that it appears in documentation like --help.
|
||||
//
|
||||
// Cobra only parses local flags on the target command, but they can appear
|
||||
// at any place in the command line (before or after the verb).
|
||||
instanceflag.RegisterPflags(RootCmd.Flags())
|
||||
RootCmd.AddCommand(runCmd)
|
||||
RootCmd.AddCommand(logsCmd)
|
||||
RootCmd.AddCommand(updateCmd)
|
||||
RootCmd.AddCommand(overwriteCmd)
|
||||
RootCmd.AddCommand(versionCmd)
|
||||
RootCmd.AddCommand(newCmd)
|
||||
RootCmd.AddCommand(editCmd)
|
||||
RootCmd.AddCommand(addCmd)
|
||||
RootCmd.AddCommand(getCmd)
|
||||
RootCmd.AddCommand(sbomCmd)
|
||||
RootCmd.AddCommand(pushCmd)
|
||||
RootCmd.AddCommand(vmCmd)
|
||||
RootCmd.AddCommand(psCmd)
|
||||
instanceflag.RegisterPflags(rootCmd.Flags())
|
||||
rootCmd.AddCommand(runCmd())
|
||||
rootCmd.AddCommand(logsCmd())
|
||||
rootCmd.AddCommand(updateCmd())
|
||||
rootCmd.AddCommand(overwriteCmd())
|
||||
rootCmd.AddCommand(versionCmd())
|
||||
rootCmd.AddCommand(newCmd())
|
||||
rootCmd.AddCommand(editCmd())
|
||||
rootCmd.AddCommand(addCmd())
|
||||
rootCmd.AddCommand(getCmd())
|
||||
rootCmd.AddCommand(sbomCmd())
|
||||
rootCmd.AddCommand(pushCmd())
|
||||
rootCmd.AddCommand(vmCmd())
|
||||
rootCmd.AddCommand(psCmd())
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
@@ -22,11 +22,12 @@ import (
|
||||
)
|
||||
|
||||
// runCmd is gok run.
|
||||
var runCmd = &cobra.Command{
|
||||
GroupID: "runtime",
|
||||
Use: "run",
|
||||
Short: "`go install` and run on a running gokrazy instance",
|
||||
Long: "gok run uses `go install` to build the Go program in the current directory," + `
|
||||
func runCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
GroupID: "runtime",
|
||||
Use: "run",
|
||||
Short: "`go install` and run on a running gokrazy instance",
|
||||
Long: "gok run uses `go install` to build the Go program in the current directory," + `
|
||||
then it stores the program in RAM of a running gokrazy instance and runs the program.
|
||||
|
||||
This enables a quick feedback loop when working on a program that is running on gokrazy,
|
||||
@@ -42,9 +43,13 @@ Examples:
|
||||
# specify extra flags on the command line
|
||||
% gok -i scan2drive run -- -tls_autocert_hosts=scan.example.com
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVarP(&runImpl.keep, "keep", "k", false, "keep temporary binary")
|
||||
instanceflag.RegisterPflags(cmd.Flags())
|
||||
return cmd
|
||||
}
|
||||
|
||||
type runImplConfig struct {
|
||||
@@ -53,11 +58,6 @@ type runImplConfig struct {
|
||||
|
||||
var runImpl runImplConfig
|
||||
|
||||
func init() {
|
||||
runCmd.Flags().BoolVarP(&runImpl.keep, "keep", "k", false, "keep temporary binary")
|
||||
instanceflag.RegisterPflags(runCmd.Flags())
|
||||
}
|
||||
|
||||
func (r *runImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||
cfg, err := config.ApplyInstanceFlag()
|
||||
if err != nil {
|
||||
@@ -69,8 +69,6 @@ func (r *runImplConfig) run(ctx context.Context, args []string, stdout, stderr i
|
||||
}
|
||||
}
|
||||
|
||||
updateflag.SetUpdate("yes")
|
||||
|
||||
var tmp string
|
||||
if r.keep {
|
||||
tmp = os.TempDir()
|
||||
@@ -122,12 +120,12 @@ func (r *runImplConfig) run(ctx context.Context, args []string, stdout, stderr i
|
||||
return err
|
||||
}
|
||||
|
||||
httpClient, _, updateBaseUrl, err := httpclient.For(cfg)
|
||||
httpClient, _, updateBaseUrl, err := httpclient.For(updateflag.Value{Update: "yes"}, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target, err := updater.NewTarget(updateBaseUrl.String(), httpClient)
|
||||
target, err := updater.NewTarget(ctx, updateBaseUrl.String(), httpClient)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking target partuuid support: %v", err)
|
||||
}
|
||||
@@ -153,7 +151,7 @@ func (r *runImplConfig) run(ctx context.Context, args []string, stdout, stderr i
|
||||
|
||||
{
|
||||
start := time.Now()
|
||||
err := target.Put("uploadtemp/gok-run/"+basename, io.TeeReader(f, &progress.Writer{}))
|
||||
err := target.Put(ctx, "uploadtemp/gok-run/"+basename, io.TeeReader(f, &progress.Writer{}))
|
||||
if err != nil {
|
||||
return fmt.Errorf("uploading temporary binary: %v", err)
|
||||
}
|
||||
@@ -171,6 +169,7 @@ func (r *runImplConfig) run(ctx context.Context, args []string, stdout, stderr i
|
||||
// /user/<basename>. Includes an automatic service restart.
|
||||
{
|
||||
err := target.Divert(
|
||||
ctx,
|
||||
"/user/"+basename,
|
||||
"gok-run/"+basename,
|
||||
cfg.PackageConfig[importPath].CommandLineFlags,
|
||||
|
||||
@@ -10,17 +10,17 @@ import (
|
||||
|
||||
"github.com/gokrazy/internal/config"
|
||||
"github.com/gokrazy/internal/instanceflag"
|
||||
"github.com/gokrazy/internal/updateflag"
|
||||
"github.com/gokrazy/tools/internal/packer"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// sbomCmd is gok sbom.
|
||||
var sbomCmd = &cobra.Command{
|
||||
GroupID: "deploy",
|
||||
Use: "sbom",
|
||||
Short: "Print the Software Bill Of Materials of a gokrazy instance",
|
||||
Long: `gok sbom generates an SBOM of what gok overwrite or gok update would build
|
||||
func sbomCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
GroupID: "deploy",
|
||||
Use: "sbom",
|
||||
Short: "Print the Software Bill Of Materials of a gokrazy instance",
|
||||
Long: `gok sbom generates an SBOM of what gok overwrite or gok update would build
|
||||
|
||||
Examples:
|
||||
# print the hash and SBOM contents in JSON format
|
||||
@@ -30,9 +30,13 @@ Examples:
|
||||
% gok -i scanner sbom --format hash
|
||||
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return sbomImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return sbomImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&sbomImpl.format, "format", "", "json", "output format. one of json or hash")
|
||||
instanceflag.RegisterPflags(cmd.Flags())
|
||||
return cmd
|
||||
}
|
||||
|
||||
type sbomConfig struct {
|
||||
@@ -41,11 +45,6 @@ type sbomConfig struct {
|
||||
|
||||
var sbomImpl sbomConfig
|
||||
|
||||
func init() {
|
||||
sbomCmd.Flags().StringVarP(&sbomImpl.format, "format", "", "json", "output format. one of json or hash")
|
||||
instanceflag.RegisterPflags(sbomCmd.Flags())
|
||||
}
|
||||
|
||||
func (r *sbomConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||
fileCfg, err := config.ApplyInstanceFlag()
|
||||
if err != nil {
|
||||
@@ -66,8 +65,6 @@ func (r *sbomConfig) run(ctx context.Context, args []string, stdout, stderr io.W
|
||||
return err
|
||||
}
|
||||
|
||||
updateflag.SetUpdate("yes")
|
||||
|
||||
var buf bytes.Buffer
|
||||
pack := &packer.Pack{
|
||||
// Send all build output to stderr so that stdout
|
||||
@@ -80,7 +77,7 @@ func (r *sbomConfig) run(ctx context.Context, args []string, stdout, stderr io.W
|
||||
Cfg: cfg,
|
||||
}
|
||||
|
||||
sbomMarshaled, sbomWithHash, err := pack.GenerateSBOM()
|
||||
sbomMarshaled, sbomWithHash, err := pack.GenerateSBOM(ctx)
|
||||
if os.IsNotExist(err) {
|
||||
// Common case, handle with a good error message
|
||||
os.Stderr.WriteString("\n")
|
||||
|
||||
@@ -13,22 +13,28 @@ import (
|
||||
)
|
||||
|
||||
// updateCmd is gok update.
|
||||
var updateCmd = &cobra.Command{
|
||||
GroupID: "deploy",
|
||||
Use: "update",
|
||||
Short: "Build a gokrazy instance and update over the network",
|
||||
Long: `Build a gokrazy instance and update over the network.
|
||||
func updateCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
GroupID: "deploy",
|
||||
Use: "update",
|
||||
Short: "Build a gokrazy instance and update over the network",
|
||||
Long: `Build a gokrazy instance and update over the network.
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flags().NArg() > 0 {
|
||||
fmt.Fprint(os.Stderr, `positional arguments are not supported
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flags().NArg() > 0 {
|
||||
fmt.Fprint(os.Stderr, `positional arguments are not supported
|
||||
|
||||
`)
|
||||
return cmd.Usage()
|
||||
}
|
||||
return cmd.Usage()
|
||||
}
|
||||
|
||||
return updateImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
return updateImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
}
|
||||
instanceflag.RegisterPflags(cmd.Flags())
|
||||
cmd.Flags().BoolVarP(&updateImpl.insecure, "insecure", "", false, "Disable TLS stripping detection. Should only be used when first enabling TLS, not permanently.")
|
||||
cmd.Flags().BoolVarP(&updateImpl.testboot, "testboot", "", false, "Trigger a testboot instead of switching to the new root partition directly")
|
||||
return cmd
|
||||
}
|
||||
|
||||
type updateImplConfig struct {
|
||||
@@ -38,12 +44,6 @@ type updateImplConfig struct {
|
||||
|
||||
var updateImpl updateImplConfig
|
||||
|
||||
func init() {
|
||||
instanceflag.RegisterPflags(updateCmd.Flags())
|
||||
updateCmd.Flags().BoolVarP(&updateImpl.insecure, "insecure", "", false, "Disable TLS stripping detection. Should only be used when first enabling TLS, not permanently.")
|
||||
updateCmd.Flags().BoolVarP(&updateImpl.testboot, "testboot", "", false, "Trigger a testboot instead of switching to the new root partition directly")
|
||||
}
|
||||
|
||||
func (r *updateImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||
fileCfg, err := config.ApplyInstanceFlag()
|
||||
if err != nil {
|
||||
@@ -90,7 +90,7 @@ func (r *updateImplConfig) run(ctx context.Context, args []string, stdout, stder
|
||||
Cfg: cfg,
|
||||
}
|
||||
|
||||
pack.Main("gokrazy gok")
|
||||
pack.Main(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -10,13 +10,16 @@ import (
|
||||
)
|
||||
|
||||
// versionCmd is gok version.
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print gok version",
|
||||
Long: `Print gok version`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return versionImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
func versionCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print gok version",
|
||||
Long: `Print gok version`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return versionImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
type versionImplConfig struct{}
|
||||
|
||||
@@ -5,11 +5,15 @@ import (
|
||||
)
|
||||
|
||||
// vmCmd is the gok vm subcommand, which (only) has nested commands like run.
|
||||
var vmCmd = &cobra.Command{
|
||||
GroupID: "vm",
|
||||
Use: "vm",
|
||||
Short: "virtual machine",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cmd.Usage()
|
||||
},
|
||||
func vmCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
GroupID: "vm",
|
||||
Use: "vm",
|
||||
Short: "virtual machine",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cmd.Usage()
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(vmRunCmd())
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -18,10 +18,11 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var vmRunCmd = &cobra.Command{
|
||||
Use: "run",
|
||||
Short: "run a virtual machine (using QEMU)",
|
||||
Long: `gok run builds a gokrazy instance and runs it using QEMU.
|
||||
func vmRunCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "run",
|
||||
Short: "run a virtual machine (using QEMU)",
|
||||
Long: `gok run builds a gokrazy instance and runs it using QEMU.
|
||||
|
||||
Extra arguments are passed to QEMU as-is.
|
||||
|
||||
@@ -35,13 +36,20 @@ Examples:
|
||||
# Directly specify QEMU USB flags
|
||||
% gok vm run -- -usb -device usb-mouse
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return vmRunImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
vmCmd.AddCommand(vmRunCmd)
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return vmRunImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&vmRunImpl.sudo, "sudo", "", "", "Whether to elevate privileges using sudo when required (one of auto, always, never, default auto)")
|
||||
const permSize = 512 * 1024 * 1024
|
||||
cmd.Flags().IntVarP(&vmRunImpl.targetStorageBytes, "target_storage_bytes", "", 1258299392+permSize, "Size of the disk image in bytes")
|
||||
cmd.Flags().StringVarP(&vmRunImpl.arch, "arch", "", "", "architecture for which to build and run QEMU. One of 'amd64' or 'arm64'")
|
||||
cmd.Flags().StringVarP(&vmRunImpl.netdev, "netdev", "", "user,id=net0,hostfwd=tcp::8080-:80,hostfwd=tcp::8022-:22", "QEMU -netdev argument")
|
||||
cmd.Flags().BoolVarP(&vmRunImpl.keep, "keep", "", false, "keep ephemeral disk images around instead of deleting them when QEMU exits")
|
||||
cmd.Flags().BoolVarP(&vmRunImpl.dry, "dryrun", "", false, "Whether to actually run QEMU or merely print the command")
|
||||
cmd.Flags().BoolVarP(&vmRunImpl.graphic, "graphic", "", true, "Run QEMU in graphical mode?")
|
||||
instanceflag.RegisterPflags(cmd.Flags())
|
||||
return cmd
|
||||
}
|
||||
|
||||
type vmRunConfig struct {
|
||||
@@ -64,23 +72,7 @@ func (r *vmRunConfig) effectiveGoarch() string {
|
||||
|
||||
var vmRunImpl vmRunConfig
|
||||
|
||||
func init() {
|
||||
vmRunCmd.Flags().StringVarP(&vmRunImpl.sudo, "sudo", "", "", "Whether to elevate privileges using sudo when required (one of auto, always, never, default auto)")
|
||||
const permSize = 512 * 1024 * 1024
|
||||
vmRunCmd.Flags().IntVarP(&vmRunImpl.targetStorageBytes, "target_storage_bytes", "", 1258299392+permSize, "Size of the disk image in bytes")
|
||||
vmRunCmd.Flags().StringVarP(&vmRunImpl.arch, "arch", "", "", "architecture for which to build and run QEMU. One of 'amd64' or 'arm64'")
|
||||
vmRunCmd.Flags().StringVarP(&vmRunImpl.netdev, "netdev", "", "user,id=net0,hostfwd=tcp::8080-:80,hostfwd=tcp::8022-:22", "QEMU -netdev argument")
|
||||
vmRunCmd.Flags().BoolVarP(&vmRunImpl.keep, "keep", "", false, "keep ephemeral disk images around instead of deleting them when QEMU exits")
|
||||
vmRunCmd.Flags().BoolVarP(&vmRunImpl.dry, "dryrun", "", false, "Whether to actually run QEMU or merely print the command")
|
||||
vmRunCmd.Flags().BoolVarP(&vmRunImpl.graphic, "graphic", "", true, "Run QEMU in graphical mode?")
|
||||
instanceflag.RegisterPflags(vmRunCmd.Flags())
|
||||
}
|
||||
|
||||
func (r *vmRunConfig) buildFullDiskImage(ctx context.Context, dest string) error {
|
||||
fileCfg, err := config.ApplyInstanceFlag()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func (r *vmRunConfig) buildFullDiskImage(ctx context.Context, dest string, fileCfg *config.Struct) error {
|
||||
|
||||
if r.arch != "" {
|
||||
os.Setenv("GOARCH", r.arch)
|
||||
@@ -137,7 +129,7 @@ func (r *vmRunConfig) buildFullDiskImage(ctx context.Context, dest string) error
|
||||
Output: &output,
|
||||
}
|
||||
|
||||
pack.Main("gokrazy gok")
|
||||
pack.Main(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -231,6 +223,22 @@ func (r *vmRunConfig) runQEMU(ctx context.Context, fullDiskImage string, extraAr
|
||||
}
|
||||
|
||||
func (r *vmRunConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||
fileCfg, err := config.ApplyInstanceFlag()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fileCfg.SerialConsole == "disabled" {
|
||||
// The serial console is disabled by default:
|
||||
// https://gokrazy.org/userguide/instance-config/#serialconsole
|
||||
// 'gok vm run' currently launches QEMU such that
|
||||
// there is a serial0 monitor available, but no HDMI.
|
||||
// Hence, print a tip for how to get the serial console to work.
|
||||
log.Printf("")
|
||||
log.Printf(` Tip: Your config.json disables the serial console. Set "SerialConsole": "ttyAMA0,115200", then select View -> serial0 in QEMU to access an interactive shell for debugging.`)
|
||||
log.Printf("")
|
||||
}
|
||||
|
||||
f, err := os.CreateTemp("", "gokrazy-vm")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -241,7 +249,7 @@ func (r *vmRunConfig) run(ctx context.Context, args []string, stdout, stderr io.
|
||||
fdi := f.Name()
|
||||
log.Printf("building disk image")
|
||||
if !r.dry {
|
||||
if err := r.buildFullDiskImage(ctx, fdi); err != nil {
|
||||
if err := r.buildFullDiskImage(ctx, fdi, fileCfg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package packer
|
||||
|
||||
import (
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/breml/rootcerts/embedded"
|
||||
"golang.org/x/crypto/x509roots/fallback/bundle"
|
||||
)
|
||||
|
||||
func (pack *Pack) findSystemCertsPEM() (string, error) {
|
||||
@@ -14,6 +15,7 @@ func (pack *Pack) findSystemCertsPEM() (string, error) {
|
||||
defer func() {
|
||||
log.Printf("Loading system CA certificates from %s", source)
|
||||
}()
|
||||
|
||||
// On Linux, we can copy the operating system’s certificate store.
|
||||
// certFiles is defined in cacerts_linux.go (or defined as empty in
|
||||
// cacertsstub.go on non-Linux):
|
||||
@@ -37,7 +39,18 @@ func (pack *Pack) findSystemCertsPEM() (string, error) {
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// Fall back to github.com/breml/rootcerts, i.e. the bundled Mozilla CA list:
|
||||
source = "bundled Mozilla CA list"
|
||||
return embedded.MozillaCACertificatesPEM(), nil
|
||||
// Fall back to the x/crypto fallback bundle root certificates:
|
||||
source = "bundled x509roots/fallback/bundle"
|
||||
return xrf(), nil
|
||||
}
|
||||
|
||||
func xrf() string {
|
||||
var certs []byte
|
||||
for c := range bundle.Roots() {
|
||||
certs = append(certs, pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: c.Certificate,
|
||||
})...)
|
||||
}
|
||||
return string(certs)
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func generateAndStoreSelfSignedCertificate(cfg *config.Struct, hostConfigPath, c
|
||||
}
|
||||
|
||||
func getCertificate(cfg *config.Struct) (string, string, error) {
|
||||
certPath, keyPath, err := tlsflag.CertificatePathsFor(cfg.Hostname)
|
||||
certPath, keyPath, err := tlsflag.CertificatePathsFor(cfg.Update.UseTLS, cfg.Hostname)
|
||||
if err != nil {
|
||||
var nycerr *tlsflag.ErrNotYetCreated
|
||||
if errors.As(err, &nycerr) {
|
||||
|
||||
56
internal/packer/eeprom.go
Normal file
56
internal/packer/eeprom.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package packer
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gokrazy/tools/internal/eeprom"
|
||||
)
|
||||
|
||||
func applyExtraEEPROM(pieeprom string, extraEEPROM []string) ([]byte, error) {
|
||||
upstream, err := os.Open(pieeprom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
upstreamBin, err := io.ReadAll(upstream)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sections, err := eeprom.Analyze(upstreamBin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bc := sections[len(sections)-1] // guaranteed to be bootconf.txt
|
||||
bootconfTxt := bc.FileContent()
|
||||
applied := overwriteBootconf(bootconfTxt, extraEEPROM)
|
||||
sections[len(sections)-1] = eeprom.FileSection(bc.Offset, bc.Filename, applied)
|
||||
return eeprom.Assemble(sections), nil
|
||||
}
|
||||
|
||||
func overwriteBootconf(bootconfTxt []byte, extraEEPROM []string) []byte {
|
||||
bootconfLines := strings.Split(strings.TrimSpace(string(bootconfTxt)), "\n")
|
||||
// For each property, there will be an entry from property to full line
|
||||
extraByProp := make(map[string]string)
|
||||
for _, line := range extraEEPROM {
|
||||
prop, _, ok := strings.Cut(line, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
extraByProp[prop] = line
|
||||
}
|
||||
var out strings.Builder
|
||||
for _, line := range bootconfLines {
|
||||
prop, _, ok := strings.Cut(line, "=")
|
||||
if !ok {
|
||||
out.WriteString(line + "\n")
|
||||
continue
|
||||
}
|
||||
if extra, ok := extraByProp[prop]; ok {
|
||||
out.WriteString(extra + "\n")
|
||||
} else {
|
||||
out.WriteString(line + "\n")
|
||||
}
|
||||
}
|
||||
return []byte(out.String())
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
398
internal/packer/packerbuild.go
Normal file
398
internal/packer/packerbuild.go
Normal file
@@ -0,0 +1,398 @@
|
||||
package packer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/trace"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gokrazy/internal/updateflag"
|
||||
"github.com/gokrazy/tools/packer"
|
||||
)
|
||||
|
||||
func (pack *Pack) logicBuild(bindir string) error {
|
||||
log := pack.Env.Logger()
|
||||
|
||||
cfg := pack.Cfg // for convenience
|
||||
args := cfg.Packages
|
||||
log.Printf("Building %d Go packages:", len(args))
|
||||
log.Printf("")
|
||||
for _, pkg := range args {
|
||||
log.Printf(" %s", pkg)
|
||||
for _, configFile := range packageConfigFiles[pkg] {
|
||||
log.Printf(" will %s",
|
||||
configFile.kind)
|
||||
if configFile.path != "" {
|
||||
log.Printf(" from %s",
|
||||
configFile.path)
|
||||
}
|
||||
if !configFile.lastModified.IsZero() {
|
||||
log.Printf(" last modified: %s (%s ago)",
|
||||
configFile.lastModified.Format(time.RFC3339),
|
||||
time.Since(configFile.lastModified).Round(1*time.Second))
|
||||
}
|
||||
}
|
||||
log.Printf("")
|
||||
}
|
||||
|
||||
pkgs := append([]string{}, cfg.GokrazyPackagesOrDefault()...)
|
||||
pkgs = append(pkgs, cfg.Packages...)
|
||||
pkgs = append(pkgs, packer.InitDeps(cfg.InternalCompatibilityFlags.InitPkg)...)
|
||||
noBuildPkgs := []string{
|
||||
cfg.KernelPackageOrDefault(),
|
||||
}
|
||||
if fw := cfg.FirmwarePackageOrDefault(); fw != "" {
|
||||
noBuildPkgs = append(noBuildPkgs, fw)
|
||||
}
|
||||
if e := cfg.EEPROMPackageOrDefault(); e != "" {
|
||||
noBuildPkgs = append(noBuildPkgs, e)
|
||||
}
|
||||
setUmask()
|
||||
buildEnv := &packer.BuildEnv{
|
||||
BuildDir: packer.BuildDirOrMigrate,
|
||||
}
|
||||
var buildErr error
|
||||
trace.WithRegion(context.Background(), "build", func() {
|
||||
buildErr = buildEnv.Build(bindir, pkgs, pack.packageBuildFlags, pack.packageBuildTags, pack.packageBuildEnv, noBuildPkgs, pack.basenames)
|
||||
})
|
||||
if buildErr != nil {
|
||||
return buildErr
|
||||
}
|
||||
|
||||
log.Printf("")
|
||||
|
||||
var err error
|
||||
trace.WithRegion(context.Background(), "validate", func() {
|
||||
err = pack.validateTargetArchMatchesKernel()
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
root *FileInfo
|
||||
foundBins []foundBin
|
||||
)
|
||||
trace.WithRegion(context.Background(), "findbins", func() {
|
||||
root, foundBins, err = findBins(cfg, buildEnv, bindir, pack.basenames)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pack.root = root
|
||||
|
||||
packageConfigFiles = make(map[string][]packageConfigFile)
|
||||
|
||||
var extraFiles map[string][]*FileInfo
|
||||
trace.WithRegion(context.Background(), "findextrafiles", func() {
|
||||
extraFiles, err = FindExtraFiles(cfg)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, packageExtraFiles := range extraFiles {
|
||||
for _, ef := range packageExtraFiles {
|
||||
for _, de := range ef.Dirents {
|
||||
if de.Filename != "perm" {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("invalid ExtraFilePaths or ExtraFileContents: cannot write extra files to user-controlled /perm partition")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(packageConfigFiles) > 0 {
|
||||
log.Printf("Including extra files for Go packages:")
|
||||
log.Printf("")
|
||||
for _, pkg := range args {
|
||||
if len(packageConfigFiles[pkg]) == 0 {
|
||||
continue
|
||||
}
|
||||
log.Printf(" %s", pkg)
|
||||
for _, configFile := range packageConfigFiles[pkg] {
|
||||
log.Printf(" will %s",
|
||||
configFile.kind)
|
||||
log.Printf(" from %s",
|
||||
configFile.path)
|
||||
log.Printf(" last modified: %s (%s ago)",
|
||||
configFile.lastModified.Format(time.RFC3339),
|
||||
time.Since(configFile.lastModified).Round(1*time.Second))
|
||||
}
|
||||
log.Printf("")
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.InternalCompatibilityFlags.InitPkg == "" {
|
||||
gokrazyInit := &gokrazyInit{
|
||||
root: root,
|
||||
flagFileContents: pack.flagFileContents,
|
||||
envFileContents: pack.envFileContents,
|
||||
buildTimestamp: pack.buildTimestamp,
|
||||
dontStart: pack.dontStart,
|
||||
waitForClock: pack.waitForClock,
|
||||
basenames: pack.basenames,
|
||||
}
|
||||
if cfg.InternalCompatibilityFlags.OverwriteInit != "" {
|
||||
return gokrazyInit.dump(cfg.InternalCompatibilityFlags.OverwriteInit)
|
||||
}
|
||||
|
||||
var tmpdir string
|
||||
trace.WithRegion(context.Background(), "buildinit", func() {
|
||||
tmpdir, err = gokrazyInit.build()
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pack.initTmp = tmpdir
|
||||
|
||||
initPath := filepath.Join(tmpdir, "init")
|
||||
|
||||
fileIsELFOrFatal(initPath)
|
||||
|
||||
gokrazy := root.mustFindDirent("gokrazy")
|
||||
gokrazy.Dirents = append(gokrazy.Dirents, &FileInfo{
|
||||
Filename: "init",
|
||||
FromHost: initPath,
|
||||
})
|
||||
}
|
||||
|
||||
defaultPassword, updateHostname := updateflag.Value{
|
||||
Update: cfg.InternalCompatibilityFlags.Update,
|
||||
}.GetUpdateTarget(cfg.Hostname)
|
||||
update, err := cfg.Update.WithFallbackToHostSpecific(cfg.Hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if update.HTTPPort == "" {
|
||||
update.HTTPPort = "80"
|
||||
}
|
||||
|
||||
if update.HTTPSPort == "" {
|
||||
update.HTTPSPort = "443"
|
||||
}
|
||||
|
||||
if update.Hostname == "" {
|
||||
update.Hostname = updateHostname
|
||||
}
|
||||
|
||||
if update.HTTPPassword == "" && !update.NoPassword {
|
||||
pw, err := ensurePasswordFileExists(updateHostname, defaultPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
update.HTTPPassword = pw
|
||||
}
|
||||
|
||||
pack.update = update
|
||||
|
||||
for _, dir := range []string{"bin", "dev", "etc", "proc", "sys", "tmp", "perm", "lib", "run", "mnt"} {
|
||||
root.Dirents = append(root.Dirents, &FileInfo{
|
||||
Filename: dir,
|
||||
})
|
||||
}
|
||||
|
||||
root.Dirents = append(root.Dirents, &FileInfo{
|
||||
Filename: "var",
|
||||
SymlinkDest: "/perm/var",
|
||||
})
|
||||
|
||||
mnt := root.mustFindDirent("mnt")
|
||||
for _, md := range cfg.MountDevices {
|
||||
if !strings.HasPrefix(md.Target, "/mnt/") {
|
||||
continue
|
||||
}
|
||||
rest := strings.TrimPrefix(md.Target, "/mnt/")
|
||||
rest = strings.TrimSuffix(rest, "/")
|
||||
if strings.Contains(rest, "/") {
|
||||
continue
|
||||
}
|
||||
mnt.Dirents = append(mnt.Dirents, &FileInfo{
|
||||
Filename: rest,
|
||||
})
|
||||
}
|
||||
|
||||
// include lib/modules from kernelPackage dir, if present
|
||||
kernelDir, err := packer.PackageDir(cfg.KernelPackageOrDefault())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pack.kernelDir = kernelDir
|
||||
modulesDir := filepath.Join(kernelDir, "lib", "modules")
|
||||
if _, err := os.Stat(modulesDir); err == nil {
|
||||
log.Printf("Including loadable kernel modules from:")
|
||||
log.Printf(" %s", modulesDir)
|
||||
modules := &FileInfo{
|
||||
Filename: "modules",
|
||||
}
|
||||
trace.WithRegion(context.Background(), "kernelmod", func() {
|
||||
_, err = addToFileInfo(modules, modulesDir)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lib := root.mustFindDirent("lib")
|
||||
lib.Dirents = append(lib.Dirents, modules)
|
||||
}
|
||||
|
||||
etc := root.mustFindDirent("etc")
|
||||
tmpdir, err := os.MkdirTemp("", "gokrazy")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
hostLocaltime, err := hostLocaltime(tmpdir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hostLocaltime != "" {
|
||||
etc.Dirents = append(etc.Dirents, &FileInfo{
|
||||
Filename: "localtime",
|
||||
FromHost: hostLocaltime,
|
||||
})
|
||||
}
|
||||
etc.Dirents = append(etc.Dirents, &FileInfo{
|
||||
Filename: "resolv.conf",
|
||||
SymlinkDest: "/tmp/resolv.conf",
|
||||
})
|
||||
etc.Dirents = append(etc.Dirents, &FileInfo{
|
||||
Filename: "hosts",
|
||||
FromLiteral: `127.0.0.1 localhost
|
||||
::1 localhost
|
||||
`,
|
||||
})
|
||||
etc.Dirents = append(etc.Dirents, &FileInfo{
|
||||
Filename: "hostname",
|
||||
FromLiteral: cfg.Hostname,
|
||||
})
|
||||
|
||||
ssl := &FileInfo{Filename: "ssl"}
|
||||
ssl.Dirents = append(ssl.Dirents, &FileInfo{
|
||||
Filename: "ca-bundle.pem",
|
||||
FromLiteral: pack.systemCertsPEM,
|
||||
})
|
||||
|
||||
pack.schema = "http"
|
||||
if update.CertPEM == "" || update.KeyPEM == "" {
|
||||
deployCertFile, deployKeyFile, err := getCertificate(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if deployCertFile != "" {
|
||||
b, err := os.ReadFile(deployCertFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
update.CertPEM = strings.TrimSpace(string(b))
|
||||
|
||||
b, err = os.ReadFile(deployKeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
update.KeyPEM = strings.TrimSpace(string(b))
|
||||
}
|
||||
}
|
||||
if update.CertPEM != "" && update.KeyPEM != "" {
|
||||
// User requested TLS
|
||||
pack.schema = "https"
|
||||
|
||||
ssl.Dirents = append(ssl.Dirents, &FileInfo{
|
||||
Filename: "gokrazy-web.pem",
|
||||
FromLiteral: update.CertPEM,
|
||||
})
|
||||
ssl.Dirents = append(ssl.Dirents, &FileInfo{
|
||||
Filename: "gokrazy-web.key.pem",
|
||||
FromLiteral: update.KeyPEM,
|
||||
})
|
||||
}
|
||||
|
||||
etc.Dirents = append(etc.Dirents, ssl)
|
||||
|
||||
if !update.NoPassword {
|
||||
etc.Dirents = append(etc.Dirents, &FileInfo{
|
||||
Filename: "gokr-pw.txt",
|
||||
Mode: 0400,
|
||||
FromLiteral: update.HTTPPassword,
|
||||
})
|
||||
}
|
||||
|
||||
etc.Dirents = append(etc.Dirents, &FileInfo{
|
||||
Filename: "http-port.txt",
|
||||
FromLiteral: update.HTTPPort,
|
||||
})
|
||||
|
||||
etc.Dirents = append(etc.Dirents, &FileInfo{
|
||||
Filename: "https-port.txt",
|
||||
FromLiteral: update.HTTPSPort,
|
||||
})
|
||||
|
||||
// GenerateSBOM() must be provided with a cfg
|
||||
// that hasn't been modified by gok at runtime,
|
||||
// as the SBOM should reflect what’s going into gokrazy,
|
||||
// not its internal implementation details
|
||||
// (i.e. cfg.InternalCompatibilityFlags untouched).
|
||||
var sbom []byte
|
||||
var sbomWithHash SBOMWithHash
|
||||
trace.WithRegion(context.Background(), "sbom", func() {
|
||||
sbom, sbomWithHash, err = generateSBOM(pack.FileCfg, foundBins)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pack.sbom = sbom
|
||||
pack.sbomWithHash = sbomWithHash
|
||||
|
||||
etcGokrazy := &FileInfo{Filename: "gokrazy"}
|
||||
etcGokrazy.Dirents = append(etcGokrazy.Dirents, &FileInfo{
|
||||
Filename: "sbom.json",
|
||||
FromLiteral: string(sbom),
|
||||
})
|
||||
mountdevices, err := json.Marshal(cfg.MountDevices)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
etcGokrazy.Dirents = append(etcGokrazy.Dirents, &FileInfo{
|
||||
Filename: "mountdevices.json",
|
||||
FromLiteral: string(mountdevices),
|
||||
})
|
||||
etc.Dirents = append(etc.Dirents, etcGokrazy)
|
||||
|
||||
empty := &FileInfo{Filename: ""}
|
||||
if paths := getDuplication(root, empty); len(paths) > 0 {
|
||||
return fmt.Errorf("root file system contains duplicate files: your config contains multiple packages that install %s", paths)
|
||||
}
|
||||
|
||||
for pkg1, fs := range extraFiles {
|
||||
for _, fs1 := range fs {
|
||||
// check against root fs
|
||||
if paths := getDuplication(root, fs1); len(paths) > 0 {
|
||||
return fmt.Errorf("extra files of package %s collides with root file system: %v", pkg1, paths)
|
||||
}
|
||||
|
||||
// check against other packages
|
||||
for pkg2, fs := range extraFiles {
|
||||
for _, fs2 := range fs {
|
||||
if pkg1 == pkg2 {
|
||||
continue
|
||||
}
|
||||
|
||||
if paths := getDuplication(fs1, fs2); len(paths) > 0 {
|
||||
return fmt.Errorf("extra files of package %s collides with package %s: %v", pkg1, pkg2, paths)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add extra files to rootfs
|
||||
if err := root.combine(fs1); err != nil {
|
||||
return fmt.Errorf("failed to add extra files from package %s: %v", pkg1, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
591
internal/packer/packerprepare.go
Normal file
591
internal/packer/packerprepare.go
Normal file
@@ -0,0 +1,591 @@
|
||||
package packer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gokrazy/internal/config"
|
||||
"github.com/gokrazy/internal/deviceconfig"
|
||||
"github.com/gokrazy/tools/internal/cap"
|
||||
"github.com/gokrazy/tools/internal/version"
|
||||
"github.com/gokrazy/tools/packer"
|
||||
)
|
||||
|
||||
type filePathAndModTime struct {
|
||||
path string
|
||||
modTime time.Time
|
||||
}
|
||||
|
||||
func findPackageFiles(fileType string) ([]filePathAndModTime, error) {
|
||||
var packageFilePaths []filePathAndModTime
|
||||
err := filepath.Walk(fileType, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info != nil && !info.Mode().IsRegular() {
|
||||
return nil
|
||||
}
|
||||
if strings.HasSuffix(path, fmt.Sprintf("/%s.txt", fileType)) {
|
||||
packageFilePaths = append(packageFilePaths, filePathAndModTime{
|
||||
path: path,
|
||||
modTime: info.ModTime(),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil // no fileType directory found
|
||||
}
|
||||
}
|
||||
|
||||
return packageFilePaths, nil
|
||||
}
|
||||
func (pack *Pack) logicPrepare(ctx context.Context) error {
|
||||
log := pack.Env.Logger()
|
||||
cfg := pack.Cfg
|
||||
|
||||
if cfg.InternalCompatibilityFlags.Update != "" &&
|
||||
cfg.InternalCompatibilityFlags.Overwrite != "" {
|
||||
return fmt.Errorf("both -update and -overwrite are specified; use either one, not both")
|
||||
}
|
||||
|
||||
// Check early on if the destination is a device that is mounted
|
||||
// so that the user does not get the impression that everything
|
||||
// is fine and about to complete after a lengthy build phase.
|
||||
// See also https://github.com/gokrazy/gokrazy/discussions/308
|
||||
switch {
|
||||
case cfg.InternalCompatibilityFlags.Overwrite != "" ||
|
||||
(pack.Output != nil && pack.Output.Type == OutputTypeFull && pack.Output.Path != ""):
|
||||
|
||||
target := cfg.InternalCompatibilityFlags.Overwrite
|
||||
st, err := os.Stat(target)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err == nil && st.Mode()&os.ModeDevice == os.ModeDevice {
|
||||
if err := verifyNotMounted(target); err != nil {
|
||||
return fmt.Errorf("cannot overwrite %s: %v (perhaps automatically?)\n please unmount all partitions and retry", target, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mbrOnlyWithoutGpt bool
|
||||
pack.firstPartitionOffsetSectors = deviceconfig.DefaultBootPartitionStartLBA
|
||||
if cfg.DeviceType != "" {
|
||||
if devcfg, ok := deviceconfig.GetDeviceConfigBySlug(cfg.DeviceType); ok {
|
||||
pack.rootDeviceFiles = devcfg.RootDeviceFiles
|
||||
mbrOnlyWithoutGpt = devcfg.MBROnlyWithoutGPT
|
||||
if devcfg.BootPartitionStartLBA != 0 {
|
||||
pack.firstPartitionOffsetSectors = devcfg.BootPartitionStartLBA
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("unknown device slug %q", cfg.DeviceType)
|
||||
}
|
||||
}
|
||||
|
||||
pack.Pack = packer.NewPackForHost(pack.firstPartitionOffsetSectors, cfg.Hostname)
|
||||
|
||||
newInstallation := cfg.InternalCompatibilityFlags.Update == ""
|
||||
useGPT := newInstallation && !mbrOnlyWithoutGpt
|
||||
|
||||
pack.Pack.UsePartuuid = newInstallation
|
||||
pack.Pack.UseGPTPartuuid = useGPT
|
||||
pack.Pack.UseGPT = useGPT
|
||||
|
||||
if os.Getenv("GOKR_PACKER_FD") != "" { // partitioning child process
|
||||
if _, err := pack.SudoPartition(cfg.InternalCompatibilityFlags.Overwrite); err != nil {
|
||||
log.Printf("%s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
log.Printf("%s %s on GOARCH=%s GOOS=%s",
|
||||
programName,
|
||||
version.ReadBrief(),
|
||||
runtime.GOARCH,
|
||||
runtime.GOOS)
|
||||
log.Printf("")
|
||||
|
||||
if cfg.InternalCompatibilityFlags.Update != "" {
|
||||
// TODO: fix update URL:
|
||||
log.Printf("Updating gokrazy installation on http://%s", cfg.Hostname)
|
||||
log.Printf("")
|
||||
}
|
||||
|
||||
log.Printf("Build target: %s", strings.Join(filterGoEnv(packer.Env()), " "))
|
||||
|
||||
pack.buildTimestamp = time.Now().Format(time.RFC3339)
|
||||
if ts, ok := ctx.Value(BuildTimestampOverride).(string); ok {
|
||||
pack.buildTimestamp = ts
|
||||
}
|
||||
log.Printf("Build timestamp: %s", pack.buildTimestamp)
|
||||
|
||||
systemCertsPEM, err := pack.findSystemCertsPEM()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pack.systemCertsPEM = systemCertsPEM
|
||||
|
||||
packageBuildFlags, err := pack.findBuildFlagsFiles(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pack.packageBuildFlags = packageBuildFlags
|
||||
|
||||
packageBuildTags, err := pack.findBuildTagsFiles(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pack.packageBuildTags = packageBuildTags
|
||||
|
||||
packageBuildEnv, err := findBuildEnv(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pack.packageBuildEnv = packageBuildEnv
|
||||
|
||||
flagFileContents, err := pack.findFlagFiles(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pack.flagFileContents = flagFileContents
|
||||
|
||||
envFileContents, err := pack.findEnvFiles(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pack.envFileContents = envFileContents
|
||||
|
||||
dontStart, err := pack.findDontStart(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pack.dontStart = dontStart
|
||||
|
||||
waitForClock, err := pack.findWaitForClock(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pack.waitForClock = waitForClock
|
||||
|
||||
basenames, err := findBasenames(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pack.basenames = basenames
|
||||
|
||||
capabilities, err := findCapabilities(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pack.xattrs = capabilities
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pack *Pack) findFlagFiles(cfg *config.Struct) (map[string][]string, error) {
|
||||
log := pack.Env.Logger()
|
||||
|
||||
if len(cfg.PackageConfig) > 0 {
|
||||
contents := make(map[string][]string)
|
||||
for pkg, packageConfig := range cfg.PackageConfig {
|
||||
if len(packageConfig.CommandLineFlags) == 0 {
|
||||
continue
|
||||
}
|
||||
contents[pkg] = packageConfig.CommandLineFlags
|
||||
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||
kind: "be started with command-line flags",
|
||||
path: cfg.Meta.Path,
|
||||
lastModified: cfg.Meta.LastModified,
|
||||
})
|
||||
}
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
flagFilePaths, err := findPackageFiles("flags")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(flagFilePaths) == 0 {
|
||||
return nil, nil // no flags.txt files found
|
||||
}
|
||||
|
||||
buildPackages := buildPackageMapFromFlags(cfg)
|
||||
|
||||
contents := make(map[string][]string)
|
||||
for _, p := range flagFilePaths {
|
||||
pkg := strings.TrimSuffix(strings.TrimPrefix(p.path, "flags/"), "/flags.txt")
|
||||
if !buildPackages[pkg] {
|
||||
log.Printf("WARNING: flag file %s does not match any specified package (%s)", pkg, cfg.Packages)
|
||||
continue
|
||||
}
|
||||
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||
kind: "be started with command-line flags",
|
||||
path: p.path,
|
||||
lastModified: p.modTime,
|
||||
})
|
||||
|
||||
b, err := os.ReadFile(p.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(string(b)), "\n")
|
||||
contents[pkg] = lines
|
||||
}
|
||||
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
func (pack *Pack) findBuildFlagsFiles(cfg *config.Struct) (map[string][]string, error) {
|
||||
log := pack.Env.Logger()
|
||||
|
||||
if len(cfg.PackageConfig) > 0 {
|
||||
contents := make(map[string][]string)
|
||||
for pkg, packageConfig := range cfg.PackageConfig {
|
||||
if len(packageConfig.GoBuildFlags) == 0 {
|
||||
continue
|
||||
}
|
||||
contents[pkg] = packageConfig.GoBuildFlags
|
||||
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||
kind: "be compiled with build flags",
|
||||
path: cfg.Meta.Path,
|
||||
lastModified: cfg.Meta.LastModified,
|
||||
})
|
||||
}
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
buildFlagsFilePaths, err := findPackageFiles("buildflags")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(buildFlagsFilePaths) == 0 {
|
||||
return nil, nil // no flags.txt files found
|
||||
}
|
||||
|
||||
buildPackages := buildPackageMapFromFlags(cfg)
|
||||
|
||||
contents := make(map[string][]string)
|
||||
for _, p := range buildFlagsFilePaths {
|
||||
pkg := strings.TrimSuffix(strings.TrimPrefix(p.path, "buildflags/"), "/buildflags.txt")
|
||||
if !buildPackages[pkg] {
|
||||
log.Printf("WARNING: buildflags file %s does not match any specified package (%s)", pkg, cfg.Packages)
|
||||
continue
|
||||
}
|
||||
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||
kind: "be compiled with build flags",
|
||||
path: p.path,
|
||||
lastModified: p.modTime,
|
||||
})
|
||||
|
||||
b, err := os.ReadFile(p.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buildFlags []string
|
||||
sc := bufio.NewScanner(strings.NewReader(string(b)))
|
||||
for sc.Scan() {
|
||||
if flag := sc.Text(); flag != "" {
|
||||
buildFlags = append(buildFlags, flag)
|
||||
}
|
||||
}
|
||||
|
||||
if err := sc.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// use full package path opposed to flags
|
||||
contents[pkg] = buildFlags
|
||||
}
|
||||
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
func findBuildEnv(cfg *config.Struct) (map[string][]string, error) {
|
||||
contents := make(map[string][]string)
|
||||
for pkg, packageConfig := range cfg.PackageConfig {
|
||||
if len(packageConfig.GoBuildEnvironment) == 0 {
|
||||
continue
|
||||
}
|
||||
contents[pkg] = packageConfig.GoBuildEnvironment
|
||||
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||
kind: "be compiled with build environment variables",
|
||||
path: cfg.Meta.Path,
|
||||
lastModified: cfg.Meta.LastModified,
|
||||
})
|
||||
}
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
func (pack *Pack) findBuildTagsFiles(cfg *config.Struct) (map[string][]string, error) {
|
||||
log := pack.Env.Logger()
|
||||
|
||||
if len(cfg.PackageConfig) > 0 {
|
||||
contents := make(map[string][]string)
|
||||
for pkg, packageConfig := range cfg.PackageConfig {
|
||||
if len(packageConfig.GoBuildTags) == 0 {
|
||||
continue
|
||||
}
|
||||
contents[pkg] = packageConfig.GoBuildTags
|
||||
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||
kind: "be compiled with build tags",
|
||||
path: cfg.Meta.Path,
|
||||
lastModified: cfg.Meta.LastModified,
|
||||
})
|
||||
}
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
buildTagsFiles, err := findPackageFiles("buildtags")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(buildTagsFiles) == 0 {
|
||||
return nil, nil // no flags.txt files found
|
||||
}
|
||||
|
||||
buildPackages := buildPackageMapFromFlags(cfg)
|
||||
|
||||
contents := make(map[string][]string)
|
||||
for _, p := range buildTagsFiles {
|
||||
pkg := strings.TrimSuffix(strings.TrimPrefix(p.path, "buildtags/"), "/buildtags.txt")
|
||||
if !buildPackages[pkg] {
|
||||
log.Printf("WARNING: buildtags file %s does not match any specified package (%s)", pkg, cfg.Packages)
|
||||
continue
|
||||
}
|
||||
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||
kind: "be compiled with build tags",
|
||||
path: p.path,
|
||||
lastModified: p.modTime,
|
||||
})
|
||||
|
||||
b, err := os.ReadFile(p.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buildTags []string
|
||||
sc := bufio.NewScanner(strings.NewReader(string(b)))
|
||||
for sc.Scan() {
|
||||
if flag := sc.Text(); flag != "" {
|
||||
buildTags = append(buildTags, flag)
|
||||
}
|
||||
}
|
||||
|
||||
if err := sc.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// use full package path opposed to flags
|
||||
contents[pkg] = buildTags
|
||||
}
|
||||
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
func (pack *Pack) findEnvFiles(cfg *config.Struct) (map[string][]string, error) {
|
||||
log := pack.Env.Logger()
|
||||
|
||||
if len(cfg.PackageConfig) > 0 {
|
||||
contents := make(map[string][]string)
|
||||
for pkg, packageConfig := range cfg.PackageConfig {
|
||||
if len(packageConfig.Environment) == 0 {
|
||||
continue
|
||||
}
|
||||
contents[pkg] = packageConfig.Environment
|
||||
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||
kind: "be started with environment variables",
|
||||
path: cfg.Meta.Path,
|
||||
lastModified: cfg.Meta.LastModified,
|
||||
})
|
||||
}
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
buildFlagsFilePaths, err := findPackageFiles("env")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(buildFlagsFilePaths) == 0 {
|
||||
return nil, nil // no flags.txt files found
|
||||
}
|
||||
|
||||
buildPackages := buildPackageMapFromFlags(cfg)
|
||||
|
||||
contents := make(map[string][]string)
|
||||
for _, p := range buildFlagsFilePaths {
|
||||
pkg := strings.TrimSuffix(strings.TrimPrefix(p.path, "env/"), "/env.txt")
|
||||
if !buildPackages[pkg] {
|
||||
log.Printf("WARNING: environment variable file %s does not match any specified package (%s)", pkg, cfg.Packages)
|
||||
continue
|
||||
}
|
||||
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||
kind: "be started with environment variables",
|
||||
path: p.path,
|
||||
lastModified: p.modTime,
|
||||
})
|
||||
|
||||
b, err := os.ReadFile(p.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(string(b)), "\n")
|
||||
contents[pkg] = lines
|
||||
}
|
||||
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
func (pack *Pack) findDontStart(cfg *config.Struct) (map[string]bool, error) {
|
||||
log := pack.Env.Logger()
|
||||
|
||||
if len(cfg.PackageConfig) > 0 {
|
||||
contents := make(map[string]bool)
|
||||
for pkg, packageConfig := range cfg.PackageConfig {
|
||||
if !packageConfig.DontStart {
|
||||
continue
|
||||
}
|
||||
contents[pkg] = packageConfig.DontStart
|
||||
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||
kind: "not be started at boot",
|
||||
path: cfg.Meta.Path,
|
||||
lastModified: cfg.Meta.LastModified,
|
||||
})
|
||||
}
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
dontStartPaths, err := findPackageFiles("dontstart")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(dontStartPaths) == 0 {
|
||||
return nil, nil // no dontstart.txt files found
|
||||
}
|
||||
|
||||
buildPackages := buildPackageMapFromFlags(cfg)
|
||||
|
||||
contents := make(map[string]bool)
|
||||
for _, p := range dontStartPaths {
|
||||
pkg := strings.TrimSuffix(strings.TrimPrefix(p.path, "dontstart/"), "/dontstart.txt")
|
||||
if !buildPackages[pkg] {
|
||||
log.Printf("WARNING: dontstart.txt file %s does not match any specified package (%s)", pkg, cfg.Packages)
|
||||
continue
|
||||
}
|
||||
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||
kind: "not be started at boot",
|
||||
path: p.path,
|
||||
lastModified: p.modTime,
|
||||
})
|
||||
|
||||
contents[pkg] = true
|
||||
}
|
||||
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
func (pack *Pack) findWaitForClock(cfg *config.Struct) (map[string]bool, error) {
|
||||
log := pack.Env.Logger()
|
||||
|
||||
if len(cfg.PackageConfig) > 0 {
|
||||
contents := make(map[string]bool)
|
||||
for pkg, packageConfig := range cfg.PackageConfig {
|
||||
if !packageConfig.WaitForClock {
|
||||
continue
|
||||
}
|
||||
contents[pkg] = packageConfig.WaitForClock
|
||||
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||
kind: "wait for clock synchronization before start",
|
||||
path: cfg.Meta.Path,
|
||||
lastModified: cfg.Meta.LastModified,
|
||||
})
|
||||
}
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
waitForClockPaths, err := findPackageFiles("waitforclock")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(waitForClockPaths) == 0 {
|
||||
return nil, nil // no waitforclock.txt files found
|
||||
}
|
||||
|
||||
buildPackages := buildPackageMapFromFlags(cfg)
|
||||
|
||||
contents := make(map[string]bool)
|
||||
for _, p := range waitForClockPaths {
|
||||
pkg := strings.TrimSuffix(strings.TrimPrefix(p.path, "waitforclock/"), "/waitforclock.txt")
|
||||
if !buildPackages[pkg] {
|
||||
log.Printf("WARNING: waitforclock.txt file %s does not match any specified package (%s)", pkg, cfg.Packages)
|
||||
continue
|
||||
}
|
||||
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||
kind: "wait for clock synchronization before start",
|
||||
path: p.path,
|
||||
lastModified: p.modTime,
|
||||
})
|
||||
|
||||
contents[pkg] = true
|
||||
}
|
||||
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
func findBasenames(cfg *config.Struct) (map[string]string, error) {
|
||||
contents := make(map[string]string)
|
||||
for pkg, packageConfig := range cfg.PackageConfig {
|
||||
if packageConfig.Basename == "" {
|
||||
continue
|
||||
}
|
||||
contents[pkg] = packageConfig.Basename
|
||||
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||
kind: "be installed with the basename set to " + packageConfig.Basename,
|
||||
})
|
||||
}
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
func findCapabilities(cfg *config.Struct) (map[string]map[string][]byte, error) {
|
||||
contents := make(map[string]map[string][]byte)
|
||||
for pkg, packageConfig := range cfg.PackageConfig {
|
||||
if packageConfig.Capabilities == "" {
|
||||
continue
|
||||
}
|
||||
set, err := cap.FromText(packageConfig.Capabilities)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to parse capabilities: %s: %w", packageConfig.Capabilities, err)
|
||||
}
|
||||
xattrValue, err := set.PackFileCap()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to pack capabilities: %s: %w", packageConfig.Capabilities, err) // This should basically never happen
|
||||
}
|
||||
set, err = cap.DigestFileCap(xattrValue)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error checking packed capabilities: %s: %w", packageConfig.Capabilities, err) // This should also never happen
|
||||
}
|
||||
contents[pkg] = map[string][]byte{
|
||||
"security.capability": xattrValue,
|
||||
}
|
||||
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||
kind: "be installed with file capabilities " + set.String(),
|
||||
})
|
||||
}
|
||||
return contents, nil
|
||||
}
|
||||
253
internal/packer/packerupdate.go
Normal file
253
internal/packer/packerupdate.go
Normal file
@@ -0,0 +1,253 @@
|
||||
package packer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gokrazy/internal/httpclient"
|
||||
"github.com/gokrazy/internal/humanize"
|
||||
"github.com/gokrazy/internal/progress"
|
||||
"github.com/gokrazy/internal/updateflag"
|
||||
"github.com/gokrazy/updater"
|
||||
)
|
||||
|
||||
func (pack *Pack) logicUpdate(ctx context.Context, isDev bool, bootSize int64, rootSize int64, tmpMBR, tmpBoot, tmpRoot *os.File, updateBaseUrl *url.URL, target *updater.Target, updateHttpClient *http.Client) error {
|
||||
log := pack.Env.Logger()
|
||||
cfg := pack.Cfg // for convenience
|
||||
update := pack.update // for convenience
|
||||
|
||||
var rootReader, bootReader, mbrReader io.Reader
|
||||
// Determine where to read the boot, root and MBR images from.
|
||||
switch {
|
||||
case cfg.InternalCompatibilityFlags.Overwrite != "":
|
||||
if isDev {
|
||||
bootFile, err := os.Open(cfg.InternalCompatibilityFlags.Overwrite + "1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bootReader = bootFile
|
||||
rootFile, err := os.Open(cfg.InternalCompatibilityFlags.Overwrite + "2")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rootReader = rootFile
|
||||
} else {
|
||||
bootFile, err := os.Open(cfg.InternalCompatibilityFlags.Overwrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := bootFile.Seek(pack.firstPartitionOffsetSectors*512, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
bootReader = &io.LimitedReader{
|
||||
R: bootFile,
|
||||
N: bootSize,
|
||||
}
|
||||
|
||||
rootFile, err := os.Open(cfg.InternalCompatibilityFlags.Overwrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := rootFile.Seek(pack.firstPartitionOffsetSectors*512+100*MB, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
rootReader = &io.LimitedReader{
|
||||
R: rootFile,
|
||||
N: rootSize,
|
||||
}
|
||||
}
|
||||
mbrFile, err := os.Open(cfg.InternalCompatibilityFlags.Overwrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mbrReader = &io.LimitedReader{
|
||||
R: mbrFile,
|
||||
N: 446,
|
||||
}
|
||||
|
||||
default:
|
||||
if cfg.InternalCompatibilityFlags.OverwriteBoot != "" {
|
||||
bootFile, err := os.Open(cfg.InternalCompatibilityFlags.OverwriteBoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bootReader = bootFile
|
||||
if cfg.InternalCompatibilityFlags.OverwriteMBR != "" {
|
||||
mbrFile, err := os.Open(cfg.InternalCompatibilityFlags.OverwriteMBR)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mbrReader = mbrFile
|
||||
} else {
|
||||
if _, err := tmpMBR.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
mbrReader = tmpMBR
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.InternalCompatibilityFlags.OverwriteRoot != "" {
|
||||
rootFile, err := os.Open(cfg.InternalCompatibilityFlags.OverwriteRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rootReader = rootFile
|
||||
}
|
||||
|
||||
if cfg.InternalCompatibilityFlags.OverwriteBoot == "" && cfg.InternalCompatibilityFlags.OverwriteRoot == "" {
|
||||
if _, err := tmpBoot.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
bootReader = tmpBoot
|
||||
|
||||
if _, err := tmpMBR.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
mbrReader = tmpMBR
|
||||
|
||||
if _, err := tmpRoot.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
rootReader = tmpRoot
|
||||
}
|
||||
}
|
||||
|
||||
updateBaseUrl.Path = "/"
|
||||
log.Printf("Updating %s", updateBaseUrl.String())
|
||||
|
||||
progctx, canc := context.WithCancel(context.Background())
|
||||
defer canc()
|
||||
prog := &progress.Reporter{}
|
||||
go prog.Report(progctx)
|
||||
|
||||
// Start with the root file system because writing to the non-active
|
||||
// partition cannot break the currently running system.
|
||||
if err := pack.updateWithProgress(prog, rootReader, target, "root file system", "root"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rootDeviceFile := range pack.rootDeviceFiles {
|
||||
f, err := os.Open(filepath.Join(pack.kernelDir, rootDeviceFile.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := pack.updateWithProgress(
|
||||
prog, f, target, fmt.Sprintf("root device file %s", rootDeviceFile.Name),
|
||||
filepath.Join("device-specific", rootDeviceFile.Name),
|
||||
); err != nil {
|
||||
if errors.Is(err, updater.ErrUpdateHandlerNotImplemented) {
|
||||
log.Printf("target does not support updating device file %s yet, ignoring", rootDeviceFile.Name)
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := pack.updateWithProgress(prog, bootReader, target, "boot file system", "boot"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := target.StreamTo(ctx, "mbr", mbrReader); err != nil {
|
||||
if err == updater.ErrUpdateHandlerNotImplemented {
|
||||
log.Printf("target does not support updating MBR yet, ignoring")
|
||||
} else {
|
||||
return fmt.Errorf("updating MBR: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.InternalCompatibilityFlags.Testboot {
|
||||
if err := target.Testboot(ctx); err != nil {
|
||||
return fmt.Errorf("enable testboot of non-active partition: %v", err)
|
||||
}
|
||||
} else {
|
||||
if err := target.Switch(ctx); err != nil {
|
||||
return fmt.Errorf("switching to non-active partition: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop progress reporting to not mess up the following logs output.
|
||||
canc()
|
||||
|
||||
log.Printf("Triggering reboot")
|
||||
if err := target.Reboot(ctx); err != nil {
|
||||
if errors.Is(err, syscall.ECONNRESET) {
|
||||
log.Printf("ignoring reboot error: %v", err)
|
||||
} else {
|
||||
return fmt.Errorf("reboot: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
const polltimeout = 5 * time.Minute
|
||||
log.Printf("Updated, waiting %v for the device to become reachable (cancel with Ctrl-C any time)", polltimeout)
|
||||
|
||||
if update.CertPEM != "" && update.KeyPEM != "" {
|
||||
// Use an HTTPS client (post-update),
|
||||
// even when the --insecure flag was specified.
|
||||
pack.schema = "https"
|
||||
var err error
|
||||
updateBaseUrl, err = updateflag.Value{
|
||||
Update: "yes",
|
||||
}.BaseURL(update.HTTPPort, update.HTTPSPort, pack.schema, update.Hostname, update.HTTPPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updateHttpClient, _, err = httpclient.GetTLSHttpClientByTLSFlag(update.UseTLS, false /* insecure */, updateBaseUrl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting http client by tls flag: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
pollctx, canc := context.WithTimeout(context.Background(), polltimeout)
|
||||
defer canc()
|
||||
for {
|
||||
if err := pollctx.Err(); err != nil {
|
||||
return fmt.Errorf("device did not become healthy after update (%v)", err)
|
||||
}
|
||||
if err := pollUpdated1(pollctx, updateHttpClient, updateBaseUrl.String(), pack.buildTimestamp); err != nil {
|
||||
log.Printf("device not yet reachable: %v", err)
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("Device ready to use!")
|
||||
break
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pack *Pack) updateWithProgress(prog *progress.Reporter, reader io.Reader, target *updater.Target, logStr string, stream string) error {
|
||||
ctx := context.Background()
|
||||
log := pack.Env.Logger()
|
||||
|
||||
start := time.Now()
|
||||
prog.SetStatus(fmt.Sprintf("update %s", logStr))
|
||||
prog.SetTotal(0)
|
||||
|
||||
if stater, ok := reader.(interface{ Stat() (os.FileInfo, error) }); ok {
|
||||
if st, err := stater.Stat(); err == nil {
|
||||
prog.SetTotal(uint64(st.Size()))
|
||||
}
|
||||
}
|
||||
if err := target.StreamTo(ctx, stream, io.TeeReader(reader, &progress.Writer{})); err != nil {
|
||||
return fmt.Errorf("updating %s: %w", logStr, err)
|
||||
}
|
||||
duration := time.Since(start)
|
||||
transferred := progress.Reset()
|
||||
log.Printf("\rTransferred %s (%s) at %.2f MiB/s (total: %v)",
|
||||
logStr,
|
||||
humanize.Bytes(transferred),
|
||||
float64(transferred)/duration.Seconds()/1024/1024,
|
||||
duration.Round(time.Second))
|
||||
|
||||
return nil
|
||||
}
|
||||
433
internal/packer/packerwrite.go
Normal file
433
internal/packer/packerwrite.go
Normal file
@@ -0,0 +1,433 @@
|
||||
package packer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/trace"
|
||||
"strings"
|
||||
|
||||
"github.com/gokrazy/internal/config"
|
||||
"github.com/gokrazy/internal/deviceconfig"
|
||||
"github.com/gokrazy/internal/httpclient"
|
||||
"github.com/gokrazy/internal/updateflag"
|
||||
"github.com/gokrazy/tools/packer"
|
||||
"github.com/gokrazy/updater"
|
||||
)
|
||||
|
||||
func (pack *Pack) logicWrite(dnsCheck chan error) error {
|
||||
ctx := context.Background()
|
||||
log := pack.Env.Logger()
|
||||
|
||||
var (
|
||||
updateHttpClient *http.Client
|
||||
updateBaseUrl *url.URL
|
||||
target *updater.Target
|
||||
)
|
||||
|
||||
newInstallation := pack.Cfg.InternalCompatibilityFlags.Update == ""
|
||||
insecure := pack.Cfg.InternalCompatibilityFlags.Insecure
|
||||
if !newInstallation {
|
||||
update := pack.update // for convenience
|
||||
var err error
|
||||
updateBaseUrl, err = updateflag.Value{
|
||||
Update: pack.Cfg.InternalCompatibilityFlags.Update,
|
||||
}.BaseURL(update.HTTPPort, update.HTTPSPort, pack.schema, update.Hostname, update.HTTPPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateHttpClient, _, err = httpclient.GetTLSHttpClientByTLSFlag(update.UseTLS, insecure, updateBaseUrl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting http client by tls flag: %v", err)
|
||||
}
|
||||
|
||||
updateBaseUrl.Path = "/"
|
||||
|
||||
target, err = updater.NewTarget(ctx, updateBaseUrl.String(), updateHttpClient)
|
||||
if err != nil {
|
||||
if !insecure {
|
||||
return fmt.Errorf("checking target partuuid support: %v", err)
|
||||
}
|
||||
log.Printf("Falling back to HTTP because of the --insecure flag")
|
||||
updateBaseUrl, err = updateflag.Value{
|
||||
Update: pack.Cfg.InternalCompatibilityFlags.Update,
|
||||
}.BaseURL(update.HTTPPort, update.HTTPSPort, "http", update.Hostname, update.HTTPPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateHttpClient, _, err = httpclient.GetTLSHttpClientByTLSFlag(update.UseTLS, insecure, updateBaseUrl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting http client by tls flag: %v", err)
|
||||
}
|
||||
target, err = updater.NewTarget(ctx, updateBaseUrl.String(), updateHttpClient)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking target partuuid support: %v", err)
|
||||
}
|
||||
}
|
||||
pack.UsePartuuid = target.Supports("partuuid")
|
||||
pack.UseGPTPartuuid = target.Supports("gpt")
|
||||
pack.UseGPT = target.Supports("gpt")
|
||||
pack.ExistingEEPROM = target.InstalledEEPROM()
|
||||
}
|
||||
log.Printf("")
|
||||
log.Printf("Feature summary:")
|
||||
log.Printf(" use GPT: %v", pack.UseGPT)
|
||||
log.Printf(" use PARTUUID: %v", pack.UsePartuuid)
|
||||
log.Printf(" use GPT PARTUUID: %v", pack.UseGPTPartuuid)
|
||||
|
||||
cfg := pack.Cfg // for convenience
|
||||
root := pack.root // for convenience
|
||||
// Determine where to write the boot and root images to.
|
||||
var (
|
||||
isDev bool
|
||||
tmpBoot, tmpRoot, tmpMBR *os.File
|
||||
bootSize, rootSize int64
|
||||
)
|
||||
switch {
|
||||
case cfg.InternalCompatibilityFlags.Overwrite != "" ||
|
||||
(pack.Output != nil && pack.Output.Type == OutputTypeFull && pack.Output.Path != ""):
|
||||
|
||||
st, err := os.Stat(cfg.InternalCompatibilityFlags.Overwrite)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
isDev = err == nil && st.Mode()&os.ModeDevice == os.ModeDevice
|
||||
|
||||
if isDev {
|
||||
if err := pack.overwriteDevice(cfg.InternalCompatibilityFlags.Overwrite, root, pack.rootDeviceFiles); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("To boot gokrazy, plug the SD card into a supported device (see https://gokrazy.org/platforms/)")
|
||||
log.Printf("")
|
||||
} else {
|
||||
lower := 1200*MB + int(pack.firstPartitionOffsetSectors)
|
||||
|
||||
if cfg.InternalCompatibilityFlags.TargetStorageBytes == 0 {
|
||||
return fmt.Errorf("--target_storage_bytes is required (e.g. --target_storage_bytes=%d) when using overwrite with a file", lower)
|
||||
}
|
||||
if cfg.InternalCompatibilityFlags.TargetStorageBytes%512 != 0 {
|
||||
return fmt.Errorf("--target_storage_bytes must be a multiple of 512 (sector size), use e.g. %d", lower)
|
||||
}
|
||||
if cfg.InternalCompatibilityFlags.TargetStorageBytes < lower {
|
||||
return fmt.Errorf("--target_storage_bytes must be at least %d (for boot + 2 root file systems + 100 MB /perm)", lower)
|
||||
}
|
||||
|
||||
bootSize, rootSize, err = pack.overwriteFile(root, pack.rootDeviceFiles, pack.firstPartitionOffsetSectors)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("To boot gokrazy, copy %s to an SD card and plug it into a supported device (see https://gokrazy.org/platforms/)", cfg.InternalCompatibilityFlags.Overwrite)
|
||||
log.Printf("")
|
||||
}
|
||||
|
||||
case pack.Output != nil && pack.Output.Type == OutputTypeGaf && pack.Output.Path != "":
|
||||
if err := pack.overwriteGaf(root, pack.sbom); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
if cfg.InternalCompatibilityFlags.OverwriteBoot != "" {
|
||||
mbrfn := cfg.InternalCompatibilityFlags.OverwriteMBR
|
||||
if cfg.InternalCompatibilityFlags.OverwriteMBR == "" {
|
||||
var err error
|
||||
tmpMBR, err = os.CreateTemp("", "gokrazy")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmpMBR.Name())
|
||||
mbrfn = tmpMBR.Name()
|
||||
}
|
||||
if err := pack.writeBootFile(cfg.InternalCompatibilityFlags.OverwriteBoot, mbrfn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.InternalCompatibilityFlags.OverwriteRoot != "" {
|
||||
var rootErr error
|
||||
trace.WithRegion(context.Background(), "writeroot", func() {
|
||||
rootErr = pack.writeRootFile(cfg.InternalCompatibilityFlags.OverwriteRoot, root)
|
||||
})
|
||||
if rootErr != nil {
|
||||
return rootErr
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.InternalCompatibilityFlags.OverwriteBoot == "" && cfg.InternalCompatibilityFlags.OverwriteRoot == "" {
|
||||
var err error
|
||||
tmpMBR, err = os.CreateTemp("", "gokrazy")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmpMBR.Name())
|
||||
|
||||
tmpBoot, err = os.CreateTemp("", "gokrazy")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmpBoot.Name())
|
||||
|
||||
if err := pack.writeBoot(tmpBoot, tmpMBR.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpRoot, err = os.CreateTemp("", "gokrazy")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmpRoot.Name())
|
||||
|
||||
if err := pack.writeRoot(tmpRoot, root); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("")
|
||||
log.Printf("Build complete!")
|
||||
|
||||
if err := pack.printHowToInteract(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := <-dnsCheck; err != nil {
|
||||
log.Printf("WARNING: if the above URL does not work, perhaps name resolution (DNS) is broken")
|
||||
log.Printf("in your local network? Resolving your hostname failed: %v", err)
|
||||
log.Printf("Did you maybe configure a DNS server other than your router?")
|
||||
log.Printf("")
|
||||
}
|
||||
|
||||
if newInstallation {
|
||||
return nil
|
||||
}
|
||||
|
||||
return pack.logicUpdate(ctx, isDev, bootSize, rootSize, tmpMBR, tmpBoot, tmpRoot, updateBaseUrl, target, updateHttpClient)
|
||||
}
|
||||
|
||||
func (p *Pack) overwriteDevice(dev string, root *FileInfo, rootDeviceFiles []deviceconfig.RootFile) error {
|
||||
log := p.Env.Logger()
|
||||
|
||||
if err := verifyNotMounted(dev); err != nil {
|
||||
return err
|
||||
}
|
||||
parttable := "GPT + Hybrid MBR"
|
||||
if !p.UseGPT {
|
||||
parttable = "no GPT, only MBR"
|
||||
}
|
||||
log.Printf("partitioning %s (%s)", dev, parttable)
|
||||
|
||||
f, err := p.partition(p.Cfg.InternalCompatibilityFlags.Overwrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.Seek(p.FirstPartitionOffsetSectors*512, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.writeBoot(f, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.writeMBR(p.FirstPartitionOffsetSectors, &offsetReadSeeker{f, p.FirstPartitionOffsetSectors * 512}, f, p.Partuuid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := f.Seek((p.FirstPartitionOffsetSectors+(100*MB/512))*512, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp("", "gokr-packer")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmp.Name())
|
||||
defer tmp.Close()
|
||||
|
||||
if err := p.writeRoot(tmp, root); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tmp.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(f, tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.writeRootDeviceFiles(f, rootDeviceFiles); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("If your applications need to store persistent data, unplug and re-plug the SD card, then create a file system using e.g.:")
|
||||
log.Printf("")
|
||||
partition := partitionPath(dev, "4")
|
||||
if p.ModifyCmdlineRoot() {
|
||||
partition = fmt.Sprintf("/dev/disk/by-partuuid/%s", p.PermUUID())
|
||||
} else {
|
||||
if target, err := filepath.EvalSymlinks(dev); err == nil {
|
||||
partition = partitionPath(target, "4")
|
||||
}
|
||||
}
|
||||
log.Printf("\tmkfs.ext4 %s", partition)
|
||||
log.Printf("")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func partitionPath(base, num string) string {
|
||||
if strings.HasPrefix(base, "/dev/mmcblk") ||
|
||||
strings.HasPrefix(base, "/dev/loop") {
|
||||
return base + "p" + num
|
||||
} else if strings.HasPrefix(base, "/dev/disk") ||
|
||||
strings.HasPrefix(base, "/dev/rdisk") {
|
||||
return base + "s" + num
|
||||
}
|
||||
return base + num
|
||||
}
|
||||
|
||||
type offsetReadSeeker struct {
|
||||
io.ReadSeeker
|
||||
offset int64
|
||||
}
|
||||
|
||||
func (ors *offsetReadSeeker) Seek(offset int64, whence int) (int64, error) {
|
||||
if whence == io.SeekStart {
|
||||
// github.com/gokrazy/internal/fat.Reader only uses io.SeekStart
|
||||
return ors.ReadSeeker.Seek(offset+ors.offset, io.SeekStart)
|
||||
}
|
||||
return ors.ReadSeeker.Seek(offset, whence)
|
||||
}
|
||||
|
||||
type countingWriter int64
|
||||
|
||||
func (cw *countingWriter) Write(p []byte) (n int, err error) {
|
||||
*cw += countingWriter(len(p))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (p *Pack) overwriteFile(root *FileInfo, rootDeviceFiles []deviceconfig.RootFile, firstPartitionOffsetSectors int64) (bootSize int64, rootSize int64, err error) {
|
||||
log := p.Env.Logger()
|
||||
|
||||
f, err := os.Create(p.Cfg.InternalCompatibilityFlags.Overwrite)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
if err := f.Truncate(int64(p.Cfg.InternalCompatibilityFlags.TargetStorageBytes)); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
if err := p.Partition(f, uint64(p.Cfg.InternalCompatibilityFlags.TargetStorageBytes)); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
if _, err := f.Seek(p.FirstPartitionOffsetSectors*512, io.SeekStart); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
var bs countingWriter
|
||||
if err := p.writeBoot(io.MultiWriter(f, &bs), ""); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
if err := p.writeMBR(p.FirstPartitionOffsetSectors, &offsetReadSeeker{f, p.FirstPartitionOffsetSectors * 512}, f, p.Partuuid); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
if _, err := f.Seek(p.FirstPartitionOffsetSectors*512+100*MB, io.SeekStart); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp("", "gokr-packer")
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
defer os.Remove(tmp.Name())
|
||||
defer tmp.Close()
|
||||
|
||||
if err := p.writeRoot(tmp, root); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if _, err := tmp.Seek(0, io.SeekStart); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
var rs countingWriter
|
||||
if _, err := io.Copy(io.MultiWriter(f, &rs), tmp); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
if err := p.writeRootDeviceFiles(f, rootDeviceFiles); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
log.Printf("If your applications need to store persistent data, create a file system using e.g.:")
|
||||
log.Printf("\t/sbin/mkfs.ext4 -F -E offset=%v %s %v", p.FirstPartitionOffsetSectors*512+1100*MB, p.Cfg.InternalCompatibilityFlags.Overwrite, packer.PermSizeInKB(firstPartitionOffsetSectors, uint64(p.Cfg.InternalCompatibilityFlags.TargetStorageBytes)))
|
||||
log.Printf("")
|
||||
|
||||
return int64(bs), int64(rs), f.Close()
|
||||
}
|
||||
|
||||
func (pack *Pack) printHowToInteract(cfg *config.Struct) error {
|
||||
log := pack.Env.Logger()
|
||||
update := pack.update // for convenience
|
||||
|
||||
updateFlag := pack.Cfg.InternalCompatibilityFlags.Update
|
||||
if updateFlag == "" {
|
||||
updateFlag = "yes"
|
||||
}
|
||||
updateBaseUrl, err := updateflag.Value{
|
||||
Update: updateFlag,
|
||||
}.BaseURL(update.HTTPPort, update.HTTPSPort, pack.schema, update.Hostname, update.HTTPPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("")
|
||||
log.Printf("To interact with the device, gokrazy provides a web interface reachable at:")
|
||||
log.Printf("")
|
||||
log.Printf("\t%s", updateBaseUrl.String())
|
||||
log.Printf("")
|
||||
log.Printf("In addition, the following Linux consoles are set up:")
|
||||
log.Printf("")
|
||||
if cfg.SerialConsoleOrDefault() != "disabled" {
|
||||
log.Printf("\t1. foreground Linux console on the serial port (115200n8, pin 6, 8, 10 for GND, TX, RX), accepting input")
|
||||
log.Printf("\t2. secondary Linux framebuffer console on HDMI; shows Linux kernel message but no init system messages")
|
||||
} else {
|
||||
log.Printf("\t1. foreground Linux framebuffer console on HDMI")
|
||||
}
|
||||
|
||||
if cfg.SerialConsoleOrDefault() != "disabled" {
|
||||
log.Printf("")
|
||||
log.Printf("Use -serial_console=disabled to make gokrazy not touch the serial port, and instead make the framebuffer console on HDMI the foreground console")
|
||||
}
|
||||
log.Printf("")
|
||||
if pack.schema == "https" {
|
||||
certObj, err := getCertificateFromString(update.CertPEM)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading certificate: %v", err)
|
||||
} else {
|
||||
log.Printf("")
|
||||
log.Printf("The TLS Certificate of the gokrazy web interface is located under")
|
||||
log.Printf("\t%s", cfg.Meta.Path)
|
||||
log.Printf("The fingerprint of the Certificate is")
|
||||
log.Printf("\t%x", getCertificateFingerprintSHA1(certObj))
|
||||
log.Printf("The certificate is valid until")
|
||||
log.Printf("\t%s", certObj.NotAfter.String())
|
||||
log.Printf("Please verify the certificate, before adding an exception to your browser!")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -19,7 +19,7 @@ func pollUpdated1(ctx context.Context, updateHttpClient *http.Client, updateBase
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
resp, err := updateHttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
82
internal/packer/validatekernel.go
Normal file
82
internal/packer/validatekernel.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package packer
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gokrazy/tools/packer"
|
||||
)
|
||||
|
||||
// kernelGoarch returns the GOARCH value that corresponds to the provided
|
||||
// vmlinuz header. It returns one of "arm", "arm64", "386", "amd64" or the empty
|
||||
// string if not detected.
|
||||
func kernelGoarch(hdr []byte) string {
|
||||
// Some constants from the file(1) command's magic.
|
||||
const (
|
||||
// 32-bit arm: https://github.com/file/file/blob/65be1904/magic/Magdir/linux#L238-L241
|
||||
arm32Magic = 0x016f2818
|
||||
arm32MagicOffset = 0x24
|
||||
// 64-bit arm: https://github.com/file/file/blob/65be1904/magic/Magdir/linux#L253-L254
|
||||
arm64Magic = 0x644d5241
|
||||
arm64MagicOffset = 0x38
|
||||
// x86: https://github.com/file/file/blob/65be1904/magic/Magdir/linux#L137-L152
|
||||
x86Magic = 0xaa55
|
||||
x86MagicOffset = 0x1fe
|
||||
x86XloadflagsOffset = 0x236
|
||||
)
|
||||
if len(hdr) >= arm64MagicOffset+4 && binary.LittleEndian.Uint32(hdr[arm64MagicOffset:]) == arm64Magic {
|
||||
return "arm64"
|
||||
}
|
||||
if len(hdr) >= arm32MagicOffset+4 && binary.LittleEndian.Uint32(hdr[arm32MagicOffset:]) == arm32Magic {
|
||||
return "arm"
|
||||
}
|
||||
if len(hdr) >= x86XloadflagsOffset+2 && binary.LittleEndian.Uint16(hdr[x86MagicOffset:]) == x86Magic {
|
||||
// XLF0 in arch/x86/boot/header.S
|
||||
if hdr[x86XloadflagsOffset]&1 != 0 {
|
||||
return "amd64"
|
||||
} else {
|
||||
return "386"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// validateTargetArchMatchesKernel validates that the packer.TargetArch
|
||||
// corresponds to the kernel's architecture.
|
||||
//
|
||||
// See https://github.com/gokrazy/gokrazy/issues/191 for background. Maybe the
|
||||
// TargetArch will become automatic in the future but for now this is a safety
|
||||
// net to prevent people from bricking their appliances with the wrong userspace
|
||||
// architecture.
|
||||
func (pack *Pack) validateTargetArchMatchesKernel() error {
|
||||
cfg := pack.Cfg
|
||||
kernelDir, err := packer.PackageDir(cfg.KernelPackageOrDefault())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
kernelPath := filepath.Join(kernelDir, "vmlinuz")
|
||||
k, err := os.Open(kernelPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer k.Close()
|
||||
hdr := make([]byte, 1<<10) // plenty
|
||||
if _, err := io.ReadFull(k, hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
kernelArch := kernelGoarch(hdr)
|
||||
if kernelArch == "" {
|
||||
return fmt.Errorf("kernel %v architecture in %s not detected", cfg.KernelPackageOrDefault(), kernelPath)
|
||||
}
|
||||
targetArch := packer.TargetArch()
|
||||
if kernelArch != targetArch {
|
||||
return fmt.Errorf("target architecture %q (GOARCH) doesn't match the %s kernel type %q",
|
||||
targetArch,
|
||||
cfg.KernelPackageOrDefault(),
|
||||
kernelArch)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func copyFile(fw *fat.Writer, dest string, src fs.File, srcName string) error {
|
||||
return src.Close()
|
||||
}
|
||||
|
||||
func copyFileSquash(d *squashfs.Directory, dest, src string) error {
|
||||
func copyFileSquash(d *squashfs.Directory, dest, src string, xattrs map[string][]byte) error {
|
||||
f, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -58,7 +58,7 @@ func copyFileSquash(d *squashfs.Directory, dest, src string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w, err := d.File(filepath.Base(dest), st.ModTime(), st.Mode()&os.ModePerm)
|
||||
w, err := d.File(filepath.Base(dest), st.ModTime(), st.Mode()&os.ModePerm, xattrs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -202,6 +202,18 @@ func (p *Pack) copyGlobsToBoot(fw *fat.Writer, srcDir string, globs []string) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Pack) writeBootFile(bootfilename, mbrfilename string) error {
|
||||
f, err := os.Create(bootfilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
if err := p.writeBoot(f, mbrfilename); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
func (p *Pack) writeBoot(f io.Writer, mbrfilename string) error {
|
||||
log := p.Env.Logger()
|
||||
log.Printf("")
|
||||
@@ -254,39 +266,25 @@ func (p *Pack) writeBoot(f io.Writer, mbrfilename string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// EEPROM update procedure. See also:
|
||||
// https://news.ycombinator.com/item?id=21674550
|
||||
writeEepromUpdateFile := func(globPattern, target string) (sig string, _ error) {
|
||||
matches, err := filepath.Glob(globPattern)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return "", fmt.Errorf("invalid -eeprom_package: no files matching %s", filepath.Base(globPattern))
|
||||
}
|
||||
|
||||
// matches must be non-empty
|
||||
bestMatch := func(matches []string) string {
|
||||
// Select the EEPROM file that sorts last.
|
||||
// This corresponds to most recent for the pieeprom-*.bin files,
|
||||
// which contain the date in yyyy-mm-dd format.
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(matches)))
|
||||
|
||||
f, err := os.Open(matches[0])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
st, err := f.Stat()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return matches[0]
|
||||
}
|
||||
// EEPROM update procedure. See also:
|
||||
// https://news.ycombinator.com/item?id=21674550
|
||||
writeEepromUpdate := func(target string, modTime time.Time, r io.Reader) (sig string, _ error) {
|
||||
// Copy the EEPROM file into the image and calculate its SHA256 hash
|
||||
// while doing so:
|
||||
w, err := fw.File(target, st.ModTime())
|
||||
w, err := fw.File(target, modTime)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(w, io.TeeReader(f, h)); err != nil {
|
||||
if _, err := io.Copy(w, io.TeeReader(r, h)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -301,33 +299,81 @@ func (p *Pack) writeBoot(f io.Writer, mbrfilename string) error {
|
||||
sigFn := target
|
||||
ext := filepath.Ext(sigFn)
|
||||
if ext == "" {
|
||||
return "", fmt.Errorf("BUG: cannot derive signature file name from matches[0]=%q", matches[0])
|
||||
return "", fmt.Errorf("BUG: cannot derive signature file name from target=%q", target)
|
||||
}
|
||||
sigFn = strings.TrimSuffix(sigFn, ext) + ".sig"
|
||||
w, err = fw.File(sigFn, st.ModTime())
|
||||
w, err = fw.File(sigFn, modTime)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = fmt.Fprintf(w, "%x\n", h.Sum(nil))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = fmt.Fprintf(w, "ts: %d\n", modTime.Unix())
|
||||
return fmt.Sprintf("%x", h.Sum(nil)), err
|
||||
}
|
||||
writeEepromUpdateFile := func(matches []string, target string) (sig string, _ error) {
|
||||
f, err := os.Open(bestMatch(matches))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
st, err := f.Stat()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return writeEepromUpdate(target, st.ModTime(), f)
|
||||
}
|
||||
|
||||
var pieSig string
|
||||
if eepromDir != "" {
|
||||
log.Printf("EEPROM directory: %s", eepromDir)
|
||||
log.Printf("(gokrazy config mtime: %v)", p.Cfg.Meta.LastModified)
|
||||
log.Printf("EEPROM update summary:")
|
||||
pieSig, err := writeEepromUpdateFile(filepath.Join(eepromDir, "pieeprom-*.bin"), "/pieeprom.upd")
|
||||
eepromGlob := filepath.Join(eepromDir, "pieeprom-*.bin")
|
||||
eepromMatches, err := filepath.Glob(eepromGlob)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vlSig, err := writeEepromUpdateFile(filepath.Join(eepromDir, "vl805-*.bin"), "/vl805.bin")
|
||||
if len(eepromMatches) == 0 {
|
||||
return fmt.Errorf("invalid -eeprom_package: no files matching %s", filepath.Base(eepromGlob))
|
||||
}
|
||||
if ee := p.Cfg.BootloaderExtraEEPROM; len(ee) > 0 {
|
||||
updated, err := applyExtraEEPROM(bestMatch(eepromMatches), ee)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pieSig, err = writeEepromUpdate("/pieeprom.upd", p.Cfg.Meta.LastModified, bytes.NewReader(updated))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
pieSig, err = writeEepromUpdateFile(eepromMatches, "/pieeprom.upd")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
vl805Glob := filepath.Join(eepromDir, "vl805-*.bin")
|
||||
vl805Matches, err := filepath.Glob(vl805Glob)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var vlSig string
|
||||
if len(vl805Matches) > 0 {
|
||||
vlSig, err = writeEepromUpdateFile(vl805Matches, "/vl805.bin")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
targetFilename := "/recovery.bin"
|
||||
if pieSig == p.ExistingEEPROM.PieepromSHA256 &&
|
||||
vlSig == p.ExistingEEPROM.VL805SHA256 {
|
||||
log.Printf(" installing recovery.bin as RECOVERY.000 (EEPROM already up-to-date)")
|
||||
targetFilename = "/RECOVERY.000"
|
||||
}
|
||||
if _, err := writeEepromUpdateFile(filepath.Join(eepromDir, "recovery.bin"), targetFilename); err != nil {
|
||||
if _, err := writeEepromUpdateFile([]string{filepath.Join(eepromDir, "recovery.bin")}, targetFilename); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -397,6 +443,7 @@ type FileInfo struct {
|
||||
FromHost string
|
||||
FromLiteral string
|
||||
SymlinkDest string
|
||||
Xattrs map[string][]byte
|
||||
|
||||
Dirents []*FileInfo
|
||||
}
|
||||
@@ -459,12 +506,79 @@ func (fi *FileInfo) mustFindDirent(path string) *FileInfo {
|
||||
return nil
|
||||
}
|
||||
|
||||
func addToFileInfo(parent *FileInfo, path string) (time.Time, error) {
|
||||
entries, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
var latestTime time.Time
|
||||
for _, entry := range entries {
|
||||
filename := entry.Name()
|
||||
// get existing file info
|
||||
var fi *FileInfo
|
||||
for _, ent := range parent.Dirents {
|
||||
if ent.Filename == filename {
|
||||
fi = ent
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
info, err = os.Stat(filepath.Join(path, filename))
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if latestTime.Before(info.ModTime()) {
|
||||
latestTime = info.ModTime()
|
||||
}
|
||||
|
||||
// or create if not exist
|
||||
if fi == nil {
|
||||
fi = &FileInfo{
|
||||
Filename: filename,
|
||||
Mode: info.Mode(),
|
||||
}
|
||||
parent.Dirents = append(parent.Dirents, fi)
|
||||
} else {
|
||||
// file overwrite is not supported -> return error
|
||||
if !info.IsDir() || fi.FromHost != "" || fi.FromLiteral != "" {
|
||||
return time.Time{}, fmt.Errorf("file already exists in filesystem: %s", filepath.Join(path, filename))
|
||||
}
|
||||
}
|
||||
|
||||
// add content
|
||||
if info.IsDir() {
|
||||
modTime, err := addToFileInfo(fi, filepath.Join(path, filename))
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
if latestTime.Before(modTime) {
|
||||
latestTime = modTime
|
||||
}
|
||||
} else {
|
||||
fi.FromHost = filepath.Join(path, filename)
|
||||
}
|
||||
}
|
||||
|
||||
return latestTime, nil
|
||||
}
|
||||
|
||||
type foundBin struct {
|
||||
gokrazyPath string
|
||||
hostPath string
|
||||
}
|
||||
|
||||
func findBins(cfg *config.Struct, buildEnv *packer.BuildEnv, bindir string, basenames map[string]string) (*FileInfo, []foundBin, error) {
|
||||
func findBins(cfg *config.Struct, buildEnv *packer.BuildEnv, bindir string, basenames map[string]string, xattrs map[string]map[string][]byte) (*FileInfo, []foundBin, error) {
|
||||
var found []foundBin
|
||||
result := FileInfo{Filename: ""}
|
||||
|
||||
@@ -519,15 +633,20 @@ func findBins(cfg *config.Struct, buildEnv *packer.BuildEnv, bindir string, base
|
||||
}
|
||||
user := FileInfo{Filename: "user"}
|
||||
for _, pkg := range mainPkgs {
|
||||
xattr := make(map[string][]byte)
|
||||
basename := pkg.Basename()
|
||||
if basenameOverride, ok := basenames[pkg.ImportPath]; ok {
|
||||
basename = basenameOverride
|
||||
}
|
||||
if xattrsOverride, ok := xattrs[pkg.ImportPath]; ok {
|
||||
xattr = xattrsOverride
|
||||
}
|
||||
binPath := filepath.Join(bindir, basename)
|
||||
fileIsELFOrFatal(binPath)
|
||||
user.Dirents = append(user.Dirents, &FileInfo{
|
||||
Filename: basename,
|
||||
FromHost: binPath,
|
||||
Xattrs: xattr,
|
||||
})
|
||||
found = append(found, foundBin{
|
||||
gokrazyPath: "/user/" + basename,
|
||||
@@ -540,14 +659,14 @@ func findBins(cfg *config.Struct, buildEnv *packer.BuildEnv, bindir string, base
|
||||
|
||||
func writeFileInfo(dir *squashfs.Directory, fi *FileInfo) error {
|
||||
if fi.FromHost != "" { // copy a regular file
|
||||
return copyFileSquash(dir, fi.Filename, fi.FromHost)
|
||||
return copyFileSquash(dir, fi.Filename, fi.FromHost, fi.Xattrs)
|
||||
}
|
||||
if fi.FromLiteral != "" { // write a regular file
|
||||
mode := fi.Mode
|
||||
if mode == 0 {
|
||||
mode = 0444
|
||||
}
|
||||
w, err := dir.File(fi.Filename, time.Now(), mode)
|
||||
w, err := dir.File(fi.Filename, time.Now(), mode, fi.Xattrs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -578,6 +697,18 @@ func writeFileInfo(dir *squashfs.Directory, fi *FileInfo) error {
|
||||
return d.Flush()
|
||||
}
|
||||
|
||||
func (p *Pack) writeRootFile(filename string, root *FileInfo) error {
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
if err := p.writeRoot(f, root); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
func (p *Pack) writeRoot(f io.WriteSeeker, root *FileInfo) error {
|
||||
log := p.Env.Logger()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user