diff --git a/go.mod b/go.mod index 0ea4dc65..22676e71 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Masterminds/semver/v3 v3.3.0 github.com/Masterminds/sprig/v3 v3.3.0 github.com/cavaliergopher/grab/v3 v3.0.1 + github.com/diskfs/go-diskfs v1.4.1 github.com/erikgeiser/promptkit v0.9.0 github.com/google/go-containerregistry v0.20.2 github.com/hashicorp/go-multierror v1.1.1 @@ -25,7 +26,7 @@ require ( github.com/pterm/pterm v0.12.79 github.com/rs/zerolog v1.33.0 github.com/sanity-io/litter v1.5.5 - github.com/sirupsen/logrus v1.9.3 + github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af github.com/spf13/viper v1.19.0 github.com/urfave/cli/v2 v2.27.4 golang.org/x/net v0.29.0 @@ -37,6 +38,7 @@ require ( require ( github.com/distribution/reference v0.6.0 + github.com/gofrs/uuid v4.4.0+incompatible github.com/google/go-github/v64 v64.0.0 github.com/google/go-github/v64 v64.0.0 github.com/twpayne/go-vfs/v5 v5.0.4 @@ -67,6 +69,8 @@ require ( github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/containerd/cgroups v1.1.0 // indirect + github.com/containerd/console v1.0.4-0.20230706203907-8f6c4e4faef5 // indirect + github.com/containerd/containerd v1.7.20 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/containerd/containerd v1.7.21 // indirect github.com/containerd/continuity v0.4.2 // indirect @@ -79,7 +83,7 @@ require ( github.com/denisbrodbeck/machineid v1.0.1 // indirect github.com/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d // indirect github.com/disintegration/imaging v1.6.2 // indirect - github.com/diskfs/go-diskfs v1.3.0 // indirect + github.com/djherbis/times v1.6.0 // indirect github.com/docker/cli v27.1.1+incompatible // indirect github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/docker v27.2.0+incompatible // indirect @@ -88,6 +92,7 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect github.com/eliukblau/pixterm v1.3.1 // indirect + github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/foxboron/go-uefi v0.0.0-20240805124652-e2076f0e58ca // indirect @@ -103,7 +108,6 @@ require ( github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gofrs/flock v0.8.1 // indirect - github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -159,7 +163,7 @@ require ( github.com/packethost/packngo v0.29.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee // indirect - github.com/pierrec/lz4 v2.6.1+incompatible // indirect + github.com/pierrec/lz4/v4 v4.1.17 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkg/xattr v0.4.9 // indirect @@ -224,7 +228,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect google.golang.org/grpc v1.62.1 // indirect google.golang.org/protobuf v1.34.1 // indirect - gopkg.in/djherbis/times.v1 v1.3.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/go.sum b/go.sum index 52804e8b..7a94770e 100644 --- a/go.sum +++ b/go.sum @@ -90,8 +90,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/containerd/console v1.0.4-0.20230706203907-8f6c4e4faef5 h1:Ig+OPkE3XQrrl+SKsOqAjlkrBN/zrr+Qpw7rCuDjRCE= +github.com/containerd/console v1.0.4-0.20230706203907-8f6c4e4faef5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/containerd v1.7.20 h1:Sl6jQYk3TRavaU83h66QMbI2Nqg9Jm6qzwX57Vsn1SQ= github.com/containerd/containerd v1.7.20/go.mod h1:52GsS5CwquuqPuLncsXwG0t2CiUce+KsNHJZQJvAgR0= github.com/containerd/containerd v1.7.21 h1:USGXRK1eOC/SX0L195YgxTHb0a00anxajOzgfN0qrCA= @@ -122,10 +122,12 @@ github.com/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d h1:CPqTNIigGwe github.com/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d/go.mod h1:QX5ZVULjAfZJux/W62Y91HvCh9hyW6enAwcrrv/sLj0= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= -github.com/diskfs/go-diskfs v1.3.0 h1:D3IVe1y7ybB5SjCO0pOmkWThL9lZEWeanp8rRa0q0sk= -github.com/diskfs/go-diskfs v1.3.0/go.mod h1:3pUpCAz75Q11om5RsGpVKUgXp2Z+ATw1xV500glmCP0= +github.com/diskfs/go-diskfs v1.4.1 h1:iODgkzHLmvXS+1VDztpW53T+dQm8GQzi20y9yUd5UCA= +github.com/diskfs/go-diskfs v1.4.1/go.mod h1:+tOkQs8CMMog6Nvljg8DGIxEXrgL48iyT3OM3IlSz74= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= +github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE= github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= @@ -147,6 +149,8 @@ github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcej github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/eliukblau/pixterm v1.3.1 h1:XeouQViH+lmzCa7sMUoK2cd7qlgHYGLIjwRKaOdJbKA= github.com/eliukblau/pixterm v1.3.1/go.mod h1:on5ueknFt+ZFVvIVVzQ7/JXwPjv5fJd8Q1Ybh7XixfU= +github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY= +github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -159,7 +163,6 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/foxboron/go-uefi v0.0.0-20240805124652-e2076f0e58ca h1:ErxkaWK5AIt8gQf3KpAuQQBdZI4ps72HzEe123kh+So= github.com/foxboron/go-uefi v0.0.0-20240805124652-e2076f0e58ca/go.mod h1:ffg/fkDeOYicEQLoO2yFFGt00KUTYVXI+rfnc8il6vQ= -github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -226,7 +229,6 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -242,7 +244,6 @@ github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSF github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -311,7 +312,6 @@ github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuOb github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -422,14 +422,12 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6 github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee h1:P6U24L02WMfj9ymZTxl7CxS73JC99x3ukk+DBkgQGQs= github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY= -github.com/pierrec/lz4 v2.3.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= -github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= +github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/xattr v0.4.1/go.mod h1:W2cGD0TBEus7MkUgv0tNZ9JutLtVO3cXu+IBRuHqnFs= github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE= github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -492,8 +490,8 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0= +github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= @@ -676,7 +674,6 @@ golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181021155630-eda9bb28ed51/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -697,12 +694,12 @@ golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -796,9 +793,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/djherbis/times.v1 v1.2.0/go.mod h1:AQlg6unIsrsCEdQYhTzERy542dz6SFdQFZFv6mUY0P8= -gopkg.in/djherbis/times.v1 v1.3.0 h1:uxMS4iMtH6Pwsxog094W0FYldiNnfY/xba00vq6C2+o= -gopkg.in/djherbis/times.v1 v1.3.0/go.mod h1:AQlg6unIsrsCEdQYhTzERy542dz6SFdQFZFv6mUY0P8= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/internal/agent/install_test.go b/internal/agent/install_test.go index f257001b..e9a8a00c 100644 --- a/internal/agent/install_test.go +++ b/internal/agent/install_test.go @@ -1,7 +1,6 @@ package agent import ( - "fmt" v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1" "os" "path/filepath" @@ -19,11 +18,6 @@ import ( . "github.com/onsi/gomega" ) -const printOutput = `BYT; -/dev/loop0:50593792s:loopback:512:512:gpt:Loopback device:;` -const partTmpl = ` -%d:%ss:%ss:2048s:ext4::type=83;` - var _ = Describe("prepareConfiguration", func() { url := "https://example.com" @@ -95,24 +89,8 @@ var _ = Describe("RunInstall", func() { Expect(err).To(BeNil()) // Side effect of runners, hijack calls to commands and return our stuff - partNum := 0 - partedOut := printOutput runner.SideEffect = func(cmd string, args ...string) ([]byte, error) { switch cmd { - case "parted": - idx := 0 - for i, arg := range args { - if arg == "mkpart" { - idx = i - break - } - } - if idx > 0 { - partNum++ - partedOut += fmt.Sprintf(partTmpl, partNum, args[idx+3], args[idx+4]) - _, _ = fs.Create(fmt.Sprintf("/some/device%d", partNum)) - } - return []byte(partedOut), nil case "lsblk": return []byte(`{ "blockdevices": diff --git a/pkg/action/bootentries.go b/pkg/action/bootentries.go index b3367831..7f789ad1 100644 --- a/pkg/action/bootentries.go +++ b/pkg/action/bootentries.go @@ -42,7 +42,7 @@ func ListBootEntries(cfg *config.Config) error { } } -// selectBootEntryGrub sets the default boot entry to the selected entry via `grub2-editenv /oem/grubenv set next_entry=entry` +// selectBootEntryGrub sets the default boot entry to the selected entry by modifying /oem/grubenv // also validates that the entry exists in our list of entries func selectBootEntryGrub(cfg *config.Config, entry string) error { // Validate if entry exists @@ -56,10 +56,13 @@ func selectBootEntryGrub(cfg *config.Config, entry string) error { return err } cfg.Logger.Infof("Setting default boot entry to %s", entry) - // Set the default entry to the selected entry via `grub2-editenv /oem/grubenv set next_entry=statereset` - out, err := cfg.Runner.Run("grub2-editenv", "/oem/grubenv", "set", fmt.Sprintf("next_entry=%s", entry)) + // Set the default entry to the selected entry on /oem/grubenv + vars := map[string]string{ + "next_entry": entry, + } + err = utils.SetPersistentVariables("/oem/grubenv", vars, cfg.Fs) if err != nil { - cfg.Logger.Errorf("could not set default boot entry: %s\noutput: %s", err, out) + cfg.Logger.Errorf("could not set default boot entry: %s\n", err) return err } cfg.Logger.Infof("Default boot entry set to %s", entry) diff --git a/pkg/action/bootentries_test.go b/pkg/action/bootentries_test.go index 6cb304da..6104c107 100644 --- a/pkg/action/bootentries_test.go +++ b/pkg/action/bootentries_test.go @@ -645,8 +645,6 @@ var _ = Describe("Bootentries tests", Label("bootentry"), func() { BeforeEach(func() { runner.SideEffect = func(cmd string, args ...string) ([]byte, error) { switch cmd { - case "grub2-editenv": - return []byte(""), nil default: return []byte{}, nil } @@ -664,13 +662,14 @@ var _ = Describe("Bootentries tests", Label("bootentry"), func() { Expect(err).ToNot(HaveOccurred()) err = fs.WriteFile("/etc/kairos/branding/grubmenu.cfg", []byte("whatever whatever --id kairos3 {"), os.ModePerm) Expect(err).ToNot(HaveOccurred()) + Expect(fs.Mkdir("/oem", os.ModePerm)).To(Succeed()) err = SelectBootEntry(config, "kairos") Expect(err).ToNot(HaveOccurred()) - Expect(runner.IncludesCmds([][]string{ - {"grub2-editenv", "/oem/grubenv", "set", "next_entry=kairos"}, - })).ToNot(HaveOccurred()) Expect(memLog.String()).To(ContainSubstring("Default boot entry set to kairos")) + variables, err := utils.ReadPersistentVariables("/oem/grubenv", fs) + Expect(err).ToNot(HaveOccurred()) + Expect(variables["next_entry"]).To(Equal("kairos")) }) }) }) diff --git a/pkg/action/install.go b/pkg/action/install.go index 1839ac03..380ff9fe 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -293,7 +293,7 @@ func (i InstallAction) Run() (err error) { } // If we want to eject the cd, create the required executable so the cd is ejected at shutdown - out, _ := i.cfg.Runner.Run("cat", "/proc/cmdline") + out, _ := i.cfg.Fs.ReadFile("/proc/cmdline") bootedFromCD := strings.Contains(string(out), "cdroot") if i.cfg.EjectCD && bootedFromCD { diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 4ce8aab9..7a8002cc 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -19,7 +19,9 @@ package action_test import ( "bytes" "fmt" + "github.com/diskfs/go-diskfs" sdkTypes "github.com/kairos-io/kairos-sdk/types" + "os" "path/filepath" "regexp" @@ -39,11 +41,6 @@ import ( "github.com/twpayne/go-vfs/v4/vfst" ) -const printOutput = `BYT; -/dev/loop0:50593792s:loopback:512:512:gpt:Loopback device:;` -const partTmpl = ` -%d:%ss:%ss:2048s:ext4::type=83;` - var _ = Describe("Install action tests", func() { var config *agentConfig.Config var runner *v1mock.FakeRunner @@ -68,7 +65,11 @@ var _ = Describe("Install action tests", func() { extractor = v1mock.NewFakeImageExtractor(logger) logger.SetLevel("debug") var err error - fs, cleanup, err = vfst.NewTestFS(map[string]interface{}{}) + // create fake files needed for the loop device to "work" + fs, cleanup, err = vfst.NewTestFS(map[string]interface{}{ + "/dev/loop-control": "", + "/dev/loop0": "", + }) Expect(err).Should(BeNil()) cloudInit = &v1mock.FakeCloudInitRunner{} @@ -94,20 +95,22 @@ var _ = Describe("Install action tests", func() { Describe("Install Action", Label("install"), func() { var device, cmdFail string var err error - var cmdline func() ([]byte, error) var spec *v1.InstallSpec var installer *action.InstallAction BeforeEach(func() { - device = "/some/device" + device = "/tmp/test.img" + Expect(os.RemoveAll(device)).Should(Succeed()) + // at least 2Gb in size as state is set to 1G + _, err = diskfs.Create(device, 2*1024*1024*1024, diskfs.Raw, 512) + Expect(err).ToNot(HaveOccurred()) + config.Install.Device = device err = fsutils.MkdirAll(fs, filepath.Dir(device), constants.DirPerm) Expect(err).To(BeNil()) _, err = fs.Create(device) Expect(err).ShouldNot(HaveOccurred()) - partNum := 0 - partedOut := printOutput cmdFail = "" runner.SideEffect = func(cmd string, args ...string) ([]byte, error) { regexCmd := regexp.MustCompile(cmdFail) @@ -115,20 +118,6 @@ var _ = Describe("Install action tests", func() { return []byte{}, fmt.Errorf("failed on %s", cmd) } switch cmd { - case "parted": - idx := 0 - for i, arg := range args { - if arg == "mkpart" { - idx = i - break - } - } - if idx > 0 { - partNum++ - partedOut += fmt.Sprintf(partTmpl, partNum, args[idx+3], args[idx+4]) - _, _ = fs.Create(fmt.Sprintf("/some/device%d", partNum)) - } - return []byte(partedOut), nil case "lsblk": return []byte(`{ "blockdevices": @@ -140,11 +129,6 @@ var _ = Describe("Install action tests", func() { {"label": "COS_PERSISTENT", "type": "part", "path": "/some/device4"} ] }`), nil - case "cat": - if args[0] == "/proc/cmdline" { - return cmdline() - } - return []byte{}, nil default: return []byte{}, nil } @@ -155,6 +139,7 @@ var _ = Describe("Install action tests", func() { spec, err = agentConfig.NewInstallSpec(config) Expect(err).ToNot(HaveOccurred()) + Expect(spec.Sanitize()).To(Succeed()) spec.Active.Size = 16 grubCfg := filepath.Join(spec.Active.MountPoint, constants.GrubConf) @@ -163,10 +148,6 @@ var _ = Describe("Install action tests", func() { _, err = fs.Create(grubCfg) Expect(err).To(BeNil()) - // Set default cmdline function so we dont panic :o - cmdline = func() ([]byte, error) { - return []byte{}, nil - } mainDisk := block.Disk{ Name: "device", Partitions: []*block.Partition{ @@ -214,6 +195,10 @@ var _ = Describe("Install action tests", func() { installer = action.NewInstallAction(config, spec) }) AfterEach(func() { + if CurrentSpecReport().Failed() { + GinkgoWriter.Printf(memLog.String()) + } + Expect(os.RemoveAll(device)).ToNot(HaveOccurred()) ghwTest.Clean() }) @@ -227,9 +212,9 @@ var _ = Describe("Install action tests", func() { _, err := fs.Stat("/usr/lib/systemd/system-shutdown/eject") Expect(err).To(HaveOccurred()) // Override cmdline to return like we are booting from cd - cmdline = func() ([]byte, error) { - return []byte("cdroot"), nil - } + _ = fsutils.MkdirAll(fs, "/proc", constants.DirPerm) + Expect(fs.WriteFile("/proc/cmdline", []byte("cdroot"), constants.FilePerm)).ToNot(HaveOccurred()) + spec.Target = device config.EjectCD = true Expect(installer.Run()).To(BeNil()) @@ -329,14 +314,6 @@ var _ = Describe("Install action tests", func() { spec.Target = device cmdFail = "blkdeactivate" Expect(installer.Run()).NotTo(BeNil()) - Expect(runner.MatchMilestones([][]string{{"parted"}})) - }) - - It("Fails on parted errors", Label("disk", "partitions"), func() { - spec.Target = device - cmdFail = "parted" - Expect(installer.Run()).NotTo(BeNil()) - Expect(runner.MatchMilestones([][]string{{"parted"}})) }) It("Fails to unmount partitions", Label("disk", "partitions"), func() { @@ -381,13 +358,5 @@ var _ = Describe("Install action tests", func() { Expect(installer.Run()).NotTo(BeNil()) Expect(runner.MatchMilestones([][]string{{"tune2fs", "-L", constants.PassiveLabel}})) }) - - It("Fails setting the grub default entry", Label("grub"), func() { - spec.Target = device - spec.GrubDefEntry = "cOS" - cmdFail = utils.FindCommand("grub2-editenv", []string{"grub2-editenv", "grub-editenv"}) - Expect(installer.Run()).NotTo(BeNil()) - Expect(runner.MatchMilestones([][]string{{cmdFail, filepath.Join(constants.StateDir, constants.GrubOEMEnv)}})) - }) }) }) diff --git a/pkg/action/reset_test.go b/pkg/action/reset_test.go index 0cb36c2d..a747b023 100644 --- a/pkg/action/reset_test.go +++ b/pkg/action/reset_test.go @@ -61,7 +61,10 @@ var _ = Describe("Reset action tests", func() { logger = sdkTypes.NewBufferLogger(memLog) extractor = v1mock.NewFakeImageExtractor(logger) var err error - fs, cleanup, err = vfst.NewTestFS(map[string]interface{}{}) + fs, cleanup, err = vfst.NewTestFS(map[string]interface{}{ + "/dev/loop-control": "", + "/dev/loop0": "", + }) Expect(err).Should(BeNil()) cloudInit = &v1mock.FakeCloudInitRunner{} diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 73af2e38..b2ea6eeb 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -19,6 +19,7 @@ package action import ( "fmt" "path/filepath" + "syscall" "time" agentConfig "github.com/kairos-io/kairos-agent/v2/pkg/config" @@ -249,7 +250,7 @@ func (u *UpgradeAction) Run() (err error) { u.Info("Backing up current active image") source := filepath.Join(u.spec.Partitions.State.MountPoint, "cOS", constants.ActiveImgFile) u.Info("Moving %s to %s", source, u.spec.Passive.File) - _, err := u.config.Runner.Run("mv", "-f", source, u.spec.Passive.File) + err = u.config.Fs.Rename(source, u.spec.Passive.File) if err != nil { u.Error("Failed to move %s to %s: %s", source, u.spec.Passive.File, err) return err @@ -262,18 +263,18 @@ func (u *UpgradeAction) Run() (err error) { u.Debug("Error while labeling the passive image %s, command output: %s", u.spec.Passive.File, out) return err } - _, _ = u.config.Runner.Run("sync") + syscall.Sync() } u.Info("Moving %s to %s", upgradeImg.File, finalImageFile) - _, err = u.config.Runner.Run("mv", "-f", upgradeImg.File, finalImageFile) + err = u.config.Fs.Rename(upgradeImg.File, finalImageFile) if err != nil { u.Error("Failed to move %s to %s: %s", upgradeImg.File, finalImageFile, err) return err } u.Info("Finished moving %s to %s", upgradeImg.File, finalImageFile) - _, _ = u.config.Runner.Run("sync") + syscall.Sync() err = u.upgradeHook(constants.AfterUpgradeHook, false) if err != nil { diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index fdea4991..44c57e0a 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -42,7 +42,7 @@ import ( "github.com/twpayne/go-vfs/v4/vfst" ) -var _ = Describe("Runtime Actions", func() { +var _ = Describe("Upgrade Actions test", func() { var config *agentConfig.Config var runner *v1mock.FakeRunner var fs vfs.FS @@ -69,7 +69,10 @@ var _ = Describe("Runtime Actions", func() { logger.SetLevel("debug") extractor = v1mock.NewFakeImageExtractor(logger) var err error - fs, cleanup, err = vfst.NewTestFS(map[string]interface{}{}) + fs, cleanup, err = vfst.NewTestFS(map[string]interface{}{ + "/dev/loop-control": "", + "/dev/loop0": "", + }) Expect(err).Should(BeNil()) cloudInit = &v1mock.FakeCloudInitRunner{} @@ -235,9 +238,6 @@ var _ = Describe("Runtime Actions", func() { }) It("Fails if some hook fails and strict is set", func() { runner.SideEffect = func(command string, args ...string) ([]byte, error) { - if command == "cat" && args[0] == "/proc/cmdline" { - return []byte(constants.ActiveLabel), nil - } return []byte{}, nil } config.Strict = true @@ -255,7 +255,7 @@ var _ = Describe("Runtime Actions", func() { Expect(err).ToNot(HaveOccurred()) // Check that the rebrand worked with our os-release value - Expect(memLog).To(ContainSubstring("default_menu_entry=TESTOS")) + Expect(memLog).To(ContainSubstring("Setting default grub entry to TESTOS"), memLog.String()) // This should be the new image info, err := fs.Stat(activeImg) @@ -287,7 +287,7 @@ var _ = Describe("Runtime Actions", func() { Expect(err).ToNot(HaveOccurred()) By("Checking the log") // Check that the rebrand worked with our os-release value - Expect(memLog).To(ContainSubstring("default_menu_entry=TESTOS")) + Expect(memLog).To(ContainSubstring("Setting default grub entry to TESTOS")) By("checking active image") // This should be the new image @@ -321,7 +321,7 @@ var _ = Describe("Runtime Actions", func() { Expect(err).ToNot(HaveOccurred()) // Check that the rebrand worked with our os-release value - Expect(memLog).To(ContainSubstring("default_menu_entry=TESTOS")) + Expect(memLog).To(ContainSubstring("Setting default grub entry to TESTOS")) // This should be the new image info, err := fs.Stat(activeImg) @@ -359,7 +359,7 @@ var _ = Describe("Runtime Actions", func() { Expect(err).ToNot(HaveOccurred()) // Check that the rebrand worked with our os-release value - Expect(memLog).To(ContainSubstring("default_menu_entry=TESTOS")) + Expect(memLog).To(ContainSubstring("Setting default grub entry to TESTOS")) // Not much that we can create here as the dir copy was done on the real os, but we do the rest of the ops on a mem one // This should be the new image @@ -447,7 +447,7 @@ var _ = Describe("Runtime Actions", func() { Expect(err).ToNot(HaveOccurred()) // Check that the rebrand worked with our os-release value - Expect(memLog).To(ContainSubstring("default_menu_entry=TESTOS")) + Expect(memLog).To(ContainSubstring("Setting default grub entry to TESTOS")) // This should be the new image info, err := fs.Stat(activeImg) diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 4fe6f017..cbf93958 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -18,6 +18,7 @@ package constants import ( "errors" + "github.com/gofrs/uuid" "os" "strings" ) @@ -203,3 +204,6 @@ func BootTitleForRole(role, title string) (string, error) { return "", errors.New("invalid role") } } + +// DiskUUID is the static UUID for main disk identification +var DiskUUID = uuid.NewV5(uuid.NamespaceURL, "KAIROS_DISK").String() diff --git a/pkg/elemental/elemental.go b/pkg/elemental/elemental.go index a4cd2dc8..6bf59d3b 100644 --- a/pkg/elemental/elemental.go +++ b/pkg/elemental/elemental.go @@ -19,15 +19,19 @@ package elemental import ( "errors" "fmt" - v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1" - "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs" + "os" "path/filepath" - "strings" + "syscall" + diskfs "github.com/diskfs/go-diskfs/disk" + "github.com/diskfs/go-diskfs/partition/gpt" agentConfig "github.com/kairos-io/kairos-agent/v2/pkg/config" cnst "github.com/kairos-io/kairos-agent/v2/pkg/constants" "github.com/kairos-io/kairos-agent/v2/pkg/partitioner" + v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1" "github.com/kairos-io/kairos-agent/v2/pkg/utils" + "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs" + "github.com/kairos-io/kairos-agent/v2/pkg/utils/loop" ) // Elemental is the struct meant to self-contain most utils and actions related to Elemental, like installing or applying selinux @@ -58,64 +62,69 @@ func (e *Elemental) FormatPartition(part *v1.Partition, opts ...string) error { // and applies the configured disk layout by creating and formatting all // required partitions func (e *Elemental) PartitionAndFormatDevice(i v1.SharedInstallSpec) error { - disk := partitioner.NewDisk( - i.GetTarget(), - partitioner.WithRunner(e.config.Runner), - partitioner.WithFS(e.config.Fs), - partitioner.WithLogger(e.config.Logger), - ) - - if !disk.Exists() { + if _, err := os.Stat(i.GetTarget()); os.IsNotExist(err) { e.config.Logger.Errorf("Disk %s does not exist", i.GetTarget()) return fmt.Errorf("disk %s does not exist", i.GetTarget()) } - e.config.Logger.Infof("Partitioning device...") - out, err := disk.NewPartitionTable(i.GetPartTable()) + disk, err := partitioner.NewDisk(i.GetTarget(), partitioner.WithLogger(e.config.Logger)) if err != nil { - e.config.Logger.Errorf("Failed creating new partition table: %s", out) return err } - parts := i.GetPartitions().PartitionsByInstallOrder(i.GetExtraPartitions()) - return e.createPartitions(disk, parts) -} - -func (e *Elemental) createAndFormatPartition(disk *partitioner.Disk, part *v1.Partition) error { - e.config.Logger.Debugf("Adding partition %s", part.Name) - num, err := disk.AddPartition(part.Size, part.FS, part.Name, part.Flags...) - if err != nil { - e.config.Logger.Errorf("Failed creating %s partition", part.Name) - return err - } - partDev, err := disk.FindPartitionDevice(num) + e.config.Logger.Infof("Partitioning device...") + err = disk.NewPartitionTable(i.GetPartTable(), i.GetPartitions().PartitionsByInstallOrder(i.GetExtraPartitions())) if err != nil { + e.config.Logger.Errorf("Failed creating new partition table: %s", err) return err } - if part.FS != "" { - e.config.Logger.Debugf("Formatting partition with label %s", part.FilesystemLabel) - err = partitioner.FormatDevice(e.config.Runner, partDev, part.FS, part.FilesystemLabel) - if err != nil { - e.config.Logger.Errorf("Failed formatting partition %s", part.Name) - return err - } - } else { - e.config.Logger.Debugf("Wipe file system on %s", part.Name) - err = disk.WipeFsOnPartition(partDev) + + // Only re-read table on devices. On files there is no need and this call will fail + if disk.Type == diskfs.Device { + err = disk.ReReadPartitionTable() if err != nil { - e.config.Logger.Errorf("Failed to wipe filesystem of partition %s", partDev) + e.config.Logger.Errorf("Reread table: %s", err) return err } } - part.Path = partDev - return nil -} -func (e *Elemental) createPartitions(disk *partitioner.Disk, parts v1.PartitionList) error { - for _, part := range parts { - err := e.createAndFormatPartition(disk, part) - if err != nil { - return err + table, err := disk.GetPartitionTable() + if err != nil { + e.config.Logger.Errorf("table: %s", err) + return err + } + err = disk.Close() + if err != nil { + e.config.Logger.Errorf("Close disk: %s", err) + } + // Sync changes + syscall.Sync() + // Trigger udevadm to refresh devices + _, err = e.config.Runner.Run("udevadm", "trigger") + if err != nil { + e.config.Logger.Errorf("Udevadm trigger failed: %s", err) + } + _, err = e.config.Runner.Run("udevadm", "settle") + if err != nil { + e.config.Logger.Errorf("Udevadm settle failed: %s", err) + } + // Partitions are in order so we can format them via that + for index, p := range table.GetPartitions() { + for _, configPart := range i.GetPartitions().PartitionsByInstallOrder(i.GetExtraPartitions()) { + if configPart.Name == cnst.BiosPartName { + // Grub partition on non-EFI is not formatted. Grub is directly installed on it + continue + } + // we have to match the Fs it was asked with the partition in the system + if p.(*gpt.Partition).Name == configPart.FilesystemLabel { + e.config.Logger.Debugf("Formatting partition: %s", configPart.FilesystemLabel) + err = partitioner.FormatDevice(e.config.Runner, fmt.Sprintf("%s%d", i.GetTarget(), index+1), configPart.FS, configPart.FilesystemLabel) + if err != nil { + e.config.Logger.Errorf("Failed formatting partition: %s", err) + return err + } + syscall.Sync() + } } } return nil @@ -224,17 +233,18 @@ func (e Elemental) MountImage(img *v1.Image, opts ...string) error { if err != nil { return err } - out, err := e.config.Runner.Run("losetup", "--show", "-f", img.File) + loopDevice, err := loop.Loop(img, e.config) if err != nil { return err } - loop := strings.TrimSpace(string(out)) - err = e.config.Mounter.Mount(loop, img.MountPoint, "auto", opts) + + err = e.config.Mounter.Mount(loopDevice, img.MountPoint, "auto", opts) if err != nil { - _, _ = e.config.Runner.Run("losetup", "-d", loop) return err } - img.LoopDevice = loop + + // Store the loop device so we can later detach it + img.LoopDevice = loopDevice return nil } @@ -252,7 +262,10 @@ func (e Elemental) UnmountImage(img *v1.Image) error { if err != nil { return err } - _, err = e.config.Runner.Run("losetup", "-d", img.LoopDevice) + err = loop.Unloop(img.LoopDevice, e.config) + if err != nil { + return err + } img.LoopDevice = "" return err } @@ -548,10 +561,10 @@ func (e Elemental) SetDefaultGrubEntry(partMountPoint string, imgMountPoint stri } } e.config.Logger.Infof("Setting default grub entry to %s", defaultEntry) - grub := utils.NewGrub(e.config) - return grub.SetPersistentVariables( + return utils.SetPersistentVariables( filepath.Join(partMountPoint, cnst.GrubOEMEnv), map[string]string{"default_menu_entry": defaultEntry}, + e.config.Fs, ) } diff --git a/pkg/elemental/elemental_test.go b/pkg/elemental/elemental_test.go index 31fea99d..a340c151 100644 --- a/pkg/elemental/elemental_test.go +++ b/pkg/elemental/elemental_test.go @@ -17,11 +17,21 @@ package elemental_test import ( + "bytes" "errors" "fmt" + "github.com/diskfs/go-diskfs" + "github.com/diskfs/go-diskfs/partition/gpt" + "github.com/gofrs/uuid" + "github.com/kairos-io/kairos-agent/v2/pkg/utils" sdkTypes "github.com/kairos-io/kairos-sdk/types" + "github.com/sanity-io/litter" + "golang.org/x/sys/unix" "os" "path/filepath" + "reflect" + "strings" + sc "syscall" "testing" agentConfig "github.com/kairos-io/kairos-agent/v2/pkg/config" @@ -32,7 +42,6 @@ import ( cnst "github.com/kairos-io/kairos-agent/v2/pkg/constants" "github.com/kairos-io/kairos-agent/v2/pkg/elemental" v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1" - "github.com/kairos-io/kairos-agent/v2/pkg/utils" v1mock "github.com/kairos-io/kairos-agent/v2/tests/mocks" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -40,11 +49,6 @@ import ( "k8s.io/mount-utils" ) -const printOutput = `BYT; -/dev/loop0:50593792s:loopback:512:512:gpt:Loopback device:;` -const partTmpl = ` -%d:%ss:%ss:2048s:ext4::type=83;` - func TestElementalSuite(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Elemental test suite") @@ -54,19 +58,38 @@ var _ = Describe("Elemental", Label("elemental"), func() { var config *agentConfig.Config var runner *v1mock.FakeRunner var logger sdkTypes.KairosLogger - var syscall v1.SyscallInterface + var syscall *v1mock.FakeSyscall var cl *v1mock.FakeHTTPClient var mounter *v1mock.ErrorMounter var fs *vfst.TestFS var cleanup func() var extractor *v1mock.FakeImageExtractor + var memLog *bytes.Buffer + var devLoopInt int BeforeEach(func() { + memLog = &bytes.Buffer{} + logger = sdkTypes.NewBufferLogger(memLog) + logger.SetLevel("debug") runner = v1mock.NewFakeRunner() syscall = &v1mock.FakeSyscall{} + devLoopInt = 44 + syscall.SideEffectSyscall = func(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err sc.Errno) { + // Trap the call for getting a free loop device number + if trap == sc.SYS_IOCTL && a2 == unix.LOOP_CTL_GET_FREE { + // This is a "get free loop device" syscall + // We return 44 so it gets the /dev/loop44 because we are cool like that + // Also we can check below that indeed it set that device as expected + return uintptr(devLoopInt), 0, sc.Errno(syscall.ReturnValue) + } + return 0, 0, sc.Errno(syscall.ReturnValue) + } mounter = v1mock.NewErrorMounter() cl = &v1mock.FakeHTTPClient{} - fs, cleanup, _ = vfst.NewTestFS(nil) + fs, cleanup, _ = vfst.NewTestFS(map[string]interface{}{ + "/dev/loop-control": "", + fmt.Sprintf("/dev/loop%d", devLoopInt): "", + }) extractor = v1mock.NewFakeImageExtractor(logger) config = agentConfig.NewConfig( agentConfig.WithFs(fs), @@ -192,7 +215,6 @@ var _ = Describe("Elemental", Label("elemental"), func() { Expect(err).NotTo(BeNil()) }) }) - Describe("UnmountPartitions", Label("UnmountPartitions", "disk", "partition", "unmount"), func() { var el *elemental.Elemental var parts v1.ElementalPartitions @@ -228,44 +250,43 @@ var _ = Describe("Elemental", Label("elemental"), func() { Expect(err).NotTo(BeNil()) }) }) - Describe("MountImage", Label("MountImage", "mount", "image"), func() { var el *elemental.Elemental var img *v1.Image BeforeEach(func() { el = elemental.NewElemental(config) - img = &v1.Image{MountPoint: "/some/mountpoint"} + img = &v1.Image{MountPoint: "/some/mountpoint", File: "/image.file"} + Expect(fs.WriteFile("/image.file", []byte{}, cnst.FilePerm)).To(Succeed()) }) It("Mounts file system image", func() { - runner.ReturnValue = []byte("/dev/loop") - Expect(el.MountImage(img)).To(BeNil()) - Expect(img.LoopDevice).To(Equal("/dev/loop")) + err := el.MountImage(img) + Expect(err).To(BeNil()) + Expect(img.LoopDevice).To(Equal(fmt.Sprintf("/dev/loop%d", devLoopInt)), litter.Sdump(img)) }) It("Fails to set a loop device", Label("loop"), func() { - runner.ReturnError = errors.New("failed to set a loop device") + // Return error on syscall call + syscall.ReturnValue = 10 Expect(el.MountImage(img)).NotTo(BeNil()) Expect(img.LoopDevice).To(Equal("")) }) It("Fails to mount a loop device", Label("loop"), func() { - runner.ReturnValue = []byte("/dev/loop") mounter.ErrorOnMount = true Expect(el.MountImage(img)).NotTo(BeNil()) Expect(img.LoopDevice).To(Equal("")) }) }) - Describe("UnmountImage", Label("UnmountImage", "mount", "image"), func() { var el *elemental.Elemental var img *v1.Image BeforeEach(func() { - runner.ReturnValue = []byte("/dev/loop") el = elemental.NewElemental(config) - img = &v1.Image{MountPoint: "/some/mountpoint"} + img = &v1.Image{MountPoint: "/some/mountpoint", File: "/image.file"} + Expect(fs.WriteFile("/image.file", []byte{}, cnst.FilePerm)).To(Succeed()) Expect(el.MountImage(img)).To(BeNil()) - Expect(img.LoopDevice).To(Equal("/dev/loop")) + Expect(img.LoopDevice).To(Equal(fmt.Sprintf("/dev/loop%d", devLoopInt))) }) It("Unmounts file system image", func() { @@ -279,11 +300,10 @@ var _ = Describe("Elemental", Label("elemental"), func() { }) It("Fails to unset a loop device", Label("loop"), func() { - runner.ReturnError = errors.New("failed to unset a loop device") + syscall.ReturnValue = 10 Expect(el.UnmountImage(img)).NotTo(BeNil()) }) }) - Describe("CreateFileSystemImage", Label("CreateFileSystemImage", "image"), func() { var el *elemental.Elemental var img *v1.Image @@ -320,7 +340,6 @@ var _ = Describe("Elemental", Label("elemental"), func() { Expect(err).NotTo(BeNil()) }) }) - Describe("FormatPartition", Label("FormatPartition", "partition", "format"), func() { It("Reformats an already existing partition", func() { el := elemental.NewElemental(config) @@ -334,158 +353,93 @@ var _ = Describe("Elemental", Label("elemental"), func() { }) Describe("PartitionAndFormatDevice", Label("PartitionAndFormatDevice", "partition", "format"), func() { - var el *elemental.Elemental var cInit *v1mock.FakeCloudInitRunner - var partNum int - var printOut string - var failPart bool var install *v1.InstallSpec var err error + var el *elemental.Elemental BeforeEach(func() { cInit = &v1mock.FakeCloudInitRunner{ExecStages: []string{}, Error: false} config.CloudInitRunner = cInit - config.Install.Device = "/some/device" - el = elemental.NewElemental(config) - install, err = agentConfig.NewInstallSpec(config) - Expect(err).ToNot(HaveOccurred()) - install.Target = "/some/device" - - err := fsutils.MkdirAll(fs, "/some", cnst.DirPerm) + Expect(os.RemoveAll("/tmp/test.img")).ToNot(HaveOccurred()) + // at least 2Gb in size as state is set to 1G + _, err = diskfs.Create("/tmp/test.img", 2*1024*1024*1024, diskfs.Raw, 512) Expect(err).ToNot(HaveOccurred()) - _, err = fs.Create("/some/device") + config.Install.Device = "/tmp/test.img" + install, err = agentConfig.NewInstallSpec(config) Expect(err).ToNot(HaveOccurred()) + install.Target = "/tmp/test.img" + el = elemental.NewElemental(config) }) - Describe("Successful run", func() { - var runFunc func(cmd string, args ...string) ([]byte, error) - var efiPartCmds, partCmds, biosPartCmds [][]string - BeforeEach(func() { - partNum, printOut = 0, printOutput - err := fsutils.MkdirAll(fs, "/some", cnst.DirPerm) - Expect(err).To(BeNil()) - efiPartCmds = [][]string{ - { - "parted", "--script", "--machine", "--", "/some/device", "unit", "s", - "mklabel", "gpt", - }, { - "parted", "--script", "--machine", "--", "/some/device", "unit", "s", - "mkpart", "efi", "fat32", "2048", "133119", "set", "1", "esp", "on", - }, {"mkfs.vfat", "-n", "COS_GRUB", "/some/device1"}, - } - biosPartCmds = [][]string{ - { - "parted", "--script", "--machine", "--", "/some/device", "unit", "s", - "mklabel", "gpt", - }, { - "parted", "--script", "--machine", "--", "/some/device", "unit", "s", - "mkpart", "bios", "", "2048", "4095", "set", "1", "bios_grub", "on", - }, {"wipefs", "--all", "/some/device1"}, - } - // These commands are only valid for EFI case - partCmds = [][]string{ - { - "parted", "--script", "--machine", "--", "/some/device", "unit", "s", - "mkpart", "oem", "ext4", "133120", "264191", - }, {"mkfs.ext4", "-L", "COS_OEM", "/some/device2"}, { - "parted", "--script", "--machine", "--", "/some/device", "unit", "s", - "mkpart", "recovery", "ext4", "264192", "673791", - }, {"mkfs.ext4", "-L", "COS_RECOVERY", "/some/device3"}, { - "parted", "--script", "--machine", "--", "/some/device", "unit", "s", - "mkpart", "state", "ext4", "673792", "2721791", - }, {"mkfs.ext4", "-L", "COS_STATE", "/some/device4"}, { - "parted", "--script", "--machine", "--", "/some/device", "unit", "s", - "mkpart", "persistent", "ext4", "2721792", "100%", - }, {"mkfs.ext4", "-L", "COS_PERSISTENT", "/some/device5"}, - } + AfterEach(func() { + Expect(os.RemoveAll("/tmp/test.img")).ToNot(HaveOccurred()) + }) - runFunc = func(cmd string, args ...string) ([]byte, error) { - switch cmd { - case "parted": - idx := 0 - for i, arg := range args { - if arg == "mkpart" { - idx = i - break - } - } - if idx > 0 { - partNum++ - printOut += fmt.Sprintf(partTmpl, partNum, args[idx+3], args[idx+4]) - _, _ = fs.Create(fmt.Sprintf("/some/device%d", partNum)) - } - return []byte(printOut), nil - default: - return []byte{}, nil - } - } - runner.SideEffect = runFunc - }) - - It("Successfully creates partitions and formats them, EFI boot", func() { - install.PartTable = v1.GPT - install.Firmware = v1.EFI - install.Partitions.SetFirmwarePartitions(v1.EFI, v1.GPT) - Expect(el.PartitionAndFormatDevice(install)).To(BeNil()) - Expect(runner.MatchMilestones(append(efiPartCmds, partCmds...))).To(BeNil()) - }) - - It("Successfully creates partitions and formats them, BIOS boot", func() { - install.PartTable = v1.GPT - install.Firmware = v1.BIOS - install.Partitions.SetFirmwarePartitions(v1.BIOS, v1.GPT) - Expect(el.PartitionAndFormatDevice(install)).To(BeNil()) - Expect(runner.MatchMilestones(biosPartCmds)).To(BeNil()) - }) - }) - - Describe("Run with failures", func() { - var runFunc func(cmd string, args ...string) ([]byte, error) - BeforeEach(func() { - err := fsutils.MkdirAll(fs, "/some", cnst.DirPerm) - Expect(err).To(BeNil()) - partNum, printOut = 0, printOutput - runFunc = func(cmd string, args ...string) ([]byte, error) { - switch cmd { - case "parted": - idx := 0 - for i, arg := range args { - if arg == "mkpart" { - idx = i - break - } - } - if idx > 0 { - partNum++ - printOut += fmt.Sprintf(partTmpl, partNum, args[idx+3], args[idx+4]) - if failPart { - return []byte{}, errors.New("Failure") - } - _, _ = fs.Create(fmt.Sprintf("/some/device%d", partNum)) - } - return []byte(printOut), nil - case "mkfs.ext4", "wipefs", "mkfs.vfat": - return []byte{}, errors.New("Failure") - default: - return []byte{}, nil - } - } - runner.SideEffect = runFunc - }) - - It("Fails creating efi partition", func() { - failPart = true - Expect(el.PartitionAndFormatDevice(install)).NotTo(BeNil()) - // Failed to create first partition - Expect(partNum).To(Equal(1)) - }) - - It("Fails formatting efi partition", func() { - failPart = false - Expect(el.PartitionAndFormatDevice(install)).NotTo(BeNil()) - // Failed to format first partition - Expect(partNum).To(Equal(1)) - }) + It("Successfully creates partitions and formats them, EFI boot", func() { + install.PartTable = v1.GPT + install.Firmware = v1.EFI + Expect(install.Partitions.SetFirmwarePartitions(v1.EFI, v1.GPT)).To(BeNil()) + Expect(el.PartitionAndFormatDevice(install)).To(BeNil()) + disk, err := diskfs.Open("/tmp/test.img", diskfs.WithOpenMode(diskfs.ReadOnly)) + defer disk.Close() + Expect(err).ToNot(HaveOccurred()) + // check that its type GPT + Expect(reflect.TypeOf(disk.Table)).To(Equal(reflect.TypeOf(&gpt.Table{}))) + // Expect the disk UUID to be constant + Expect(strings.ToLower(disk.Table.UUID())).To(Equal(strings.ToLower(cnst.DiskUUID))) + // 5 partitions (boot, oem, recovery, state and persistent) + Expect(len(disk.Table.GetPartitions())).To(Equal(5)) + // Cast the boot partition into specific type to check the type and such + part := disk.Table.GetPartitions()[0] + partition, ok := part.(*gpt.Partition) + Expect(ok).To(BeTrue()) + // Should be efi type + Expect(partition.Type).To(Equal(gpt.EFISystemPartition)) + // should have boot label + Expect(partition.Name).To(Equal(cnst.EfiLabel)) + // Should have predictable UUID + Expect(strings.ToLower(partition.UUID())).To(Equal(strings.ToLower(uuid.NewV5(uuid.NamespaceURL, cnst.EfiLabel).String()))) + // Check the rest have the proper types + for i := 1; i < len(disk.Table.GetPartitions()); i++ { + part := disk.Table.GetPartitions()[i] + partition, ok := part.(*gpt.Partition) + Expect(ok).To(BeTrue()) + // all of them should have the Linux fs type + Expect(partition.Type).To(Equal(gpt.LinuxFilesystem)) + } + }) + It("Successfully creates partitions and formats them, BIOS boot", func() { + install.PartTable = v1.GPT + install.Firmware = v1.BIOS + Expect(install.Partitions.SetFirmwarePartitions(v1.BIOS, v1.GPT)).To(BeNil()) + Expect(el.PartitionAndFormatDevice(install)).To(BeNil()) + disk, err := diskfs.Open("/tmp/test.img", diskfs.WithOpenMode(diskfs.ReadOnly)) + defer disk.Close() + Expect(err).ToNot(HaveOccurred()) + // check that its type GPT + Expect(reflect.TypeOf(disk.Table)).To(Equal(reflect.TypeOf(&gpt.Table{}))) + // Expect the disk UUID to be constant + Expect(strings.ToLower(disk.Table.UUID())).To(Equal(strings.ToLower(cnst.DiskUUID))) + // 5 partitions (boot, oem, recovery, state and persistent) + Expect(len(disk.Table.GetPartitions())).To(Equal(5)) + // Cast the boot partition into specific type to check the type and such + part := disk.Table.GetPartitions()[0] + partition, ok := part.(*gpt.Partition) + Expect(ok).To(BeTrue()) + // Should be BIOS boot type + Expect(partition.Type).To(Equal(gpt.BIOSBoot)) + // should have boot label + Expect(partition.Name).To(Equal(cnst.EfiLabel)) + // Should have predictable UUID + Expect(strings.ToLower(partition.UUID())).To(Equal(strings.ToLower(uuid.NewV5(uuid.NamespaceURL, cnst.EfiLabel).String()))) + for i := 1; i < len(disk.Table.GetPartitions()); i++ { + part := disk.Table.GetPartitions()[i] + partition, ok := part.(*gpt.Partition) + Expect(ok).To(BeTrue()) + // all of them should have the Linux fs type + Expect(partition.Type).To(Equal(gpt.LinuxFilesystem)) + } }) }) Describe("DeployImage", Label("DeployImage"), func() { @@ -512,9 +466,8 @@ var _ = Describe("Elemental", Label("elemental"), func() { return []byte{}, errors.New("Command failed") } switch cmd { - case "losetup": - return []byte("/dev/loop"), nil default: + GinkgoWriter.Println(fmt.Sprintf("Command %s called but we dont catch it", cmd)) return []byte{}, nil } } @@ -887,32 +840,39 @@ var _ = Describe("Elemental", Label("elemental"), func() { Describe("SetDefaultGrubEntry", Label("SetDefaultGrubEntry", "grub"), func() { It("Sets the default grub entry without issues", func() { el := elemental.NewElemental(config) - Expect(el.SetDefaultGrubEntry("/mountpoint", "/imgMountpoint", "default_entry")).To(BeNil()) + Expect(config.Fs.Mkdir("/tmp", cnst.DirPerm)).To(BeNil()) + Expect(el.SetDefaultGrubEntry("/tmp", "/imgMountpoint", "dio")).To(BeNil()) + varsParsed, err := utils.ReadPersistentVariables(filepath.Join("/tmp", cnst.GrubOEMEnv), config.Fs) + Expect(err).To(BeNil()) + Expect(varsParsed["default_menu_entry"]).To(Equal("dio")) }) It("does nothing on empty default entry and no /etc/os-release", func() { el := elemental.NewElemental(config) + Expect(config.Fs.Mkdir("/mountpoint", cnst.DirPerm)).To(BeNil()) Expect(el.SetDefaultGrubEntry("/mountpoint", "/imgMountPoint", "")).To(BeNil()) - // No grub2-editenv command called - Expect(runner.CmdsMatch([][]string{{"grub2-editenv"}})).NotTo(BeNil()) + _, err := utils.ReadPersistentVariables(filepath.Join("/tmp", cnst.GrubOEMEnv), config.Fs) + // Because it didnt do anything due to the entry being empty, the file should not be there + Expect(err).ToNot(BeNil()) + _, err = config.Fs.Stat(filepath.Join("/tmp", cnst.GrubOEMEnv)) + Expect(err).ToNot(BeNil()) }) It("loads /etc/os-release on empty default entry", func() { err := fsutils.MkdirAll(config.Fs, "/imgMountPoint/etc", cnst.DirPerm) Expect(err).ShouldNot(HaveOccurred()) err = config.Fs.WriteFile("/imgMountPoint/etc/os-release", []byte("GRUB_ENTRY_NAME=test"), cnst.FilePerm) Expect(err).ShouldNot(HaveOccurred()) + Expect(config.Fs.Mkdir("/mountpoint", cnst.DirPerm)).To(BeNil()) el := elemental.NewElemental(config) Expect(el.SetDefaultGrubEntry("/mountpoint", "/imgMountPoint", "")).To(BeNil()) - // Calls grub2-editenv with the loaded content from /etc/os-release - editEnv := utils.FindCommand("grub2-editenv", []string{"grub2-editenv", "grub-editenv"}) - Expect(runner.CmdsMatch([][]string{ - {editEnv, "/mountpoint/grub_oem_env", "set", "default_menu_entry=test"}, - })).To(BeNil()) + varsParsed, err := utils.ReadPersistentVariables(filepath.Join("/mountpoint", cnst.GrubOEMEnv), config.Fs) + Expect(err).To(BeNil()) + Expect(varsParsed["default_menu_entry"]).To(Equal("test")) + }) It("Fails setting grubenv", func() { - runner.ReturnError = errors.New("failure") el := elemental.NewElemental(config) - Expect(el.SetDefaultGrubEntry("/mountpoint", "/imgMountPoint", "default_entry")).NotTo(BeNil()) + Expect(el.SetDefaultGrubEntry("nonexisting", "nonexisting", "default_entry")).NotTo(BeNil()) }) }) Describe("FindKernelInitrd", Label("find"), func() { diff --git a/pkg/partitioner/disk.go b/pkg/partitioner/disk.go index d1a160eb..c0d09c35 100644 --- a/pkg/partitioner/disk.go +++ b/pkg/partitioner/disk.go @@ -1,455 +1,144 @@ -/* -Copyright © 2022 SUSE LLC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - package partitioner import ( - "errors" "fmt" - "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs" - "github.com/kairos-io/kairos-agent/v2/pkg/utils/partitions" - sdkTypes "github.com/kairos-io/kairos-sdk/types" - "os" - "regexp" - "strings" - "time" - + "github.com/diskfs/go-diskfs" + "github.com/diskfs/go-diskfs/disk" + "github.com/diskfs/go-diskfs/partition" + "github.com/diskfs/go-diskfs/partition/gpt" + "github.com/gofrs/uuid" + cnst "github.com/kairos-io/kairos-agent/v2/pkg/constants" v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1" - "github.com/twpayne/go-vfs/v4" -) - -const ( - partitionTries = 10 - // Parted warning substring for expanded disks without fixing GPT headers - partedWarn = "Not all of the space available" + sdkTypes "github.com/kairos-io/kairos-sdk/types" + "github.com/sanity-io/litter" ) -var unallocatedRegexp = regexp.MustCompile(partedWarn) - type Disk struct { - device string - sectorS uint - lastS uint - parts []Partition - label string - runner v1.Runner - fs v1.FS - logger sdkTypes.KairosLogger -} - -// MiBToSectors returns the number of sectors that correspond to the given amount -// of MB. -func MiBToSectors(totalMB uint, sectorSize uint) uint { - bytes := totalMB * 1024 * 1024 - return bytes / sectorSize + *disk.Disk + logger sdkTypes.KairosLogger } -// SectorsToMiB returns the number of MBs that correspond to the given amount -// of sectors. -func SectorsToMiB(totalSectors uint, sectorSize uint) uint { - bytes := totalSectors * sectorSize - - return bytes / (1024 * 1024) // Mb -} - -func NewDisk(device string, opts ...DiskOptions) *Disk { - dev := &Disk{device: device} - - for _, opt := range opts { - if err := opt(dev); err != nil { - return nil +func (d *Disk) NewPartitionTable(partType string, parts v1.PartitionList) error { + d.logger.Infof("Creating partition table for partition type %s", partType) + var table partition.Table + switch partType { + case v1.GPT: + table = &gpt.Table{ + ProtectiveMBR: true, + GUID: cnst.DiskUUID, // Set know predictable UUID + Partitions: kairosPartsToDiskfsGPTParts(parts, d.Size), } + default: + return fmt.Errorf("invalid partition type: %s", partType) } - - if dev.runner == nil { - dev.runner = &v1.RealRunner{} - } - - if dev.fs == nil { - dev.fs = vfs.OSFS - } - - l := dev.logger - if &l == nil { - dev.logger = sdkTypes.NewKairosLogger("partitioner", "info", false) - } - - return dev -} - -// FormatDevice formats a block device with the given parameters -func FormatDevice(runner v1.Runner, device string, fileSystem string, label string, opts ...string) error { - mkfs := MkfsCall{fileSystem: fileSystem, label: label, customOpts: opts, dev: device, runner: runner} - _, err := mkfs.Apply() - return err -} - -func (dev Disk) String() string { - return dev.device -} - -func (dev Disk) GetSectorSize() uint { - return dev.sectorS -} - -func (dev Disk) GetLastSector() uint { - return dev.lastS -} - -func (dev Disk) GetLabel() string { - return dev.label -} - -func (dev *Disk) Exists() bool { - fi, err := dev.fs.Stat(dev.device) - if err != nil { - return false - } - // resolve symlink if any - if fi.Mode()&os.ModeSymlink != 0 { - d, err := dev.fs.Readlink(dev.device) - if err != nil { - return false - } - dev.device = d - } - return true -} - -func (dev *Disk) Reload() error { - pc := NewPartedCall(dev.String(), dev.runner) - prnt, err := pc.Print() - if err != nil { - return fmt.Errorf("error: %w. output: %s", err, prnt) - } - - // if the unallocated space warning is found it is assumed GPT headers - // are not properly located to match disk size, so we use sgdisk - // to expand the partition table to fully match disk size. - // It is expected that in upcoming parted releases (>3.4) there will be - // --fix flag to solve this issue transparently on the fly on any parted call. - // However this option is not yet present in all major distros. - if unallocatedRegexp.Match([]byte(prnt)) { - // Parted has not a proper way to doing it in non interactive mode, - // because of that we use sgdisk for that... - out, err := dev.runner.Run("sgdisk", "-e", dev.device) - if err != nil { - return fmt.Errorf("error: %w. output: %s", err, string(out)) - } - // Reload disk data with fixed headers - prnt, err = pc.Print() - if err != nil { - return fmt.Errorf("error: %w. output: %s", err, prnt) - } - } - - sectorS, err := pc.GetSectorSize(prnt) - if err != nil { - return err - } - lastS, err := pc.GetLastSector(prnt) - if err != nil { - return err - } - label, err := pc.GetPartitionTableLabel(prnt) + err := d.Partition(table) if err != nil { return err } - partitions := pc.GetPartitions(prnt) - dev.sectorS = sectorS - dev.lastS = lastS - dev.parts = partitions - dev.label = label + d.logger.Infof("Created partition table for partition type %s", partType) return nil } -// Size is expressed in MiB here -func (dev *Disk) CheckDiskFreeSpaceMiB(minSpace uint) bool { - freeS, err := dev.GetFreeSpace() - if err != nil { - dev.logger.Warnf("Could not calculate disk free space") - return false - } - minSec := MiBToSectors(minSpace, dev.sectorS) - - return freeS >= minSec +func getSectorEndFromSize(start, size uint64) uint64 { + return (size / uint64(diskfs.SectorSize512)) + start - 1 } -func (dev *Disk) GetFreeSpace() (uint, error) { - //Check we have loaded partition table data - if dev.sectorS == 0 { - err := dev.Reload() - if err != nil { - dev.logger.Errorf("Failed analyzing disk: %v\n", err) - return 0, err +func kairosPartsToDiskfsGPTParts(parts v1.PartitionList, diskSize int64) []*gpt.Partition { + var partitions []*gpt.Partition + for index, part := range parts { + var start uint64 + var end uint64 + var size uint64 + if len(partitions) == 0 { + // first partition, align to 1Mb + start = 1024 * 1024 / uint64(diskfs.SectorSize512) + } else { + // get latest partition end, sum 1 + start = partitions[len(partitions)-1].End + 1 } - } - - return dev.computeFreeSpace(), nil -} -func (dev Disk) computeFreeSpace() uint { - if len(dev.parts) > 0 { - lastPart := dev.parts[len(dev.parts)-1] - return dev.lastS - (lastPart.StartS + lastPart.SizeS - 1) - } - // First partition starts at a 1MiB offset - return dev.lastS - (1*1024*1024/dev.sectorS - 1) -} - -func (dev Disk) computeFreeSpaceWithoutLast() uint { - if len(dev.parts) > 1 { - part := dev.parts[len(dev.parts)-2] - return dev.lastS - (part.StartS + part.SizeS - 1) - } - // Assume first partitions is alined to 1MiB - return dev.lastS - (1024*1024/dev.sectorS - 1) -} - -func (dev *Disk) NewPartitionTable(label string) (string, error) { - match, _ := regexp.MatchString("msdos|gpt", label) - if !match { - return "", errors.New("Invalid partition table type, only msdos and gpt are supported") - } - pc := NewPartedCall(dev.String(), dev.runner) - pc.SetPartitionTableLabel(label) - pc.WipeTable(true) - out, err := pc.WriteChanges() - if err != nil { - return out, err - } - err = dev.Reload() - if err != nil { - dev.logger.Errorf("Failed analyzing disk: %v\n", err) - return "", err - } - return out, nil -} - -// AddPartition adds a partition. Size is expressed in MiB here -// Size is expressed in MiB here -func (dev *Disk) AddPartition(size uint, fileSystem string, pLabel string, flags ...string) (int, error) { - pc := NewPartedCall(dev.String(), dev.runner) - - //Check we have loaded partition table data - if dev.sectorS == 0 { - err := dev.Reload() - if err != nil { - dev.logger.Errorf("Failed analyzing disk: %v\n", err) - return 0, err - } - } - - pc.SetPartitionTableLabel(dev.label) - - var partNum int - var startS uint - if len(dev.parts) > 0 { - lastP := len(dev.parts) - 1 - partNum = dev.parts[lastP].Number - startS = dev.parts[lastP].StartS + dev.parts[lastP].SizeS - } else { - //First partition is aligned at 1MiB - startS = 1024 * 1024 / dev.sectorS - } - - size = MiBToSectors(size, dev.sectorS) - freeS := dev.computeFreeSpace() - if size > freeS { - return 0, fmt.Errorf("not enough free space in disk. Required: %d sectors; Available %d sectors", size, freeS) - } - - partNum++ - var part = Partition{ - Number: partNum, - StartS: startS, - SizeS: size, - PLabel: pLabel, - FileSystem: fileSystem, - } - - pc.CreatePartition(&part) - for _, flag := range flags { - pc.SetPartitionFlag(partNum, flag, true) - } - - out, err := pc.WriteChanges() - dev.logger.Debugf("partitioner output: %s", out) - if err != nil { - dev.logger.Errorf("Failed creating partition: %v", err) - return 0, err - } - - // Reload new partition in dev - err = dev.Reload() - if err != nil { - dev.logger.Errorf("Failed analyzing disk: %v\n", err) - return 0, err - } - return partNum, nil -} - -func (dev Disk) FormatPartition(partNum int, fileSystem string, label string) (string, error) { - pDev, err := dev.FindPartitionDevice(partNum) - if err != nil { - return "", err - } - - mkfs := MkfsCall{fileSystem: fileSystem, label: label, customOpts: []string{}, dev: pDev, runner: dev.runner} - return mkfs.Apply() -} - -func (dev Disk) WipeFsOnPartition(device string) error { - _, err := dev.runner.Run("wipefs", "--all", device) - return err -} - -func (dev Disk) FindPartitionDevice(partNum int) (string, error) { - re := regexp.MustCompile(`.*\d+$`) - var device string - - if match := re.Match([]byte(dev.device)); match { - device = fmt.Sprintf("%sp%d", dev.device, partNum) - } else { - device = fmt.Sprintf("%s%d", dev.device, partNum) - } - - for tries := 0; tries <= partitionTries; tries++ { - dev.logger.Debugf("Trying to find the partition device %d of device %s (try number %d)", partNum, dev, tries+1) - _, _ = dev.runner.Run("udevadm", "settle") - if exists, _ := fsutils.Exists(dev.fs, device); exists { - return device, nil - } - time.Sleep(1 * time.Second) - } - return "", fmt.Errorf("could not find partition device '%s' for partition %d", device, partNum) -} - -// ExpandLastPartition expands the latest partition in the disk. Size is expressed in MiB here -// Size is expressed in MiB here -func (dev *Disk) ExpandLastPartition(size uint) (string, error) { - pc := NewPartedCall(dev.String(), dev.runner) + // part.Size 0 means take over whats left on the disk + if part.Size == 0 { + // Remember to add the 1Mb alignment to total size + // This will be on bytes already no need to transform it + var sizeUsed = uint64(1024 * 1024) + for _, p := range partitions { + sizeUsed = sizeUsed + p.Size + } + // leave 1Mb at the end for backup GPT header + size = uint64(diskSize) - sizeUsed - uint64(1024*1024) + } else { + // Change it to bytes + // If its the last partition to do, leave 1 Mb at the end for backup GPT header + if index == len(parts)-1 { + size = uint64(part.Size*1024*1024) - uint64(1024*1024) + } else { + size = uint64(part.Size * 1024 * 1024) + } - //Check we have loaded partition table data - if dev.sectorS == 0 { - err := dev.Reload() - if err != nil { - dev.logger.Errorf("Failed analyzing disk: %v\n", err) - return "", err } - } - - pc.SetPartitionTableLabel(dev.label) - - if len(dev.parts) == 0 { - return "", errors.New("There is no partition to expand") - } - - // This is a safe guard to avoid re-executing this operation on each boot. - // On a system which is partitioned, one sector is left out, so we - // use a buffer to avoid expansion at all if we don't have at least 10MB free. - // There is no point to try to expand a partition to get 10MB - if we do expansion is to take over - // the space for the last partition, which is meant for the persistent data (and thus we want to expand for gaining GBs, not MBs.) - freeSpace := dev.computeFreeSpace() - limit := MiBToSectors(10, dev.sectorS) - if freeSpace < limit { - return "", fmt.Errorf("not enough free space (%d) to expand, at least %d is required", freeSpace, limit) - } - part := dev.parts[len(dev.parts)-1] - var sizeSectors uint - if size > 0 { - sizeSectors = MiBToSectors(size, dev.sectorS) - part := dev.parts[len(dev.parts)-1] - if sizeSectors < part.SizeS { - return "", errors.New("Layout plugin can only expand a partition, not shrink it") - } - freeS := dev.computeFreeSpaceWithoutLast() - if sizeSectors > freeS { - return "", fmt.Errorf("not enough free space for to expand last partition up to %d sectors", size) - } - } - part.SizeS = sizeSectors - pc.DeletePartition(part.Number) - pc.CreatePartition(&part) - out, err := pc.WriteChanges() - if err != nil { - return out, err - } - err = dev.Reload() + end = getSectorEndFromSize(start, size) + + if part.Name == cnst.EfiPartName && part.FS == cnst.EfiFs { + // EFI boot partition + partitions = append(partitions, &gpt.Partition{ + Start: start, + End: end, + Type: gpt.EFISystemPartition, + Size: size, // partition size in bytes + GUID: uuid.NewV5(uuid.NamespaceURL, part.FilesystemLabel).String(), // set know predictable UUID + Name: part.FilesystemLabel, + Attributes: 0x1, // system partition flag + }) + } else if part.Name == cnst.BiosPartName { + // Non-EFI boot partition + partitions = append(partitions, &gpt.Partition{ + Start: start, + End: end, + Type: gpt.BIOSBoot, + Size: size, // partition size in bytes + GUID: uuid.NewV5(uuid.NamespaceURL, part.FilesystemLabel).String(), // set know predictable UUID + Name: part.FilesystemLabel, + Attributes: 0x4, // legacy bios bootable flag + }) + } else { + // Other partitions + partitions = append(partitions, &gpt.Partition{ + Start: start, + End: end, + Type: gpt.LinuxFilesystem, + Size: size, + GUID: uuid.NewV5(uuid.NamespaceURL, part.FilesystemLabel).String(), + Name: part.FilesystemLabel, + }) + } + } + return partitions +} + +type DiskOptions func(d *Disk) error + +func WithLogger(logger sdkTypes.KairosLogger) func(d *Disk) error { + return func(d *Disk) error { + d.logger = logger + return nil + } +} + +func NewDisk(device string, opts ...DiskOptions) (*Disk, error) { + d, err := diskfs.Open(device, diskfs.WithSectorSize(512)) if err != nil { - return "", err + return nil, err } - pDev, err := dev.FindPartitionDevice(part.Number) - if err != nil { - return "", err - } - return dev.expandFilesystem(pDev) -} - -func (dev Disk) expandFilesystem(device string) (string, error) { - var out []byte - var err error + dev := &Disk{d, sdkTypes.NewKairosLogger("partitioner", "info", false)} - fs, err := partitions.GetPartitionFS(device) - if err != nil { - return fs, err - } - - switch strings.TrimSpace(fs) { - case "ext2", "ext3", "ext4": - out, err = dev.runner.Run("e2fsck", "-fy", device) - if err != nil { - return string(out), err - } - out, err = dev.runner.Run("resize2fs", device) - - if err != nil { - return string(out), err - } - case "xfs": - // to grow an xfs fs it needs to be mounted :/ - tmpDir, err := fsutils.TempDir(dev.fs, "", "partitioner") - defer func(fs v1.FS, path string) { - _ = fs.RemoveAll(path) - }(dev.fs, tmpDir) - - if err != nil { - return string(out), err - } - out, err = dev.runner.Run("mount", "-t", "xfs", device, tmpDir) - if err != nil { - return string(out), err - } - _, err = dev.runner.Run("xfs_growfs", tmpDir) - if err != nil { - // If we error out, try to umount the dir to not leave it hanging - out, err2 := dev.runner.Run("umount", tmpDir) - if err2 != nil { - return string(out), err2 - } - return string(out), err - } - out, err = dev.runner.Run("umount", tmpDir) - if err != nil { - return string(out), err + for _, opt := range opts { + if err := opt(dev); err != nil { + return nil, err } - default: - return "", fmt.Errorf("could not find filesystem for %s, not resizing the filesystem", device) } - return "", nil + dev.logger.Debugf("Initialized new disk from device %s", litter.Sdump(dev)) + return dev, nil } diff --git a/pkg/partitioner/mkfs.go b/pkg/partitioner/mkfs.go index b901fe27..61bfe9c2 100644 --- a/pkg/partitioner/mkfs.go +++ b/pkg/partitioner/mkfs.go @@ -75,3 +75,13 @@ func (mkfs MkfsCall) Apply() (string, error) { out, err := mkfs.runner.Run(tool, opts...) return string(out), err } + +// FormatDevice formats a block device with the given parameters +func FormatDevice(runner v1.Runner, device string, fileSystem string, label string, opts ...string) error { + mkfs := MkfsCall{fileSystem: fileSystem, label: label, customOpts: opts, dev: device, runner: runner} + out, err := mkfs.Apply() + if err != nil { + fmt.Println(out) + } + return err +} diff --git a/pkg/partitioner/options.go b/pkg/partitioner/options.go deleted file mode 100644 index 8559b475..00000000 --- a/pkg/partitioner/options.go +++ /dev/null @@ -1,45 +0,0 @@ -/* -Copyright © 2022 SUSE LLC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package partitioner - -import ( - v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1" - sdkTypes "github.com/kairos-io/kairos-sdk/types" -) - -type DiskOptions func(d *Disk) error - -func WithFS(fs v1.FS) func(d *Disk) error { - return func(d *Disk) error { - d.fs = fs - return nil - } -} - -func WithRunner(runner v1.Runner) func(d *Disk) error { - return func(d *Disk) error { - d.runner = runner - return nil - } -} - -func WithLogger(logger sdkTypes.KairosLogger) func(d *Disk) error { - return func(d *Disk) error { - d.logger = logger - return nil - } -} diff --git a/pkg/partitioner/parted.go b/pkg/partitioner/parted.go deleted file mode 100644 index 9def7f5c..00000000 --- a/pkg/partitioner/parted.go +++ /dev/null @@ -1,231 +0,0 @@ -/* -Copyright © 2022 SUSE LLC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package partitioner - -import ( - "bufio" - "errors" - "fmt" - "regexp" - "strconv" - "strings" - - "github.com/kairos-io/kairos-agent/v2/pkg/constants" - v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1" -) - -type PartedCall struct { - dev string - wipe bool - parts []*Partition - deletions []int - label string - runner v1.Runner - flags []partFlag -} - -type partFlag struct { - flag string - active bool - number int -} - -// We only manage sizes in sectors unit for the Partition structre in parted wrapper -// FileSystem here is only used by parted to determine the partition ID or type -type Partition struct { - Number int - StartS uint - SizeS uint - PLabel string - FileSystem string -} - -func NewPartedCall(dev string, runner v1.Runner) *PartedCall { - return &PartedCall{dev: dev, wipe: false, parts: []*Partition{}, deletions: []int{}, label: "", runner: runner, flags: []partFlag{}} -} - -func (pc PartedCall) optionsBuilder() []string { - opts := []string{} - label := pc.label - match, _ := regexp.MatchString(fmt.Sprintf("msdos|%s", constants.GPT), label) - // Fallback to gpt if label is empty or invalid - if !match { - label = constants.GPT - } - - if pc.wipe { - opts = append(opts, "mklabel", label) - } - - for _, partnum := range pc.deletions { - opts = append(opts, "rm", fmt.Sprintf("%d", partnum)) - } - - isFat, _ := regexp.Compile("fat|vfat") - for _, part := range pc.parts { - var pLabel string - if label == constants.GPT && part.PLabel != "" { - pLabel = part.PLabel - } else if label == constants.GPT { - pLabel = fmt.Sprintf("part%d", part.Number) - } else { - pLabel = "primary" - } - - opts = append(opts, "mkpart", pLabel) - - if isFat.MatchString(part.FileSystem) { - opts = append(opts, "fat32") - } else { - opts = append(opts, part.FileSystem) - } - - if part.SizeS == 0 { - // Size set to zero means is interperted as all space available - opts = append(opts, fmt.Sprintf("%d", part.StartS), "100%") - } else { - opts = append(opts, fmt.Sprintf("%d", part.StartS), fmt.Sprintf("%d", part.StartS+part.SizeS-1)) - } - } - - for _, flag := range pc.flags { - opts = append(opts, "set", fmt.Sprintf("%d", flag.number), flag.flag) - if flag.active { - opts = append(opts, "on") - } else { - opts = append(opts, "off") - } - } - - if len(opts) == 0 { - return nil - } - - return append([]string{"--script", "--machine", "--", pc.dev, "unit", "s"}, opts...) -} - -func (pc *PartedCall) WriteChanges() (string, error) { - opts := pc.optionsBuilder() - if len(opts) == 0 { - return "", nil - } - - out, err := pc.runner.Run("parted", opts...) - pc.wipe = false - pc.parts = []*Partition{} - pc.deletions = []int{} - return string(out), err -} - -func (pc *PartedCall) SetPartitionTableLabel(label string) { - pc.label = label -} - -func (pc *PartedCall) CreatePartition(p *Partition) { - pc.parts = append(pc.parts, p) -} - -func (pc *PartedCall) DeletePartition(num int) { - pc.deletions = append(pc.deletions, num) -} - -func (pc *PartedCall) SetPartitionFlag(num int, flag string, active bool) { - pc.flags = append(pc.flags, partFlag{flag: flag, active: active, number: num}) -} - -func (pc *PartedCall) WipeTable(wipe bool) { - pc.wipe = wipe -} - -func (pc PartedCall) Print() (string, error) { - out, err := pc.runner.Run("parted", "--script", "--machine", "--", pc.dev, "unit", "s", "print") - return string(out), err -} - -// Parses the output of a PartedCall.Print call -func (pc PartedCall) parseHeaderFields(printOut string, field int) (string, error) { - re := regexp.MustCompile(`^(.*):(\d+)s:(.*):(\d+):(\d+):(.*):(.*):(.*);$`) - - scanner := bufio.NewScanner(strings.NewReader(strings.TrimSpace(printOut))) - for scanner.Scan() { - match := re.FindStringSubmatch(strings.TrimSpace(scanner.Text())) - if match != nil { - return match[field], nil - } - } - return "", errors.New("failed parsing parted header data") -} - -// Parses the output of a PartedCall.Print call -func (pc PartedCall) GetLastSector(printOut string) (uint, error) { - field, err := pc.parseHeaderFields(printOut, 2) - if err != nil { - return 0, errors.New("Failed parsing last sector") - } - lastSec, err := strconv.ParseUint(field, 10, 0) - return uint(lastSec), err -} - -// Parses the output of a PartedCall.Print call -func (pc PartedCall) GetSectorSize(printOut string) (uint, error) { - field, err := pc.parseHeaderFields(printOut, 4) - if err != nil { - return 0, errors.New("Failed parsing sector size") - } - secSize, err := strconv.ParseUint(field, 10, 0) - return uint(secSize), err -} - -// Parses the output of a PartedCall.Print call -func (pc PartedCall) GetPartitionTableLabel(printOut string) (string, error) { - return pc.parseHeaderFields(printOut, 6) -} - -// Parses the output of a GdiskCall.Print call -func (pc PartedCall) GetPartitions(printOut string) []Partition { - re := regexp.MustCompile(`^(\d+):(\d+)s:(\d+)s:(\d+)s:(.*):(.*):(.*);$`) - var start uint - var end uint - var size uint - var pLabel string - var partNum int - var partitions []Partition - - scanner := bufio.NewScanner(strings.NewReader(strings.TrimSpace(printOut))) - for scanner.Scan() { - match := re.FindStringSubmatch(strings.TrimSpace(scanner.Text())) - if match != nil { - partNum, _ = strconv.Atoi(match[1]) - parsed, _ := strconv.ParseUint(match[2], 10, 0) - start = uint(parsed) - parsed, _ = strconv.ParseUint(match[3], 10, 0) - end = uint(parsed) - size = end - start + 1 - pLabel = match[6] - - partitions = append(partitions, Partition{ - Number: partNum, - StartS: start, - SizeS: size, - PLabel: pLabel, - FileSystem: "", - }) - } - } - - return partitions -} diff --git a/pkg/partitioner/partitioner_test.go b/pkg/partitioner/partitioner_test.go deleted file mode 100644 index 941db8d0..00000000 --- a/pkg/partitioner/partitioner_test.go +++ /dev/null @@ -1,404 +0,0 @@ -/* -Copyright © 2021 SUSE LLC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package partitioner_test - -import ( - "errors" - "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs" - "testing" - - "github.com/jaypipes/ghw/pkg/block" - - "github.com/kairos-io/kairos-agent/v2/pkg/constants" - part "github.com/kairos-io/kairos-agent/v2/pkg/partitioner" - mocks "github.com/kairos-io/kairos-agent/v2/tests/mocks" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/twpayne/go-vfs/v4" - "github.com/twpayne/go-vfs/v4/vfst" -) - -const printOutput = `BYT; -/dev/loop0:50593792s:loopback:512:512:msdos:Loopback device:; -1:2048s:98303s:96256s:ext4::type=83; -2:98304s:29394943s:29296640s:ext4::boot, type=83; -3:29394944s:45019135s:15624192s:ext4::type=83; -4:45019136s:50331647s:5312512s:ext4::type=83;` - -func TestElementalSuite(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Partitioner test suite") -} - -var _ = Describe("Partitioner", Label("disk", "partition", "partitioner"), func() { - var runner *mocks.FakeRunner - BeforeEach(func() { - runner = mocks.NewFakeRunner() - }) - Describe("Parted tests", Label("parted"), func() { - var pc *part.PartedCall - BeforeEach(func() { - pc = part.NewPartedCall("/dev/device", runner) - }) - It("Write changes does nothing with empty setup", func() { - pc := part.NewPartedCall("/dev/device", runner) - _, err := pc.WriteChanges() - Expect(err).To(BeNil()) - }) - It("Runs complex command", func() { - cmds := [][]string{{ - "parted", "--script", "--machine", "--", "/dev/device", - "unit", "s", "mklabel", "gpt", "mkpart", "p.efi", "fat32", - "2048", "206847", "mkpart", "p.root", "ext4", "206848", "100%", - }} - part1 := part.Partition{ - Number: 0, StartS: 2048, SizeS: 204800, - PLabel: "p.efi", FileSystem: "vfat", - } - pc.CreatePartition(&part1) - part2 := part.Partition{ - Number: 0, StartS: 206848, SizeS: 0, - PLabel: "p.root", FileSystem: "ext4", - } - pc.CreatePartition(&part2) - pc.WipeTable(true) - _, err := pc.WriteChanges() - Expect(err).To(BeNil()) - Expect(runner.CmdsMatch(cmds)).To(BeNil()) - }) - It("Set a new partition label", func() { - cmds := [][]string{{ - "parted", "--script", "--machine", "--", "/dev/device", - "unit", "s", "mklabel", "msdos", - }} - pc.SetPartitionTableLabel("msdos") - pc.WipeTable(true) - _, err := pc.WriteChanges() - Expect(err).To(BeNil()) - Expect(runner.CmdsMatch(cmds)).To(BeNil()) - }) - It("Creates a new partition", func() { - cmds := [][]string{{ - "parted", "--script", "--machine", "--", "/dev/device", - "unit", "s", "mkpart", "p.root", "ext4", "2048", "206847", - }, { - "parted", "--script", "--machine", "--", "/dev/device", - "unit", "s", "mkpart", "p.root", "ext4", "2048", "100%", - }} - partition := part.Partition{ - Number: 0, StartS: 2048, SizeS: 204800, - PLabel: "p.root", FileSystem: "ext4", - } - pc.CreatePartition(&partition) - _, err := pc.WriteChanges() - Expect(err).To(BeNil()) - partition = part.Partition{ - Number: 0, StartS: 2048, SizeS: 0, - PLabel: "p.root", FileSystem: "ext4", - } - pc.CreatePartition(&partition) - _, err = pc.WriteChanges() - Expect(err).To(BeNil()) - Expect(runner.CmdsMatch(cmds)).To(BeNil()) - }) - It("Deletes a partition", func() { - cmd := []string{ - "parted", "--script", "--machine", "--", "/dev/device", - "unit", "s", "rm", "1", "rm", "2", - } - pc.DeletePartition(1) - pc.DeletePartition(2) - _, err := pc.WriteChanges() - Expect(err).To(BeNil()) - Expect(runner.CmdsMatch([][]string{cmd})).To(BeNil()) - }) - It("Set a partition flag", func() { - cmds := [][]string{{ - "parted", "--script", "--machine", "--", "/dev/device", - "unit", "s", "set", "1", "flag", "on", "set", "2", "flag", "off", - }} - pc.SetPartitionFlag(1, "flag", true) - pc.SetPartitionFlag(2, "flag", false) - _, err := pc.WriteChanges() - Expect(err).To(BeNil()) - Expect(runner.CmdsMatch(cmds)).To(BeNil()) - }) - It("Wipes partition table creating a new one", func() { - cmd := []string{ - "parted", "--script", "--machine", "--", "/dev/device", - "unit", "s", "mklabel", "gpt", - } - pc.WipeTable(true) - _, err := pc.WriteChanges() - Expect(err).To(BeNil()) - Expect(runner.CmdsMatch([][]string{cmd})).To(BeNil()) - }) - It("Prints partitin table info", func() { - cmd := []string{ - "parted", "--script", "--machine", "--", "/dev/device", - "unit", "s", "print", - } - _, err := pc.Print() - Expect(err).To(BeNil()) - Expect(runner.CmdsMatch([][]string{cmd})).To(BeNil()) - }) - It("Gets last sector of the disk", func() { - lastSec, _ := pc.GetLastSector(printOutput) - Expect(lastSec).To(Equal(uint(50593792))) - _, err := pc.GetLastSector("invalid parted print output") - Expect(err).NotTo(BeNil()) - }) - It("Gets sector size of the disk", func() { - secSize, _ := pc.GetSectorSize(printOutput) - Expect(secSize).To(Equal(uint(512))) - _, err := pc.GetSectorSize("invalid parted print output") - Expect(err).NotTo(BeNil()) - }) - It("Gets partition table label", func() { - label, _ := pc.GetPartitionTableLabel(printOutput) - Expect(label).To(Equal("msdos")) - _, err := pc.GetPartitionTableLabel("invalid parted print output") - Expect(err).NotTo(BeNil()) - }) - It("Gets partitions info of the disk", func() { - parts := pc.GetPartitions(printOutput) - Expect(len(parts)).To(Equal(4)) - Expect(parts[1].StartS).To(Equal(uint(98304))) - }) - }) - Describe("Mkfs tests", Label("mkfs", "filesystem"), func() { - It("Successfully formats a partition with xfs", func() { - mkfs := part.NewMkfsCall("/dev/device", "xfs", "OEM", runner) - _, err := mkfs.Apply() - Expect(err).To(BeNil()) - cmds := [][]string{{"mkfs.xfs", "-L", "OEM", "/dev/device"}} - Expect(runner.CmdsMatch(cmds)).To(BeNil()) - }) - It("Successfully formats a partition with vfat", func() { - mkfs := part.NewMkfsCall("/dev/device", "vfat", "EFI", runner) - _, err := mkfs.Apply() - Expect(err).To(BeNil()) - cmds := [][]string{{"mkfs.vfat", "-n", "EFI", "/dev/device"}} - Expect(runner.CmdsMatch(cmds)).To(BeNil()) - }) - It("Fails for unsupported filesystem", func() { - mkfs := part.NewMkfsCall("/dev/device", "btrfs", "OEM", runner) - _, err := mkfs.Apply() - Expect(err).NotTo(BeNil()) - }) - }) - Describe("Disk tests", Label("mkfs", "filesystem"), func() { - var dev *part.Disk - var cmds [][]string - var printCmd []string - var fs vfs.FS - var cleanup func() - - BeforeEach(func() { - fs, cleanup, _ = vfst.NewTestFS(nil) - - err := fsutils.MkdirAll(fs, "/dev", constants.DirPerm) - Expect(err).To(BeNil()) - _, err = fs.Create("/dev/device") - Expect(err).To(BeNil()) - - dev = part.NewDisk("/dev/device", part.WithRunner(runner), part.WithFS(fs)) - printCmd = []string{ - "parted", "--script", "--machine", "--", "/dev/device", - "unit", "s", "print", - } - cmds = [][]string{printCmd} - }) - AfterEach(func() { cleanup() }) - It("Creates a default disk", func() { - dev = part.NewDisk("/dev/device") - }) - Describe("Load data without changes", func() { - BeforeEach(func() { - runner.ReturnValue = []byte(printOutput) - }) - It("Loads disk layout data", func() { - Expect(dev.Reload()).To(BeNil()) - Expect(dev.String()).To(Equal("/dev/device")) - Expect(dev.GetSectorSize()).To(Equal(uint(512))) - Expect(dev.GetLastSector()).To(Equal(uint(50593792))) - Expect(runner.CmdsMatch(cmds)).To(BeNil()) - }) - It("Computes available free space", func() { - Expect(dev.GetFreeSpace()).To(Equal(uint(262145))) - Expect(runner.CmdsMatch(cmds)).To(BeNil()) - }) - It("Checks it has at least 128MB of free space", func() { - Expect(dev.CheckDiskFreeSpaceMiB(128)).To(Equal(true)) - Expect(runner.CmdsMatch(cmds)).To(BeNil()) - }) - It("Checks it has less than 130MB of free space", func() { - Expect(dev.CheckDiskFreeSpaceMiB(130)).To(Equal(false)) - Expect(runner.CmdsMatch(cmds)).To(BeNil()) - }) - It("Get partition label", func() { - dev.Reload() - Expect(dev.GetLabel()).To(Equal("msdos")) - }) - It("It fixes GPT headers if the disk was expanded", func() { - runner.ReturnValue = []byte("Warning: Not all of the space available to /dev/loop0...\n" + printOutput) - Expect(dev.Reload()).To(BeNil()) - Expect(runner.MatchMilestones([][]string{ - {"parted", "--script", "--machine", "--", "/dev/device", "unit", "s", "print"}, - {"sgdisk", "-e", "/dev/device"}, - {"parted", "--script", "--machine", "--", "/dev/device", "unit", "s", "print"}, - })).To(BeNil()) - }) - }) - Describe("Modify disk", func() { - It("Format an already existing partition", func() { - err := part.FormatDevice(runner, "/dev/device1", "ext4", "MY_LABEL") - Expect(err).To(BeNil()) - Expect(runner.CmdsMatch([][]string{ - {"mkfs.ext4", "-L", "MY_LABEL", "/dev/device1"}, - })).To(BeNil()) - }) - It("Fails to create an unsupported partition table label", func() { - runner.ReturnValue = []byte(printOutput) - _, err := dev.NewPartitionTable("invalidLabel") - Expect(err).NotTo(BeNil()) - }) - It("Creates new partition table label", func() { - cmds = [][]string{{ - "parted", "--script", "--machine", "--", "/dev/device", - "unit", "s", "mklabel", "gpt", - }, printCmd} - runner.ReturnValue = []byte(printOutput) - _, err := dev.NewPartitionTable("gpt") - Expect(err).To(BeNil()) - Expect(runner.CmdsMatch(cmds)).To(BeNil()) - }) - It("Adds a new partition", func() { - cmds = [][]string{printCmd, { - "parted", "--script", "--machine", "--", "/dev/device", - "unit", "s", "mkpart", "primary", "ext4", "50331648", "100%", - "set", "5", "boot", "on", - }, printCmd} - runner.ReturnValue = []byte(printOutput) - num, err := dev.AddPartition(0, "ext4", "ignored", "boot") - Expect(err).To(BeNil()) - Expect(num).To(Equal(5)) - Expect(runner.CmdsMatch(cmds)).To(BeNil()) - }) - It("Fails to a new partition if there is not enough space available", func() { - cmds = [][]string{printCmd} - runner.ReturnValue = []byte(printOutput) - _, err := dev.AddPartition(130, "ext4", "ignored") - Expect(err).NotTo(BeNil()) - Expect(runner.CmdsMatch(cmds)).To(BeNil()) - }) - It("Finds device for a given partition number", func() { - _, err := fs.Create("/dev/device4") - Expect(err).To(BeNil()) - cmds = [][]string{{"udevadm", "settle"}} - Expect(dev.FindPartitionDevice(4)).To(Equal("/dev/device4")) - Expect(runner.CmdsMatch(cmds)).To(BeNil()) - }) - It("Does not find device for a given partition number", func() { - dev := part.NewDisk("/dev/lp0") - _, err := dev.FindPartitionDevice(4) - Expect(err).NotTo(BeNil()) - }) - It("Formats a partition", func() { - _, err := fs.Create("/dev/device4") - Expect(err).To(BeNil()) - cmds = [][]string{ - {"udevadm", "settle"}, - {"mkfs.xfs", "-L", "OEM", "/dev/device4"}, - } - _, err = dev.FormatPartition(4, "xfs", "OEM") - Expect(err).To(BeNil()) - Expect(runner.CmdsMatch(cmds)).To(BeNil()) - }) - It("Clears filesystem header from a partition", func() { - cmds = [][]string{ - {"wipefs", "--all", "/dev/device1"}, - } - Expect(dev.WipeFsOnPartition("/dev/device1")).To(BeNil()) - Expect(runner.CmdsMatch(cmds)).To(BeNil()) - }) - It("Fails while removing file system header", func() { - runner.ReturnError = errors.New("some error") - Expect(dev.WipeFsOnPartition("/dev/device1")).NotTo(BeNil()) - }) - Describe("Expanding partitions", func() { - BeforeEach(func() { - cmds = [][]string{ - printCmd, { - "parted", "--script", "--machine", "--", "/dev/device", - "unit", "s", "rm", "4", "mkpart", "primary", "", "45019136", "100%", - }, printCmd, {"udevadm", "settle"}, - } - runFunc := func(cmd string, args ...string) ([]byte, error) { - switch cmd { - case "parted": - return []byte(printOutput), nil - default: - return []byte{}, nil - } - } - runner.SideEffect = runFunc - }) - It("Expands ext4 partition", func() { - _, err := fs.Create("/dev/device4") - Expect(err).To(BeNil()) - extCmds := [][]string{ - {"e2fsck", "-fy", "/dev/device4"}, {"resize2fs", "/dev/device4"}, - } - ghwTest := mocks.GhwMock{} - disk := block.Disk{Name: "device", Partitions: []*block.Partition{ - { - Name: "device4", - Type: "ext4", - }, - }} - ghwTest.AddDisk(disk) - ghwTest.CreateDevices() - defer ghwTest.Clean() - _, err = dev.ExpandLastPartition(0) - Expect(err).To(BeNil()) - Expect(runner.CmdsMatch(append(cmds, extCmds...))).To(BeNil()) - }) - It("Expands xfs partition", func() { - _, err := fs.Create("/dev/device4") - Expect(err).To(BeNil()) - xfsCmds := [][]string{ - {"mount", "-t", "xfs"}, {"xfs_growfs"}, {"umount"}, - } - ghwTest := mocks.GhwMock{} - disk := block.Disk{Name: "device", Partitions: []*block.Partition{ - { - Name: "device4", - Type: "xfs", - }, - }} - ghwTest.AddDisk(disk) - ghwTest.CreateDevices() - defer ghwTest.Clean() - _, err = dev.ExpandLastPartition(0) - Expect(err).To(BeNil()) - Expect(runner.CmdsMatch(append(cmds, xfsCmds...))).To(BeNil()) - }) - }) - }) - }) -}) diff --git a/pkg/types/v1/config.go b/pkg/types/v1/config.go index 7d4f6469..ff3034dc 100644 --- a/pkg/types/v1/config.go +++ b/pkg/types/v1/config.go @@ -287,7 +287,7 @@ func (ep *ElementalPartitions) SetFirmwarePartitions(firmware string, partTable ep.BIOS = nil } else if firmware == BIOS && partTable == GPT { ep.BIOS = &Partition{ - FilesystemLabel: "", + FilesystemLabel: constants.EfiLabel, Size: constants.BiosSize, Name: constants.BiosPartName, FS: "", diff --git a/pkg/types/v1/fs.go b/pkg/types/v1/fs.go index 69608324..2d82307e 100644 --- a/pkg/types/v1/fs.go +++ b/pkg/types/v1/fs.go @@ -36,4 +36,5 @@ type FS interface { Remove(name string) error OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error) WriteFile(filename string, data []byte, perm os.FileMode) error + Rename(oldpath, newpath string) error } diff --git a/pkg/types/v1/syscall.go b/pkg/types/v1/syscall.go index e6a9ac15..9328dc45 100644 --- a/pkg/types/v1/syscall.go +++ b/pkg/types/v1/syscall.go @@ -24,6 +24,7 @@ type SyscallInterface interface { Chroot(string) error Chdir(string) error Mount(string, string, string, uintptr, string) error + Syscall(uintptr, uintptr, uintptr, uintptr) (uintptr, uintptr, syscall.Errno) } type RealSyscall struct{} @@ -38,3 +39,7 @@ func (r *RealSyscall) Chdir(path string) error { func (r *RealSyscall) Mount(source string, target string, fstype string, flags uintptr, data string) error { return syscall.Mount(source, target, fstype, flags, data) } + +func (r *RealSyscall) Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno) { + return syscall.Syscall(trap, a1, a2, a3) +} diff --git a/pkg/utils/common.go b/pkg/utils/common.go index 9ccb95d0..dff109e3 100644 --- a/pkg/utils/common.go +++ b/pkg/utils/common.go @@ -67,6 +67,7 @@ func GetDeviceByLabel(runner v1.Runner, label string, attempts int) (string, err // partition and return a v1.Partition object func GetFullDeviceByLabel(runner v1.Runner, label string, attempts int) (*v1.Partition, error) { for tries := 0; tries < attempts; tries++ { + _, _ = runner.Run("udevadm", "trigger") _, _ = runner.Run("udevadm", "settle") parts, err := partitions.GetAllPartitions() if err != nil { diff --git a/pkg/utils/grub.go b/pkg/utils/grub.go index af6c5df5..470b2d5b 100644 --- a/pkg/utils/grub.go +++ b/pkg/utils/grub.go @@ -17,12 +17,15 @@ limitations under the License. package utils import ( + "bytes" "fmt" agentConfig "github.com/kairos-io/kairos-agent/v2/pkg/config" + v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1" "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs" "github.com/kairos-io/kairos-sdk/utils" "io/fs" "path/filepath" + "sort" "strings" "github.com/kairos-io/kairos-agent/v2/pkg/constants" @@ -102,8 +105,8 @@ func (g Grub) Install(target, rootDir, bootDir, grubConf, tty string, efi bool, if tty == "" { // Get current tty and remove /dev/ from its name - out, err := g.config.Runner.Run("tty") - tty = strings.TrimPrefix(strings.TrimSpace(string(out)), "/dev/") + out, err := g.config.Fs.Readlink("/dev/fd/0") + tty = strings.TrimPrefix(strings.TrimSpace(out), "/dev/") if err != nil { g.config.Logger.Warnf("failed to find current tty, leaving it unset") tty = "" @@ -227,17 +230,30 @@ func (g Grub) Install(target, rootDir, bootDir, grubConf, tty string, efi bool, return nil } -// Sets the given key value pairs into as grub variables into the given file -func (g Grub) SetPersistentVariables(grubEnvFile string, vars map[string]string) error { - for key, value := range vars { - g.config.Logger.Debugf("Running grub2-editenv with params: %s set %s=%s", grubEnvFile, key, value) - out, err := g.config.Runner.Run(FindCommand("grub2-editenv", []string{"grub2-editenv", "grub-editenv"}), grubEnvFile, "set", fmt.Sprintf("%s=%s", key, value)) - if err != nil { - g.config.Logger.Errorf(fmt.Sprintf("Failed setting grub variables: %s", out)) - return err +// SetPersistentVariables sets the given vars into the given grubEnvFile for grub to read them +func SetPersistentVariables(grubEnvFile string, vars map[string]string, fs v1.FS) error { + var b bytes.Buffer + b.WriteString("# GRUB Environment Block\n") + + keys := make([]string, 0, len(vars)) + for k := range vars { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + if len(vars[k]) > 0 { + b.WriteString(fmt.Sprintf("%s=%s\n", k, vars[k])) } } - return nil + + // https://www.gnu.org/software/grub/manual/grub/html_node/Environment-block.html + // It has to be exactly 1024bytes, if its not it has to be filled with # + toBeFilled := 1024 - b.Len()%1024 + for i := 0; i < toBeFilled; i++ { + b.WriteByte('#') + } + return fs.WriteFile(grubEnvFile, b.Bytes(), cnst.FilePerm) } // copyGrubFonts will try to finds and copy the needed grub fonts into the system @@ -352,3 +368,25 @@ func (g Grub) copyGrub() error { } return nil } + +// ReadPersistentVariables will read a grub env file and parse the values +func ReadPersistentVariables(grubEnvFile string, fs v1.FS) (map[string]string, error) { + vars := make(map[string]string) + f, err := fs.ReadFile(grubEnvFile) + if err != nil { + return nil, err + } + for _, a := range strings.Split(string(f), "\n") { + // comment or fillup, so skip + if strings.HasPrefix(a, "#") { + continue + } + splitted := strings.Split(a, "=") + if len(splitted) == 2 { + vars[splitted[0]] = splitted[1] + } else { + return nil, fmt.Errorf("invalid format for %s", a) + } + } + return vars, nil +} diff --git a/pkg/utils/loop/loopback.go b/pkg/utils/loop/loopback.go new file mode 100644 index 00000000..74673f07 --- /dev/null +++ b/pkg/utils/loop/loopback.go @@ -0,0 +1,112 @@ +package loop + +import ( + "fmt" + "github.com/kairos-io/kairos-agent/v2/pkg/config" + "os" + "syscall" + "unsafe" + + v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1" + "golang.org/x/sys/unix" +) + +// syscalls will return an errno type (which implements error) for all calls, +// including success (errno 0) so we need to check its value to know if its an actual error or not +func errnoIsErr(err error) error { + if err != nil && err.(syscall.Errno) != 0 { + return err + } + + return nil +} + +// Loop will setup a /dev/loopX device linked to the image file by using syscalls directly to set it +func Loop(img *v1.Image, cfg *config.Config) (loopDevice string, err error) { + log := cfg.Logger + log.Debugf("Opening loop control device") + fd, err := cfg.Fs.OpenFile("/dev/loop-control", os.O_RDONLY, 0o644) + if err != nil { + log.Error("failed to open /dev/loop-control") + return loopDevice, err + } + + defer fd.Close() + log.Debugf("Getting free loop device") + loopInt, _, err := cfg.Syscall.Syscall(syscall.SYS_IOCTL, fd.Fd(), unix.LOOP_CTL_GET_FREE, 0) + if errnoIsErr(err) != nil { + log.Error("failed to get loop device") + return loopDevice, err + } + + loopDevice = fmt.Sprintf("/dev/loop%d", loopInt) + log.Logger.Debug().Str("device", loopDevice).Msg("Opening loop device") + loopFile, err := cfg.Fs.OpenFile(loopDevice, os.O_RDWR, 0) + if err != nil { + log.Error("failed to open loop device") + return loopDevice, err + } + log.Logger.Debug().Str("image", img.File).Msg("Opening img file") + imageFile, err := cfg.Fs.OpenFile(img.File, os.O_RDWR, os.ModePerm) + if err != nil { + log.Error("failed to open image file") + return loopDevice, err + } + defer loopFile.Close() + defer imageFile.Close() + + log.Debugf("Setting loop device") + _, _, err = cfg.Syscall.Syscall( + syscall.SYS_IOCTL, + loopFile.Fd(), + unix.LOOP_SET_FD, + imageFile.Fd(), + ) + if errnoIsErr(err) != nil { + log.Error("failed to set loop device") + return loopDevice, err + } + + // Force kernel to scan partition table on loop device + status := &unix.LoopInfo64{ + Flags: unix.LO_FLAGS_PARTSCAN, + } + // Dont set read only flag + status.Flags &= ^uint32(unix.LO_FLAGS_READ_ONLY) + + log.Debugf("Setting loop flags") + _, _, err = cfg.Syscall.Syscall( + syscall.SYS_IOCTL, + loopFile.Fd(), + unix.LOOP_SET_STATUS64, + uintptr(unsafe.Pointer(status)), + ) + + if errnoIsErr(err) != nil { + log.Error("failed to set loop device status") + return loopDevice, err + } + + return loopDevice, nil +} + +// Unloop will clear a loop device and free the underlying image linked to it +func Unloop(loopDevice string, cfg *config.Config) error { + log := cfg.Logger + log.Logger.Debug().Str("device", loopDevice).Msg("Opening loop device") + fd, err := cfg.Fs.OpenFile(loopDevice, os.O_RDONLY, 0o644) + if err != nil { + log.Error("failed to set open loop device") + return err + } + defer fd.Close() + log.Debugf("Clearing loop device") + _, _, err = cfg.Syscall.Syscall(syscall.SYS_IOCTL, fd.Fd(), unix.LOOP_CLR_FD, 0) + + if errnoIsErr(err) != nil { + log.Error("failed to set loop device status") + return err + } + + return nil +} diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index 94bc2d0b..a794795e 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -184,6 +184,7 @@ var _ = Describe("Utils", Label("utils"), func() { var cmds [][]string BeforeEach(func() { cmds = [][]string{ + {"udevadm", "trigger"}, {"udevadm", "settle"}, } }) @@ -323,6 +324,7 @@ var _ = Describe("Utils", Label("utils"), func() { var cmds [][]string BeforeEach(func() { cmds = [][]string{ + {"udevadm", "trigger"}, {"udevadm", "settle"}, } }) @@ -881,27 +883,24 @@ var _ = Describe("Utils", Label("utils"), func() { }) Describe("SetPersistentVariables", func() { It("Sets the grub environment file", func() { - grub := utils.NewGrub(config) - Expect(grub.SetPersistentVariables( - "somefile", map[string]string{"key1": "value1", "key2": "value2"}, + temp, err := os.CreateTemp("", "grub-*") + Expect(err).ShouldNot(HaveOccurred()) + defer os.Remove(temp.Name()) + Expect(utils.SetPersistentVariables( + temp.Name(), map[string]string{"key1": "value1", "key2": "value2"}, + config.Fs, )).To(BeNil()) - editEnv := utils.FindCommand("grub2-editenv", []string{"grub2-editenv", "grub-editenv"}) - Expect(runner.IncludesCmds([][]string{ - {editEnv, "somefile", "set", "key1=value1"}, - {editEnv, "somefile", "set", "key2=value2"}, - })).To(BeNil()) + readVars, err := utils.ReadPersistentVariables(temp.Name(), config.Fs) + Expect(err).To(BeNil()) + Expect(readVars["key1"]).To(Equal("value1")) + Expect(readVars["key2"]).To(Equal("value2")) }) - It("Fails running grub2-editenv", func() { - runner.ReturnError = errors.New("grub error") - grub := utils.NewGrub(config) - e := grub.SetPersistentVariables( - "somefile", map[string]string{"key1": "value1"}, + It("Fails setting variables", func() { + e := utils.SetPersistentVariables( + "badfilenopath", map[string]string{"key1": "value1"}, + config.Fs, ) Expect(e).NotTo(BeNil()) - editEnv := utils.FindCommand("grub2-editenv", []string{"grub2-editenv", "grub-editenv"}) - Expect(runner.CmdsMatch([][]string{ - {editEnv, "somefile", "set", "key1=value1"}, - })).To(BeNil()) }) }) }) diff --git a/tests/mocks/syscall_mock.go b/tests/mocks/syscall_mock.go index a232bea9..b67f64d5 100644 --- a/tests/mocks/syscall_mock.go +++ b/tests/mocks/syscall_mock.go @@ -18,14 +18,17 @@ package mocks import ( "errors" + "syscall" ) // FakeSyscall is a test helper method to track calls to syscall // It can also fail on Chroot command type FakeSyscall struct { - chrootHistory []string // Track calls to chroot - ErrorOnChroot bool - mounts []FakeMount + chrootHistory []string // Track calls to chroot + ErrorOnChroot bool + ReturnValue int // What to return when FakeSyscall.Syscall is called + mounts []FakeMount + SideEffectSyscall func(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno) // Function to stub the result of calling FakeSyscall.Syscall } type FakeMount struct { @@ -79,3 +82,10 @@ func (f *FakeSyscall) WasMountCalledWith(source string, target string, fstype st } return false } + +func (f *FakeSyscall) Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno) { + if f.SideEffectSyscall != nil { + return f.SideEffectSyscall(trap, a1, a2, a3) + } + return 0, 0, syscall.Errno(f.ReturnValue) +}