From aa6510123287d9554c1fa9d0cfadfa24c6b7feaf Mon Sep 17 00:00:00 2001 From: Andrew Rynhard Date: Thu, 1 Nov 2018 22:05:24 -0700 Subject: [PATCH] feat: add blockd service (#172) --- docs/categories/index.html | 7 + docs/components/blockd/index.html | 283 ++++++++++++ docs/components/index.html | 7 + docs/components/index.xml | 10 + docs/components/init/index.html | 7 + docs/components/kernel/index.html | 7 + docs/components/kubeadm/index.html | 7 + docs/components/osctl/index.html | 11 + docs/components/osd/index.html | 7 + docs/components/proxyd/index.html | 7 + docs/components/trustd/index.html | 7 + docs/configuration/controlplane/index.html | 7 + docs/configuration/index.html | 7 + docs/configuration/workers/index.html | 7 + docs/dianemo/index.html | 7 + docs/examples/aws/index.html | 7 + docs/examples/index.html | 7 + docs/examples/kvm/index.html | 7 + docs/index.html | 7 + docs/index.json | 2 +- docs/index.xml | 10 + docs/sitemap.xml | 5 + docs/tags/index.html | 7 + src/docs/src/content/components/blockd.md | 19 + src/image/.conform.yaml | 2 +- src/image/src/entrypoint.sh | 30 +- src/initramfs/.conform.yaml | 76 +++- src/initramfs/cmd/blockd/main.go | 65 +++ src/initramfs/cmd/blockd/pkg/reg/reg.go | 26 ++ src/initramfs/cmd/blockd/proto/api.pb.go | 165 +++++++ src/initramfs/cmd/blockd/proto/api.proto | 17 + src/initramfs/cmd/init/main.go | 1 + .../cmd/init/pkg/fs/xfs/util/util.go | 27 ++ .../cmd/init/pkg/fs/xfs/util/util_test.go | 102 +++++ src/initramfs/cmd/init/pkg/fs/xfs/xfs.go | 25 ++ src/initramfs/cmd/init/pkg/mount/mount.go | 93 +++- .../cmd/init/pkg/system/services/blockd.go | 78 ++++ src/initramfs/cmd/osd/main.go | 35 +- src/initramfs/go.mod | 3 +- src/initramfs/go.sum | 4 + src/initramfs/pkg/blockdevice/blockdevice.go | 69 +++ src/initramfs/pkg/blockdevice/pkg/lba/lba.go | 44 ++ .../pkg/blockdevice/pkg/serde/serde.go | 68 +++ .../pkg/blockdevice/table/gpt/gpt.go | 416 +++++++++++++++++ .../pkg/blockdevice/table/gpt/gpt_options.go | 49 ++ .../blockdevice/table/gpt/header/header.go | 418 ++++++++++++++++++ .../table/gpt/header/header_options.go | 49 ++ .../table/gpt/partition/partition.go | 189 ++++++++ .../table/gpt/partition/partition_options.go | 52 +++ src/initramfs/pkg/blockdevice/table/table.go | 67 +++ src/initramfs/pkg/grpc/factory/factory.go | 24 +- .../{cmd/osd/pkg => pkg/grpc}/gen/gen.go | 22 +- src/initramfs/pkg/userdata/userdata.go | 6 + src/tools/.conform.yaml | 4 +- 54 files changed, 2623 insertions(+), 62 deletions(-) create mode 100644 docs/components/blockd/index.html create mode 100644 src/docs/src/content/components/blockd.md create mode 100644 src/initramfs/cmd/blockd/main.go create mode 100644 src/initramfs/cmd/blockd/pkg/reg/reg.go create mode 100644 src/initramfs/cmd/blockd/proto/api.pb.go create mode 100644 src/initramfs/cmd/blockd/proto/api.proto create mode 100644 src/initramfs/cmd/init/pkg/fs/xfs/util/util.go create mode 100644 src/initramfs/cmd/init/pkg/fs/xfs/util/util_test.go create mode 100644 src/initramfs/cmd/init/pkg/fs/xfs/xfs.go create mode 100644 src/initramfs/cmd/init/pkg/system/services/blockd.go create mode 100644 src/initramfs/pkg/blockdevice/blockdevice.go create mode 100644 src/initramfs/pkg/blockdevice/pkg/lba/lba.go create mode 100644 src/initramfs/pkg/blockdevice/pkg/serde/serde.go create mode 100644 src/initramfs/pkg/blockdevice/table/gpt/gpt.go create mode 100644 src/initramfs/pkg/blockdevice/table/gpt/gpt_options.go create mode 100644 src/initramfs/pkg/blockdevice/table/gpt/header/header.go create mode 100644 src/initramfs/pkg/blockdevice/table/gpt/header/header_options.go create mode 100644 src/initramfs/pkg/blockdevice/table/gpt/partition/partition.go create mode 100644 src/initramfs/pkg/blockdevice/table/gpt/partition/partition_options.go create mode 100644 src/initramfs/pkg/blockdevice/table/table.go rename src/initramfs/{cmd/osd/pkg => pkg/grpc}/gen/gen.go (78%) diff --git a/docs/categories/index.html b/docs/categories/index.html index 7868222c8..6ab8e0e11 100644 --- a/docs/categories/index.html +++ b/docs/categories/index.html @@ -170,6 +170,13 @@ + + diff --git a/docs/components/blockd/index.html b/docs/components/blockd/index.html new file mode 100644 index 000000000..de2ece515 --- /dev/null +++ b/docs/components/blockd/index.html @@ -0,0 +1,283 @@ + + + + + + + Autonomy + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + + +
+
+
+

blockd

+

Dianemo comes with a reserved block device with three partitions:

+ +
    +
  • an EFI System Partition (ESP)
  • +
  • a ROOT partition mounted as read-only that contains the minimal set of binaries to operate system services
  • +
  • and a DATA partion that is mounted as read/write at /var/run
  • +
+ +

These partitions are reserved and cannot be modified. +The one expection to this is that the DATA partition will be resized automatically in the init process to the maximum size possible. +Managing any other block device can be done via the blockd service.

+

+
+
+
+ +
+
+
+
+ + + + diff --git a/docs/components/index.html b/docs/components/index.html index 3378f4b2a..834ec5b0c 100644 --- a/docs/components/index.html +++ b/docs/components/index.html @@ -170,6 +170,13 @@ + + diff --git a/docs/components/index.xml b/docs/components/index.xml index 28f205697..9b16371fd 100644 --- a/docs/components/index.xml +++ b/docs/components/index.xml @@ -79,5 +79,15 @@ To make this work, we needed an out-of-band tool for managing the nodes. In an i retrieve container logs restart a service reset a node reboot a node retrieve kernel logs generate pki resources inject data into node configuration files + + blockd + https://dianemo.autonomy.io/components/blockd/ + Tue, 30 Oct 2018 09:16:35 -0700 + + https://dianemo.autonomy.io/components/blockd/ + Dianemo comes with a reserved block device with three partitions: + an EFI System Partition (ESP) a ROOT partition mounted as read-only that contains the minimal set of binaries to operate system services and a DATA partion that is mounted as read/write at /var/run These partitions are reserved and cannot be modified. The one expection to this is that the DATA partition will be resized automatically in the init process to the maximum size possible. + + \ No newline at end of file diff --git a/docs/components/init/index.html b/docs/components/init/index.html index e9b60e4b3..e3c2ecf71 100644 --- a/docs/components/init/index.html +++ b/docs/components/init/index.html @@ -170,6 +170,13 @@ + + diff --git a/docs/components/kernel/index.html b/docs/components/kernel/index.html index b44de955d..127a495fc 100644 --- a/docs/components/kernel/index.html +++ b/docs/components/kernel/index.html @@ -170,6 +170,13 @@ + + diff --git a/docs/components/kubeadm/index.html b/docs/components/kubeadm/index.html index 36335816d..52a70107f 100644 --- a/docs/components/kubeadm/index.html +++ b/docs/components/kubeadm/index.html @@ -170,6 +170,13 @@ + + diff --git a/docs/components/osctl/index.html b/docs/components/osctl/index.html index 226a66689..a0f7d3e3b 100644 --- a/docs/components/osctl/index.html +++ b/docs/components/osctl/index.html @@ -170,6 +170,13 @@ + + @@ -261,6 +268,10 @@ With it you can do things like:

+ + + +
diff --git a/docs/components/osd/index.html b/docs/components/osd/index.html index 14349ccaf..99c551262 100644 --- a/docs/components/osd/index.html +++ b/docs/components/osd/index.html @@ -170,6 +170,13 @@ + + diff --git a/docs/components/proxyd/index.html b/docs/components/proxyd/index.html index 30ef8f815..cd588628f 100644 --- a/docs/components/proxyd/index.html +++ b/docs/components/proxyd/index.html @@ -170,6 +170,13 @@ + + diff --git a/docs/components/trustd/index.html b/docs/components/trustd/index.html index fcb645690..9acaf02be 100644 --- a/docs/components/trustd/index.html +++ b/docs/components/trustd/index.html @@ -170,6 +170,13 @@ + + diff --git a/docs/configuration/controlplane/index.html b/docs/configuration/controlplane/index.html index c09788842..3b701da9a 100644 --- a/docs/configuration/controlplane/index.html +++ b/docs/configuration/controlplane/index.html @@ -170,6 +170,13 @@ + + diff --git a/docs/configuration/index.html b/docs/configuration/index.html index ede731234..320f59f8f 100644 --- a/docs/configuration/index.html +++ b/docs/configuration/index.html @@ -170,6 +170,13 @@ + + diff --git a/docs/configuration/workers/index.html b/docs/configuration/workers/index.html index f30da9f82..8b869f80c 100644 --- a/docs/configuration/workers/index.html +++ b/docs/configuration/workers/index.html @@ -170,6 +170,13 @@ + + diff --git a/docs/dianemo/index.html b/docs/dianemo/index.html index a5bd8ae35..8ba60d410 100644 --- a/docs/dianemo/index.html +++ b/docs/dianemo/index.html @@ -170,6 +170,13 @@ + + diff --git a/docs/examples/aws/index.html b/docs/examples/aws/index.html index 647179294..675a28231 100644 --- a/docs/examples/aws/index.html +++ b/docs/examples/aws/index.html @@ -170,6 +170,13 @@ + + diff --git a/docs/examples/index.html b/docs/examples/index.html index cd264b3f0..bc6d7c6aa 100644 --- a/docs/examples/index.html +++ b/docs/examples/index.html @@ -170,6 +170,13 @@ + + diff --git a/docs/examples/kvm/index.html b/docs/examples/kvm/index.html index 52a96c978..a1edde171 100644 --- a/docs/examples/kvm/index.html +++ b/docs/examples/kvm/index.html @@ -170,6 +170,13 @@ + + diff --git a/docs/index.html b/docs/index.html index acedb688e..017755de7 100644 --- a/docs/index.html +++ b/docs/index.html @@ -171,6 +171,13 @@ + + diff --git a/docs/index.json b/docs/index.json index 228468d93..c5064e0d0 100644 --- a/docs/index.json +++ b/docs/index.json @@ -1 +1 @@ -[{"categories":null,"contents":"The kernel included with Dianemo is configured according to the recommendations outlined in the Kernel Self Protection Project (KSSP).\n","permalink":"https://dianemo.autonomy.io/components/kernel/","tags":null,"title":"kernel"},{"categories":null,"contents":"A common theme throughout the design of Dianemo is minimalism. We believe strongly in the UNIX philosophy that each program should do one job well. The init included in Dianemo is one example of this.\nWe wanted to create a focused init that had one job - run Kubernetes. There simply is no mechanism in place to do anything else.\nTo accomplish this, we must address real world operations needs like:\n Orchestration around creating a highly available control plane Log retrieval Restarting system services Rebooting a node and more In the following sections we will take a closer look at how these needs are addressed, and how services managed by init are designed to enhance the Kubernetes experience.\n","permalink":"https://dianemo.autonomy.io/components/init/","tags":null,"title":"init"},{"categories":null,"contents":"At the heart of Dianemo is kubeadm, allowing it to harness the power of the official upstream bootstrap tool. By integrating with kubeadm natively, Dianemo stands to gain a strong community of users and developers already familiar with kubeadm.\n","permalink":"https://dianemo.autonomy.io/components/kubeadm/","tags":null,"title":"kubeadm"},{"categories":null,"contents":"Security is one of the highest priorities within Autonomy. To run a Kubernetes cluster a certain level of trust is required to operate a cluster. For example, orchestrating the bootstrap of a highly available control plane requires the distribution of sensitive PKI data.\nTo that end, we created trustd. Based on the concept of a Root of Trust, trustd is a simple daemon responsible for establishing trust within the system. Once trust is established, various methods become available to the trustee. It can, for example, accept a write request from another node to place a file on disk.\nWe imagine that the number available methods will grow as Dianemo gets tested in the real world.\n","permalink":"https://dianemo.autonomy.io/components/trustd/","tags":null,"title":"trustd"},{"categories":null,"contents":"Highly available Kubernetes clusters are crucial for production quality clusters. The proxyd component is a simple yet powerful reverse proxy that adapts to where Dianemo is employed and provides load balancing across all API servers.\n","permalink":"https://dianemo.autonomy.io/components/proxyd/","tags":null,"title":"proxyd"},{"categories":null,"contents":"Dianemo is unique in that it has no concept of host-level access. There are no shells installed. No ssh daemon. Only what is required to run Kubernetes. Furthermore, there is no way to run any custom processes on the host level.\nTo make this work, we needed an out-of-band tool for managing the nodes. In an ideal world, the system would be self-healing and we would never have to touch it. But, in the real world, this does not happen. We still need a way to handle operational scenarios that may arise.\nThe osd daemon provides a way to do just that. Based on the Principle of Least Privilege, osd provides operational value for cluster administrations by providing an API for node management.\n","permalink":"https://dianemo.autonomy.io/components/osd/","tags":null,"title":"osd"},{"categories":null,"contents":"The osctl CLI is the client to the osd service running on every node. With it you can do things like:\n retrieve container logs restart a service reset a node reboot a node retrieve kernel logs generate pki resources inject data into node configuration files ","permalink":"https://dianemo.autonomy.io/components/osctl/","tags":null,"title":"osctl"},{"categories":null,"contents":"First, create the AMI:\ndocker run \\ --rm \\ --volume $HOME/.aws/credentials:/root/.aws/credentials \\ --env AWS_DEFAULT_PROFILE=${PROFILE} \\ --env AWS_DEFAULT_REGION=${REGION} \\ autonomy/dianemo:latest ami -var regions=${COMMA_SEPARATED_LIST_OF_REGIONS} Once the AMI is created, you can now start an EC2 instance using the AMI ID. Provide the proper configuration as the instance\u0026rsquo;s user data.\n An official Terraform module is currently being developed, stay tuned!\n ","permalink":"https://dianemo.autonomy.io/examples/aws/","tags":null,"title":"AWS"},{"categories":null,"contents":"version: \u0026quot;\u0026quot; security: os: ca: crt: ${BASE64_ENCODED_PEM_FORMATTED_PUBLIC_X509} key: ${BASE64_ENCODED_PEM_FORMATTED_PRIVATE_X509} identity: crt: ${BASE64_ENCODED_PEM_FORMATTED_PUBLIC_X509} key: ${BASE64_ENCODED_PEM_FORMATTED_PRIVATE_X509} kubernetes: ca: crt: ${BASE64_ENCODED_PEM_FORMATTED_PUBLIC_X509} key: ${BASE64_ENCODED_PEM_FORMATTED_PRIVATE_X509} networking: os: {} kubernetes: {} services: kubeadm: init: type: initial etcdMemberName: etcd-1 containerRuntime: docker configuration: | apiVersion: kubeadm.k8s.io/v1alpha2 kind: MasterConfiguration clusterName: example bootstrapTokens: - token: abcdef.0123456789abcdef ttl: 0s kubeProxy: config: ipvs: scheduler: lc mode: ipvs networking: dnsDomain: cluster.local podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12 trustd: username: example password: example You can generate the PKI resources and inject them into the configuration with osctl.\n ","permalink":"https://dianemo.autonomy.io/configuration/controlplane/","tags":null,"title":"Control Plane"},{"categories":null,"contents":"Dianemo is a modern Linux distribution designed to be secure, immutable, and minimal.\n","permalink":"https://dianemo.autonomy.io/dianemo/","tags":null,"title":"Dianemo"},{"categories":null,"contents":" Install the Master Node On the KVM host, install a master node to an available block device:\ndocker run \\ --rm \\ --privileged \\ --volume /dev:/dev \\ autonomy/dianemo:latest image -b /dev/sda -f -p bare-metal -u http://${IP}:8080/master.yaml http://${IP}:8080/master.yaml should be reachable by the VM and contain a valid master configuration file.\n virt-install \\ -n master \\ --description \u0026quot;Kubernetes master node.\u0026quot; \\ --os-type=Linux \\ --os-variant=generic \\ --virt-type=kvm \\ --cpu=host \\ --ram=4096 \\ --vcpus=2 \\ --disk path=/dev/sdc \\ --network bridge=br0,model=e1000,mac=52:54:00:A8:4C:E1 \\ --graphics none \\ --boot hd \\ --rng /dev/random Install a Worker Node Similarly, install a worker node to an available block device:\ndocker run \\ --rm \\ --privileged \\ --volume /dev:/dev \\ autonomy/dianemo:latest image -b /dev/sdb -f -p bare-metal -u http://${IP}:8080/worker.yaml http://${IP}:8080/worker.yaml should be reachable by the VM and contain a valid worker configuration file.\n virt-install \\ -n master \\ --description \u0026quot;Kubernetes worker node.\u0026quot; \\ --os-type=Linux \\ --os-variant=generic \\ --virt-type=kvm \\ --cpu=host \\ --ram=4096 \\ --vcpus=2 \\ --disk path=/dev/sdc \\ --network bridge=br0,model=e1000,mac=52:54:00:B9:5D:F2 \\ --graphics none \\ --boot hd \\ --rng /dev/random ","permalink":"https://dianemo.autonomy.io/examples/kvm/","tags":null,"title":"KVM"},{"categories":null,"contents":"version: \u0026quot;\u0026quot; security: os: ca: crt: ${BASE64_ENCODED_PEM_FORMATTED_PUBLIC_X509} networking: os: {} kubernetes: {} services: kubeadm: containerRuntime: docker configuration: | apiVersion: kubeadm.k8s.io/v1alpha2 kind: NodeConfiguration token: abcdef.0123456789abcdef discoveryTokenAPIServers: - ${MASTER_IP}:443 discoveryTokenCACertHashes: - sha256:${CA_CERT_HASH} trustd: username: example password: example endpoints: - ${MASTER_IP} ","permalink":"https://dianemo.autonomy.io/configuration/workers/","tags":null,"title":"Workers"}] \ No newline at end of file +[{"categories":null,"contents":"The kernel included with Dianemo is configured according to the recommendations outlined in the Kernel Self Protection Project (KSSP).\n","permalink":"https://dianemo.autonomy.io/components/kernel/","tags":null,"title":"kernel"},{"categories":null,"contents":"A common theme throughout the design of Dianemo is minimalism. We believe strongly in the UNIX philosophy that each program should do one job well. The init included in Dianemo is one example of this.\nWe wanted to create a focused init that had one job - run Kubernetes. There simply is no mechanism in place to do anything else.\nTo accomplish this, we must address real world operations needs like:\n Orchestration around creating a highly available control plane Log retrieval Restarting system services Rebooting a node and more In the following sections we will take a closer look at how these needs are addressed, and how services managed by init are designed to enhance the Kubernetes experience.\n","permalink":"https://dianemo.autonomy.io/components/init/","tags":null,"title":"init"},{"categories":null,"contents":"At the heart of Dianemo is kubeadm, allowing it to harness the power of the official upstream bootstrap tool. By integrating with kubeadm natively, Dianemo stands to gain a strong community of users and developers already familiar with kubeadm.\n","permalink":"https://dianemo.autonomy.io/components/kubeadm/","tags":null,"title":"kubeadm"},{"categories":null,"contents":"Security is one of the highest priorities within Autonomy. To run a Kubernetes cluster a certain level of trust is required to operate a cluster. For example, orchestrating the bootstrap of a highly available control plane requires the distribution of sensitive PKI data.\nTo that end, we created trustd. Based on the concept of a Root of Trust, trustd is a simple daemon responsible for establishing trust within the system. Once trust is established, various methods become available to the trustee. It can, for example, accept a write request from another node to place a file on disk.\nWe imagine that the number available methods will grow as Dianemo gets tested in the real world.\n","permalink":"https://dianemo.autonomy.io/components/trustd/","tags":null,"title":"trustd"},{"categories":null,"contents":"Highly available Kubernetes clusters are crucial for production quality clusters. The proxyd component is a simple yet powerful reverse proxy that adapts to where Dianemo is employed and provides load balancing across all API servers.\n","permalink":"https://dianemo.autonomy.io/components/proxyd/","tags":null,"title":"proxyd"},{"categories":null,"contents":"Dianemo is unique in that it has no concept of host-level access. There are no shells installed. No ssh daemon. Only what is required to run Kubernetes. Furthermore, there is no way to run any custom processes on the host level.\nTo make this work, we needed an out-of-band tool for managing the nodes. In an ideal world, the system would be self-healing and we would never have to touch it. But, in the real world, this does not happen. We still need a way to handle operational scenarios that may arise.\nThe osd daemon provides a way to do just that. Based on the Principle of Least Privilege, osd provides operational value for cluster administrations by providing an API for node management.\n","permalink":"https://dianemo.autonomy.io/components/osd/","tags":null,"title":"osd"},{"categories":null,"contents":"The osctl CLI is the client to the osd service running on every node. With it you can do things like:\n retrieve container logs restart a service reset a node reboot a node retrieve kernel logs generate pki resources inject data into node configuration files ","permalink":"https://dianemo.autonomy.io/components/osctl/","tags":null,"title":"osctl"},{"categories":null,"contents":"Dianemo comes with a reserved block device with three partitions:\n an EFI System Partition (ESP) a ROOT partition mounted as read-only that contains the minimal set of binaries to operate system services and a DATA partion that is mounted as read/write at /var/run These partitions are reserved and cannot be modified. The one expection to this is that the DATA partition will be resized automatically in the init process to the maximum size possible. Managing any other block device can be done via the blockd service.\n","permalink":"https://dianemo.autonomy.io/components/blockd/","tags":null,"title":"blockd"},{"categories":null,"contents":"First, create the AMI:\ndocker run \\ --rm \\ --volume $HOME/.aws/credentials:/root/.aws/credentials \\ --env AWS_DEFAULT_PROFILE=${PROFILE} \\ --env AWS_DEFAULT_REGION=${REGION} \\ autonomy/dianemo:latest ami -var regions=${COMMA_SEPARATED_LIST_OF_REGIONS} Once the AMI is created, you can now start an EC2 instance using the AMI ID. Provide the proper configuration as the instance\u0026rsquo;s user data.\n An official Terraform module is currently being developed, stay tuned!\n ","permalink":"https://dianemo.autonomy.io/examples/aws/","tags":null,"title":"AWS"},{"categories":null,"contents":"version: \u0026quot;\u0026quot; security: os: ca: crt: ${BASE64_ENCODED_PEM_FORMATTED_PUBLIC_X509} key: ${BASE64_ENCODED_PEM_FORMATTED_PRIVATE_X509} identity: crt: ${BASE64_ENCODED_PEM_FORMATTED_PUBLIC_X509} key: ${BASE64_ENCODED_PEM_FORMATTED_PRIVATE_X509} kubernetes: ca: crt: ${BASE64_ENCODED_PEM_FORMATTED_PUBLIC_X509} key: ${BASE64_ENCODED_PEM_FORMATTED_PRIVATE_X509} networking: os: {} kubernetes: {} services: kubeadm: init: type: initial etcdMemberName: etcd-1 containerRuntime: docker configuration: | apiVersion: kubeadm.k8s.io/v1alpha2 kind: MasterConfiguration clusterName: example bootstrapTokens: - token: abcdef.0123456789abcdef ttl: 0s kubeProxy: config: ipvs: scheduler: lc mode: ipvs networking: dnsDomain: cluster.local podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12 trustd: username: example password: example You can generate the PKI resources and inject them into the configuration with osctl.\n ","permalink":"https://dianemo.autonomy.io/configuration/controlplane/","tags":null,"title":"Control Plane"},{"categories":null,"contents":"Dianemo is a modern Linux distribution designed to be secure, immutable, and minimal.\n","permalink":"https://dianemo.autonomy.io/dianemo/","tags":null,"title":"Dianemo"},{"categories":null,"contents":" Install the Master Node On the KVM host, install a master node to an available block device:\ndocker run \\ --rm \\ --privileged \\ --volume /dev:/dev \\ autonomy/dianemo:latest image -b /dev/sda -f -p bare-metal -u http://${IP}:8080/master.yaml http://${IP}:8080/master.yaml should be reachable by the VM and contain a valid master configuration file.\n virt-install \\ -n master \\ --description \u0026quot;Kubernetes master node.\u0026quot; \\ --os-type=Linux \\ --os-variant=generic \\ --virt-type=kvm \\ --cpu=host \\ --ram=4096 \\ --vcpus=2 \\ --disk path=/dev/sdc \\ --network bridge=br0,model=e1000,mac=52:54:00:A8:4C:E1 \\ --graphics none \\ --boot hd \\ --rng /dev/random Install a Worker Node Similarly, install a worker node to an available block device:\ndocker run \\ --rm \\ --privileged \\ --volume /dev:/dev \\ autonomy/dianemo:latest image -b /dev/sdb -f -p bare-metal -u http://${IP}:8080/worker.yaml http://${IP}:8080/worker.yaml should be reachable by the VM and contain a valid worker configuration file.\n virt-install \\ -n master \\ --description \u0026quot;Kubernetes worker node.\u0026quot; \\ --os-type=Linux \\ --os-variant=generic \\ --virt-type=kvm \\ --cpu=host \\ --ram=4096 \\ --vcpus=2 \\ --disk path=/dev/sdc \\ --network bridge=br0,model=e1000,mac=52:54:00:B9:5D:F2 \\ --graphics none \\ --boot hd \\ --rng /dev/random ","permalink":"https://dianemo.autonomy.io/examples/kvm/","tags":null,"title":"KVM"},{"categories":null,"contents":"version: \u0026quot;\u0026quot; security: os: ca: crt: ${BASE64_ENCODED_PEM_FORMATTED_PUBLIC_X509} networking: os: {} kubernetes: {} services: kubeadm: containerRuntime: docker configuration: | apiVersion: kubeadm.k8s.io/v1alpha2 kind: NodeConfiguration token: abcdef.0123456789abcdef discoveryTokenAPIServers: - ${MASTER_IP}:443 discoveryTokenCACertHashes: - sha256:${CA_CERT_HASH} trustd: username: example password: example endpoints: - ${MASTER_IP} ","permalink":"https://dianemo.autonomy.io/configuration/workers/","tags":null,"title":"Workers"}] \ No newline at end of file diff --git a/docs/index.xml b/docs/index.xml index aa0358be4..3dbae0063 100644 --- a/docs/index.xml +++ b/docs/index.xml @@ -79,6 +79,16 @@ To make this work, we needed an out-of-band tool for managing the nodes. In an i retrieve container logs restart a service reset a node reboot a node retrieve kernel logs generate pki resources inject data into node configuration files + + blockd + https://dianemo.autonomy.io/components/blockd/ + Tue, 30 Oct 2018 09:16:35 -0700 + + https://dianemo.autonomy.io/components/blockd/ + Dianemo comes with a reserved block device with three partitions: + an EFI System Partition (ESP) a ROOT partition mounted as read-only that contains the minimal set of binaries to operate system services and a DATA partion that is mounted as read/write at /var/run These partitions are reserved and cannot be modified. The one expection to this is that the DATA partition will be resized automatically in the init process to the maximum size possible. + + AWS https://dianemo.autonomy.io/examples/aws/ diff --git a/docs/sitemap.xml b/docs/sitemap.xml index e65b13464..357291cac 100644 --- a/docs/sitemap.xml +++ b/docs/sitemap.xml @@ -37,6 +37,11 @@ 2018-10-29T19:40:55-07:00 + + https://dianemo.autonomy.io/components/blockd/ + 2018-10-30T09:16:35-07:00 + + https://dianemo.autonomy.io/examples/aws/ 2018-10-29T19:40:55-07:00 diff --git a/docs/tags/index.html b/docs/tags/index.html index 160f5f211..87674ea3d 100644 --- a/docs/tags/index.html +++ b/docs/tags/index.html @@ -170,6 +170,13 @@ + + diff --git a/src/docs/src/content/components/blockd.md b/src/docs/src/content/components/blockd.md new file mode 100644 index 000000000..097c851bd --- /dev/null +++ b/src/docs/src/content/components/blockd.md @@ -0,0 +1,19 @@ +--- +title: "blockd" +date: 2018-10-30T09:16:35-07:00 +draft: false +menu: + main: + parent: 'components' + weight: 80 +--- + +Dianemo comes with a reserved block device with three partitions: + +- an EFI System Partition (`ESP`) +- a `ROOT` partition mounted as read-only that contains the minimal set of binaries to operate system services +- and a `DATA` partion that is mounted as read/write at `/var/run` + +These partitions are reserved and cannot be modified. +The one expection to this is that the `DATA` partition will be resized automatically in the `init` process to the maximum size possible. +Managing any other block device can be done via the `blockd` service. diff --git a/src/image/.conform.yaml b/src/image/.conform.yaml index 00f06c1ab..6b25bf339 100644 --- a/src/image/.conform.yaml +++ b/src/image/.conform.yaml @@ -64,7 +64,7 @@ tasks: COPY src/packer.json /packer.json COPY src/entrypoint.sh /bin/entrypoint.sh RUN chmod +x /bin/entrypoint.sh - {{ if .Git.IsTag }} + {{ if and .Git.IsClean .Git.IsTag }} ENV VERSION {{ .Git.Tag }} {{ else }} ENV VERSION {{ .Git.SHA }} diff --git a/src/image/src/entrypoint.sh b/src/image/src/entrypoint.sh index 24973c607..631786d35 100755 --- a/src/image/src/entrypoint.sh +++ b/src/image/src/entrypoint.sh @@ -11,33 +11,43 @@ function create_image() { if [ "$FULL" = true ] ; then if [ "$RAW" = true ] ; then - parted -s -a optimal ${RAW_IMAGE} mkpart ESP fat32 0 $((${INITRAMFS_SIZE} + 50))M - parted -s -a optimal ${RAW_IMAGE} mkpart ROOT xfs $((${INITRAMFS_SIZE} + 50))M $((${ROOTFS_SIZE} + ${INITRAMFS_SIZE} + 100))M - parted -s -a optimal ${RAW_IMAGE} mkpart DATA xfs $((${ROOTFS_SIZE} + ${INITRAMFS_SIZE} + 100))M 100% + parted -s -a optimal ${RAW_IMAGE} mkpart primary fat32 0 $((${INITRAMFS_SIZE} + 50))M + parted ${RAW_IMAGE} name 1 ESP + parted -s -a optimal ${RAW_IMAGE} mkpart primary xfs $((${INITRAMFS_SIZE} + 50))M $((${ROOTFS_SIZE} + ${INITRAMFS_SIZE} + 100))M + parted ${RAW_IMAGE} name 2 ROOT + parted -s -a optimal ${RAW_IMAGE} mkpart primary xfs $((${ROOTFS_SIZE} + ${INITRAMFS_SIZE} + 100))M 100% + parted ${RAW_IMAGE} name 3 DATA losetup ${DEVICE} ${RAW_IMAGE} partx -av ${DEVICE} extract_boot_partition ${DEVICE}p1 extract_root_partition ${DEVICE}p2 extract_data_partition ${DEVICE}p3 else - parted -s -a optimal ${DEVICE} mkpart ESP fat32 0 $((${INITRAMFS_SIZE} + 50))M - parted -s -a optimal ${DEVICE} mkpart ROOT xfs $((${INITRAMFS_SIZE} + 50))M $((${ROOTFS_SIZE} + ${INITRAMFS_SIZE} + 100))M - parted -s -a optimal ${DEVICE} mkpart DATA xfs $((${ROOTFS_SIZE} + ${INITRAMFS_SIZE} + 100))M 100% + parted -s -a optimal ${DEVICE} mkpart primary fat32 0 $((${INITRAMFS_SIZE} + 50))M + parted ${DEVICE} name 1 ESP + parted -s -a optimal ${DEVICE} mkpart primary xfs $((${INITRAMFS_SIZE} + 50))M $((${ROOTFS_SIZE} + ${INITRAMFS_SIZE} + 100))M + parted ${DEVICE} name 2 ROOT + parted -s -a optimal ${DEVICE} mkpart primary xfs $((${ROOTFS_SIZE} + ${INITRAMFS_SIZE} + 100))M 100% + parted ${DEVICE} name 3 DATA extract_boot_partition ${DEVICE}1 extract_root_partition ${DEVICE}2 extract_data_partition ${DEVICE}3 fi else if [ "$RAW" = true ] ; then - parted -s -a optimal ${RAW_IMAGE} mkpart ROOT xfs 0 $((${ROOTFS_SIZE} + 50))M - parted -s -a optimal ${RAW_IMAGE} mkpart DATA xfs $((${ROOTFS_SIZE} + 50))M 100% + parted -s -a optimal ${RAW_IMAGE} mkpart primary xfs 0 $((${ROOTFS_SIZE} + 50))M + parted ${RAW_IMAGE} name 1 ROOT + parted -s -a optimal ${RAW_IMAGE} mkpart primary xfs $((${ROOTFS_SIZE} + 50))M 100% + parted ${RAW_IMAGE} name 2 DATA losetup ${DEVICE} ${RAW_IMAGE} partx -av ${DEVICE} extract_root_partition ${DEVICE}p1 extract_data_partition ${DEVICE}p2 else - parted -s -a optimal ${DEVICE} mkpart ROOT xfs 0 $((${ROOTFS_SIZE} + 50))M - parted -s -a optimal ${DEVICE} mkpart DATA xfs $((${ROOTFS_SIZE} + 50))M 100% + parted -s -a optimal ${DEVICE} mkpart primary xfs 0 $((${ROOTFS_SIZE} + 50))M + parted ${DEVICE} name 1 ROOT + parted -s -a optimal ${DEVICE} mkpart primary xfs $((${ROOTFS_SIZE} + 50))M 100% + parted ${DEVICE} name 2 DATA extract_root_partition ${DEVICE}1 extract_data_partition ${DEVICE}2 fi diff --git a/src/initramfs/.conform.yaml b/src/initramfs/.conform.yaml index 733ca5c80..605e21eb3 100644 --- a/src/initramfs/.conform.yaml +++ b/src/initramfs/.conform.yaml @@ -3,6 +3,7 @@ metadata: variables: rootfs: /rootfs versionPath: github.com/autonomy/dianemo/src/initramfs/pkg/version + srcXfsprogs: https://www.kernel.org/pub/linux/utils/fs/xfs/xfsprogs/xfsprogs-4.18.0.tar.xz script: template: | #!/bin/sh @@ -18,6 +19,9 @@ script: docker tag {{ .Repository }}:proxyd autonomy/proxyd:{{ .Git.SHA }} docker save autonomy/proxyd:{{ .Git.SHA }} -o ../../build/proxyd.tar + docker tag {{ .Repository }}:blockd autonomy/blockd:{{ .Git.SHA }} + docker save autonomy/blockd:{{ .Git.SHA }} -o ../../build/blockd.tar + {{ if and (.Git.IsClean) (or (.Git.IsTag) (eq .Git.Branch "master")) }} docker login --username=$DOCKER_USERNAME --password=$DOCKER_PASSWORD @@ -32,21 +36,27 @@ script: docker tag autonomy/proxyd:{{ .Git.SHA }} autonomy/proxyd:latest docker push autonomy/proxyd:{{ .Git.SHA }} docker push autonomy/proxyd:latest + + docker tag autonomy/blockd:{{ .Git.SHA }} autonomy/blockd:latest + docker push autonomy/blockd:{{ .Git.SHA }} + docker push autonomy/blockd:latest {{ end }} pipeline: stages: - generate - base + - initramfs - osd - osctl - trustd - proxyd - - initramfs + - blockd + - test + - image stages: base: tasks: - src - - test osd: tasks: - osd @@ -64,10 +74,16 @@ stages: proxyd: tasks: - proxyd + blockd: + tasks: + - blockd initramfs: tasks: + - xfsprogs - init - initramfs + image: + tasks: - image generate: artifacts: @@ -75,26 +91,47 @@ stages: destination: ./cmd/osd - source: /src/github.com/autonomy/dianemo/src/initramfs/cmd/trustd/proto destination: ./cmd/trustd + - source: /src/github.com/autonomy/dianemo/src/initramfs/cmd/blockd/proto + destination: ./cmd/blockd tasks: - proto + test: + tasks: + - test tasks: - image: - template: | - FROM scratch - WORKDIR /tmp - COPY --from=init /initramfs/initramfs.xz initramfs.xz - CMD false - init: + blockd: template: | FROM {{ .Repository }}:base AS {{ .Docker.CurrentStage }} + WORKDIR /src/github.com/autonomy/dianemo/src/initramfs/cmd/{{ .Docker.CurrentStage }} + {{ if and .Git.IsClean .Git.IsTag }} + RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -a \ + -ldflags "-s -w -X {{ index .Variables "versionPath" }}.Name=Blockd -X {{ index .Variables "versionPath" }}.Tag={{ .Git.Tag }} -X {{ index .Variables "versionPath" }}.SHA={{ .Git.SHA }} -X \"{{ index .Variables "versionPath" }}.Built={{ .Built }}\"" \ + -o /{{ .Docker.CurrentStage }} + {{ else }} + RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -a \ + -ldflags "-s -w -X {{ index .Variables "versionPath" }}.Name=Blockd -X {{ index .Variables "versionPath" }}.Tag=none -X {{ index .Variables "versionPath" }}.SHA={{ .Git.SHA }}" \ + -o /{{ .Docker.CurrentStage }} + {{ end }} + RUN chmod +x /{{ .Docker.CurrentStage }} + FROM scratch + COPY --from={{ .Docker.CurrentStage }} /{{ .Docker.CurrentStage }} /{{ .Docker.CurrentStage }} + ENTRYPOINT ["/{{ .Docker.CurrentStage }}"] + image: + template: | + FROM scratch + WORKDIR /tmp + COPY --from=dianemo/initramfs:initramfs /initramfs/initramfs.xz initramfs.xz + CMD false + init: + template: | WORKDIR /src/github.com/autonomy/dianemo/src/initramfs/cmd/{{ .Docker.CurrentStage }} {{ if and .Git.IsClean .Git.IsTag }} RUN GOOS=linux GOARCH=amd64 go build \ - -ldflags "-s -w -linkmode external -extldflags \"-L/lib -lblkid -luuid\" -X {{ index .Variables "versionPath" }}.Name=Dianemo -X {{ index .Variables "versionPath" }}.Tag={{ .Git.Tag }} -X {{ index .Variables "versionPath" }}.SHA={{ .Git.SHA }} -X \"{{ index .Variables "versionPath" }}.Built={{ .Built }}\"" \ + -ldflags "-s -w -X {{ index .Variables "versionPath" }}.Name=Dianemo -X {{ index .Variables "versionPath" }}.Tag={{ .Git.Tag }} -X {{ index .Variables "versionPath" }}.SHA={{ .Git.SHA }} -X \"{{ index .Variables "versionPath" }}.Built={{ .Built }}\"" \ -o /initramfs/init {{ else }} RUN GOOS=linux GOARCH=amd64 go build \ - -ldflags "-s -w -linkmode external -extldflags \"-L/lib -lblkid -luuid\" -X {{ index .Variables "versionPath" }}.Name=Dianemo -X {{ index .Variables "versionPath" }}.Tag=none -X {{ index .Variables "versionPath" }}.SHA={{ .Git.SHA }}" \ + -ldflags "-s -w -X {{ index .Variables "versionPath" }}.Name=Dianemo -X {{ index .Variables "versionPath" }}.Tag=none -X {{ index .Variables "versionPath" }}.SHA={{ .Git.SHA }}" \ -o /initramfs/init {{ end }} RUN chmod +x /initramfs/init @@ -110,7 +147,7 @@ tasks: {{ else }} RUN find . 2>/dev/null | cpio -H newc -o | xz -v -C crc32 -0 -e -T 0 -z >/tmp/initramfs.xz {{ end }} - RUN cp /tmp/initramfs.xz /initramfs/initramfs.xz + RUN cp /tmp/initramfs.xz . osctl: template: | FROM {{ .Repository }}:base AS {{ .Docker.CurrentStage }} @@ -166,6 +203,9 @@ tasks: WORKDIR /src/github.com/autonomy/dianemo/src/initramfs/cmd/trustd COPY ./cmd/trustd/proto ./proto RUN protoc -I/usr/local/include -I./proto --go_out=plugins=grpc:proto proto/api.proto + WORKDIR /src/github.com/autonomy/dianemo/src/initramfs/cmd/blockd + COPY ./cmd/blockd/proto ./proto + RUN protoc -I/usr/local/include -I./proto --go_out=plugins=grpc:proto proto/api.proto proxyd: template: | FROM {{ .Repository }}:base AS {{ .Docker.CurrentStage }} @@ -199,6 +239,7 @@ tasks: RUN go mod verify test: template: | + FROM {{ .Repository }}:base AS {{ .Docker.CurrentStage }} WORKDIR /src/github.com/autonomy/dianemo/src/initramfs RUN chmod +x ./hack/test.sh RUN ./hack/test.sh --lint ./hack/golangci-lint.yaml @@ -220,3 +261,14 @@ tasks: FROM scratch COPY --from={{ .Docker.CurrentStage }} /{{ .Docker.CurrentStage }} /{{ .Docker.CurrentStage }} ENTRYPOINT ["/{{ .Docker.CurrentStage }}"] + xfsprogs: + template: | + FROM {{ .Repository }}:base AS {{ .Docker.CurrentStage }} + WORKDIR /tmp/{{ .Docker.CurrentStage }} + RUN curl -L {{index .Variables "srcXfsprogs" }} | tar -xJ --strip-components=1 + RUN make \ + DEBUG=-DNDEBUG \ + INSTALL_USER=0 \ + INSTALL_GROUP=0 \ + LOCAL_CONFIGURE_OPTIONS="--prefix=/usr" + RUN make install DESTDIR={{ index .Variables "rootfs" }} diff --git a/src/initramfs/cmd/blockd/main.go b/src/initramfs/cmd/blockd/main.go new file mode 100644 index 000000000..cf415f291 --- /dev/null +++ b/src/initramfs/cmd/blockd/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "flag" + "log" + + "github.com/autonomy/dianemo/src/initramfs/cmd/trustd/pkg/reg" + "github.com/autonomy/dianemo/src/initramfs/pkg/grpc/factory" + "github.com/autonomy/dianemo/src/initramfs/pkg/grpc/gen" + "github.com/autonomy/dianemo/src/initramfs/pkg/grpc/tls" + "github.com/autonomy/dianemo/src/initramfs/pkg/userdata" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +var ( + dataPath *string + generate *bool + trustdPort *int +) + +func init() { + log.SetFlags(log.Lshortfile | log.Ldate | log.Lmicroseconds | log.Ltime) + dataPath = flag.String("userdata", "", "the path to the user data") + trustdPort = flag.Int("trustd-port", 50001, "the trustd port") + generate = flag.Bool("generate", false, "generate the TLS certificate using one of the Root of Trusts") + flag.Parse() +} + +func main() { + data, err := userdata.Open(*dataPath) + if err != nil { + log.Fatalf("open user data: %v", err) + } + + if *generate { + var generator *gen.Generator + generator, err = gen.NewGenerator(data, *trustdPort) + if err != nil { + log.Fatal(err) + } + if err = generator.Identity(data.Security); err != nil { + log.Fatalf("generate identity: %v", err) + } + } + + config, err := tls.NewConfig(tls.Mutual, data.Security.OS) + if err != nil { + log.Fatalf("credentials: %v", err) + } + + log.Println("Starting blockd") + err = factory.Listen( + ®.Registrator{Data: data.Security.OS}, + factory.Network("unix"), + factory.ServerOptions( + grpc.Creds( + credentials.NewTLS(config), + ), + ), + ) + if err != nil { + log.Fatalf("listen: %v", err) + } +} diff --git a/src/initramfs/cmd/blockd/pkg/reg/reg.go b/src/initramfs/cmd/blockd/pkg/reg/reg.go new file mode 100644 index 000000000..e08c0b0de --- /dev/null +++ b/src/initramfs/cmd/blockd/pkg/reg/reg.go @@ -0,0 +1,26 @@ +package reg + +import ( + "context" + + "github.com/autonomy/dianemo/src/initramfs/cmd/blockd/proto" + "github.com/autonomy/dianemo/src/initramfs/pkg/userdata" + "github.com/golang/protobuf/ptypes/empty" + "google.golang.org/grpc" +) + +// Registrator is the concrete type that implements the factory.Registrator and +// proto.BlockdServer interfaces. +type Registrator struct { + Data *userdata.OSSecurity +} + +// Register implements the factory.Registrator interface. +func (r *Registrator) Register(s *grpc.Server) { + proto.RegisterBlockdServer(s, r) +} + +// Resize implements the proto.BlockdServer interface. +func (r *Registrator) Resize(ctx context.Context, in *proto.ResizePartitionRequest) (reply *empty.Empty, err error) { + return nil, nil +} diff --git a/src/initramfs/cmd/blockd/proto/api.pb.go b/src/initramfs/cmd/blockd/proto/api.pb.go new file mode 100644 index 000000000..5f6cb2cce --- /dev/null +++ b/src/initramfs/cmd/blockd/proto/api.pb.go @@ -0,0 +1,165 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: api.proto + +package proto + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + empty "github.com/golang/protobuf/ptypes/empty" + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +// The request message containing the process name. +type ResizePartitionRequest struct { + Number int32 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` + Size int64 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ResizePartitionRequest) Reset() { *m = ResizePartitionRequest{} } +func (m *ResizePartitionRequest) String() string { return proto.CompactTextString(m) } +func (*ResizePartitionRequest) ProtoMessage() {} +func (*ResizePartitionRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_00212fb1f9d3bf1c, []int{0} +} + +func (m *ResizePartitionRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ResizePartitionRequest.Unmarshal(m, b) +} +func (m *ResizePartitionRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ResizePartitionRequest.Marshal(b, m, deterministic) +} +func (m *ResizePartitionRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_ResizePartitionRequest.Merge(m, src) +} +func (m *ResizePartitionRequest) XXX_Size() int { + return xxx_messageInfo_ResizePartitionRequest.Size(m) +} +func (m *ResizePartitionRequest) XXX_DiscardUnknown() { + xxx_messageInfo_ResizePartitionRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_ResizePartitionRequest proto.InternalMessageInfo + +func (m *ResizePartitionRequest) GetNumber() int32 { + if m != nil { + return m.Number + } + return 0 +} + +func (m *ResizePartitionRequest) GetSize() int64 { + if m != nil { + return m.Size + } + return 0 +} + +func init() { + proto.RegisterType((*ResizePartitionRequest)(nil), "proto.ResizePartitionRequest") +} + +func init() { proto.RegisterFile("api.proto", fileDescriptor_00212fb1f9d3bf1c) } + +var fileDescriptor_00212fb1f9d3bf1c = []byte{ + // 163 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4c, 0x2c, 0xc8, 0xd4, + 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x05, 0x53, 0x52, 0xd2, 0xe9, 0xf9, 0xf9, 0xe9, 0x39, + 0xa9, 0xfa, 0x60, 0x5e, 0x52, 0x69, 0x9a, 0x7e, 0x6a, 0x6e, 0x41, 0x49, 0x25, 0x44, 0x8d, 0x92, + 0x0b, 0x97, 0x58, 0x50, 0x6a, 0x71, 0x66, 0x55, 0x6a, 0x40, 0x62, 0x51, 0x49, 0x66, 0x49, 0x66, + 0x7e, 0x5e, 0x50, 0x6a, 0x61, 0x69, 0x6a, 0x71, 0x89, 0x90, 0x18, 0x17, 0x5b, 0x5e, 0x69, 0x6e, + 0x52, 0x6a, 0x91, 0x04, 0xa3, 0x02, 0xa3, 0x06, 0x6b, 0x10, 0x94, 0x27, 0x24, 0xc4, 0xc5, 0x02, + 0x52, 0x2f, 0xc1, 0xa4, 0xc0, 0xa8, 0xc1, 0x1c, 0x04, 0x66, 0x1b, 0x79, 0x73, 0xb1, 0x39, 0xe5, + 0xe4, 0x27, 0x67, 0xa7, 0x08, 0x39, 0x72, 0xb1, 0x41, 0xcc, 0x13, 0x92, 0x85, 0xd8, 0xa0, 0x87, + 0xdd, 0x78, 0x29, 0x31, 0x3d, 0x88, 0xb3, 0xf4, 0x60, 0xce, 0xd2, 0x73, 0x05, 0x39, 0x4b, 0x89, + 0x21, 0x89, 0x0d, 0x2c, 0x62, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0xa6, 0xfc, 0x26, 0x27, 0xca, + 0x00, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// BlockdClient is the client API for Blockd service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type BlockdClient interface { + Resize(ctx context.Context, in *ResizePartitionRequest, opts ...grpc.CallOption) (*empty.Empty, error) +} + +type blockdClient struct { + cc *grpc.ClientConn +} + +func NewBlockdClient(cc *grpc.ClientConn) BlockdClient { + return &blockdClient{cc} +} + +func (c *blockdClient) Resize(ctx context.Context, in *ResizePartitionRequest, opts ...grpc.CallOption) (*empty.Empty, error) { + out := new(empty.Empty) + err := c.cc.Invoke(ctx, "/proto.Blockd/Resize", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// BlockdServer is the server API for Blockd service. +type BlockdServer interface { + Resize(context.Context, *ResizePartitionRequest) (*empty.Empty, error) +} + +func RegisterBlockdServer(s *grpc.Server, srv BlockdServer) { + s.RegisterService(&_Blockd_serviceDesc, srv) +} + +func _Blockd_Resize_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ResizePartitionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BlockdServer).Resize(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.Blockd/Resize", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BlockdServer).Resize(ctx, req.(*ResizePartitionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _Blockd_serviceDesc = grpc.ServiceDesc{ + ServiceName: "proto.Blockd", + HandlerType: (*BlockdServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Resize", + Handler: _Blockd_Resize_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api.proto", +} diff --git a/src/initramfs/cmd/blockd/proto/api.proto b/src/initramfs/cmd/blockd/proto/api.proto new file mode 100644 index 000000000..eeb3fb207 --- /dev/null +++ b/src/initramfs/cmd/blockd/proto/api.proto @@ -0,0 +1,17 @@ + +syntax = "proto3"; + +package proto; + +import "google/protobuf/empty.proto"; + +// The Blockd service definition. +service Blockd { + rpc Resize(ResizePartitionRequest) returns (google.protobuf.Empty) {} +} + +// The request message containing the process name. +message ResizePartitionRequest { + int32 number = 1; + int64 size = 2; +} diff --git a/src/initramfs/cmd/init/main.go b/src/initramfs/cmd/init/main.go index 563c8b3a5..8c6bcdd1c 100644 --- a/src/initramfs/cmd/init/main.go +++ b/src/initramfs/cmd/init/main.go @@ -104,6 +104,7 @@ func root() (err error) { &services.Containerd{}, &services.CRT{}, &services.OSD{}, + &services.Blockd{}, &services.Kubelet{}, &services.Kubeadm{}, ) diff --git a/src/initramfs/cmd/init/pkg/fs/xfs/util/util.go b/src/initramfs/cmd/init/pkg/fs/xfs/util/util.go new file mode 100644 index 000000000..c5767767e --- /dev/null +++ b/src/initramfs/cmd/init/pkg/fs/xfs/util/util.go @@ -0,0 +1,27 @@ +package util + +import "C" +import ( + "strings" +) + +func PartNo(partname string) string { + if strings.HasPrefix(partname, "/dev/nvme") { + idx := strings.Index(partname, "p") + return partname[idx+1:] + } else if strings.HasPrefix(partname, "/dev/sd") || strings.HasPrefix(partname, "/dev/hd") { + return strings.TrimLeft(partname, "/abcdefghijklmnopqrstuvwxyz") + } + + return "" +} + +func DevnameFromPartname(partname, partno string) string { + if strings.HasPrefix(partname, "/dev/nvme") { + return strings.TrimRight(partname, "p"+partno) + } else if strings.HasPrefix(partname, "/dev/sd") || strings.HasPrefix(partname, "/dev/hd") { + return strings.TrimRight(partname, partno) + } + + return "" +} diff --git a/src/initramfs/cmd/init/pkg/fs/xfs/util/util_test.go b/src/initramfs/cmd/init/pkg/fs/xfs/util/util_test.go new file mode 100644 index 000000000..6fb35bd24 --- /dev/null +++ b/src/initramfs/cmd/init/pkg/fs/xfs/util/util_test.go @@ -0,0 +1,102 @@ +package util + +import ( + "testing" +) + +func Test_PartNo(t *testing.T) { + type args struct { + devname string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "hda1", + args: args{ + devname: "/dev/hda1", + }, + want: "1", + }, + { + name: "hda10", + args: args{ + devname: "/dev/hda10", + }, + want: "10", + }, + { + name: "sda1", + args: args{ + devname: "/dev/sda1", + }, + want: "1", + }, + { + name: "sda10", + args: args{ + devname: "/dev/sda10", + }, + want: "10", + }, + { + name: "nvme1n2p2", + args: args{ + devname: "/dev/nvme1n2p2", + }, + want: "2", + }, + { + name: "nvme1n2p11", + args: args{ + devname: "/dev/nvme1n2p11", + }, + want: "11", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := PartNo(tt.args.devname); got != tt.want { + t.Errorf("PartNo() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_DevnameFromPartname(t *testing.T) { + type args struct { + devname string + partno string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "hda1", + args: args{ + devname: "/dev/hda1", + partno: PartNo("/dev/hda1"), + }, + want: "/dev/hda", + }, + { + name: "nvme1n2p11", + args: args{ + devname: "/dev/nvme1n2p11", + partno: PartNo("/dev/nvme1n2p11"), + }, + want: "/dev/nvme1n2", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := DevnameFromPartname(tt.args.devname, tt.args.partno); got != tt.want { + t.Errorf("DevnameFromPartname() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/src/initramfs/cmd/init/pkg/fs/xfs/xfs.go b/src/initramfs/cmd/init/pkg/fs/xfs/xfs.go new file mode 100644 index 000000000..ccce371b4 --- /dev/null +++ b/src/initramfs/cmd/init/pkg/fs/xfs/xfs.go @@ -0,0 +1,25 @@ +// Package xfs provides an interface to xfsprogs. +package xfs + +import ( + "os" + "os/exec" +) + +// GrowFS expands an XFS filesystem to the maximum possible. The partition +// MUST be mounted, or this will fail. +func GrowFS(partname string) error { + return cmd("xfs_growfs", "-d", partname) +} + +func cmd(name string, arg ...string) error { + cmd := exec.Command(name, arg...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stdout + err := cmd.Start() + if err != nil { + return err + } + + return cmd.Wait() +} diff --git a/src/initramfs/cmd/init/pkg/mount/mount.go b/src/initramfs/cmd/init/pkg/mount/mount.go index c51505b8f..2e1db7eb0 100644 --- a/src/initramfs/cmd/init/pkg/mount/mount.go +++ b/src/initramfs/cmd/init/pkg/mount/mount.go @@ -6,10 +6,14 @@ import ( "fmt" "os" "path" + "strings" "sync" "github.com/autonomy/dianemo/src/initramfs/cmd/init/pkg/constants" + "github.com/autonomy/dianemo/src/initramfs/cmd/init/pkg/fs/xfs" "github.com/autonomy/dianemo/src/initramfs/cmd/init/pkg/mount/blkid" + "github.com/autonomy/dianemo/src/initramfs/pkg/blockdevice" + gptpartition "github.com/autonomy/dianemo/src/initramfs/pkg/blockdevice/table/gpt/partition" "golang.org/x/sys/unix" ) @@ -62,7 +66,11 @@ func Init(s string) (err error) { if err = mountSpecialDevices(); err != nil { return } - if err = mountBlockDevices(s); err != nil { + blockdevices, err := probe() + if err != nil { + return fmt.Errorf("probe block devices: %s", err.Error()) + } + if err = mountBlockDevices(blockdevices, s); err != nil { return } @@ -168,12 +176,56 @@ func mountSpecialDevices() (err error) { return nil } -func mountBlockDevices(s string) (err error) { - probed, err := probe() - if err != nil { - return fmt.Errorf("probe block devices: %s", err.Error()) +// nolint: gocyclo +func fixDataPartition(blockdevices []*BlockDevice) error { + for _, b := range blockdevices { + if b.LABEL == constants.DataPartitionLabel { + devname := devnameFromPartname(b.dev) + + bd, err := blockdevice.Open(devname) + if err != nil { + return err + } + // nolint: errcheck + defer bd.Close() + + pt, err := bd.PartitionTable() + if err != nil { + return err + } + + if err := pt.Read(); err != nil { + return err + } + + if err := pt.Repair(); err != nil { + return err + } + + for _, partition := range pt.Partitions() { + if partition.(*gptpartition.Partition).Name == constants.DataPartitionLabel { + if err := pt.Resize(partition); err != nil { + return err + } + } + } + + // Rereading the partition table requires that all partitions be unmounted + // or it will fail with EBUSY. + if err := bd.RereadPartitionTable(devname); err != nil { + return err + } + } } - for _, b := range probed { + + return nil +} + +func mountBlockDevices(blockdevices []*BlockDevice, s string) (err error) { + if err = fixDataPartition(blockdevices); err != nil { + return err + } + for _, b := range blockdevices { mountpoint := &Point{ source: b.dev, fstype: b.TYPE, @@ -196,6 +248,13 @@ func mountBlockDevices(s string) (err error) { return fmt.Errorf("mount %s: %s", mountpoint.target, err.Error()) } + if b.LABEL == constants.DataPartitionLabel { + // The XFS partition MUST be mounted, or this will fail. + if err = xfs.GrowFS(mountpoint.target); err != nil { + return err + } + } + instance.blockdevices[b.LABEL] = mountpoint } @@ -258,3 +317,25 @@ func probeDevice(devname string) (*BlockDevice, error) { LABEL: LABEL, }, nil } + +func partNo(partname string) string { + if strings.HasPrefix(partname, "/dev/nvme") { + idx := strings.Index(partname, "p") + return partname[idx+1:] + } else if strings.HasPrefix(partname, "/dev/sd") || strings.HasPrefix(partname, "/dev/hd") { + return strings.TrimLeft(partname, "/abcdefghijklmnopqrstuvwxyz") + } + + return "" +} + +func devnameFromPartname(partname string) string { + partno := partNo(partname) + if strings.HasPrefix(partname, "/dev/nvme") { + return strings.TrimRight(partname, "p"+partno) + } else if strings.HasPrefix(partname, "/dev/sd") || strings.HasPrefix(partname, "/dev/hd") { + return strings.TrimRight(partname, partno) + } + + return "" +} diff --git a/src/initramfs/cmd/init/pkg/system/services/blockd.go b/src/initramfs/cmd/init/pkg/system/services/blockd.go new file mode 100644 index 000000000..4486351cb --- /dev/null +++ b/src/initramfs/cmd/init/pkg/system/services/blockd.go @@ -0,0 +1,78 @@ +// nolint: dupl,golint +package services + +import ( + "os" + + "github.com/autonomy/dianemo/src/initramfs/cmd/init/pkg/constants" + "github.com/autonomy/dianemo/src/initramfs/cmd/init/pkg/system/conditions" + "github.com/autonomy/dianemo/src/initramfs/cmd/init/pkg/system/runner" + "github.com/autonomy/dianemo/src/initramfs/cmd/init/pkg/system/runner/containerd" + "github.com/autonomy/dianemo/src/initramfs/pkg/userdata" + "github.com/autonomy/dianemo/src/initramfs/pkg/version" + "github.com/containerd/containerd/oci" + specs "github.com/opencontainers/runtime-spec/specs-go" +) + +// Blockd implements the Service interface. It serves as the concrete type with +// the required methods. +type Blockd struct{} + +// ID implements the Service interface. +func (t *Blockd) ID(data *userdata.UserData) string { + return "blockd" +} + +// PreFunc implements the Service interface. +func (t *Blockd) PreFunc(data *userdata.UserData) error { + return os.Mkdir("/run/blockd", os.ModeDir) +} + +// PostFunc implements the Service interface. +func (t *Blockd) PostFunc(data *userdata.UserData) (err error) { + return nil +} + +// ConditionFunc implements the Service interface. +func (t *Blockd) ConditionFunc(data *userdata.UserData) conditions.ConditionFunc { + return conditions.None() +} + +func (t *Blockd) Start(data *userdata.UserData) error { + // Set the image. + var image string + if data.Services.Blockd != nil && data.Services.Blockd.Image != "" { + image = data.Services.Blockd.Image + } else { + image = "docker.io/autonomy/blockd:" + version.SHA + } + + // Set the process arguments. + args := runner.Args{ + ID: t.ID(data), + ProcessArgs: []string{"/blockd", "--userdata=" + constants.UserDataPath}, + } + if data.Services.Kubeadm.Init == nil { + args.ProcessArgs = append(args.ProcessArgs, "--generate=true") + } + + // Set the mounts. + mounts := []specs.Mount{ + {Type: "bind", Destination: "/dev", Source: "/dev", Options: []string{"rbind", "rshared", "rw"}}, + {Type: "bind", Destination: constants.UserDataPath, Source: constants.UserDataPath, Options: []string{"rbind", "ro"}}, + {Type: "bind", Destination: "/var/etc/kubernetes", Source: "/var/etc/kubernetes", Options: []string{"bind", "rw"}}, + {Type: "bind", Destination: "/run/factory", Source: "/run/blockd", Options: []string{"rbind", "rshared", "rw"}}, + } + + r := containerd.Containerd{} + + return r.Run( + data, + args, + runner.WithContainerImage(image), + runner.WithOCISpecOpts( + containerd.WithMemoryLimit(int64(1000000*512)), + oci.WithMounts(mounts), + ), + ) +} diff --git a/src/initramfs/cmd/osd/main.go b/src/initramfs/cmd/osd/main.go index 943ce1db9..2959bff1c 100644 --- a/src/initramfs/cmd/osd/main.go +++ b/src/initramfs/cmd/osd/main.go @@ -4,10 +4,9 @@ import ( "flag" "log" - "github.com/autonomy/dianemo/src/initramfs/cmd/osd/pkg/gen" "github.com/autonomy/dianemo/src/initramfs/cmd/osd/pkg/reg" "github.com/autonomy/dianemo/src/initramfs/pkg/grpc/factory" - "github.com/autonomy/dianemo/src/initramfs/pkg/grpc/middleware/auth/basic" + "github.com/autonomy/dianemo/src/initramfs/pkg/grpc/gen" "github.com/autonomy/dianemo/src/initramfs/pkg/grpc/tls" "github.com/autonomy/dianemo/src/initramfs/pkg/userdata" "google.golang.org/grpc" @@ -15,48 +14,33 @@ import ( ) var ( - dataPath *string - generate *bool - port *int - rotPort *int + dataPath *string + generate *bool + port *int + trustdPort *int ) func init() { log.SetFlags(log.Lshortfile | log.Ldate | log.Lmicroseconds | log.Ltime) dataPath = flag.String("userdata", "", "the path to the user data") port = flag.Int("port", 50000, "the port to listen on") - rotPort = flag.Int("rot-port", 50001, "the port to listen on") + trustdPort = flag.Int("trustd-port", 50001, "the trustd port") generate = flag.Bool("generate", false, "generate the TLS certificate using one of the Root of Trusts") flag.Parse() } func main() { - var err error - data, err := userdata.Open(*dataPath) if err != nil { log.Fatalf("open user data: %v", err) } if *generate { - if len(data.Services.Trustd.Endpoints) == 0 { - log.Fatalf("at least one root of trust endpoint is required") - } - - creds := basic.NewCredentials( - data.Security.OS.CA.Crt, - data.Services.Trustd.Username, - data.Services.Trustd.Password, - ) - - // TODO: In the case of failure, attempt to generate the identity from - // another RoT. - var conn *grpc.ClientConn - conn, err = basic.NewConnection(data.Services.Trustd.Endpoints[0], *rotPort, creds) + var generator *gen.Generator + generator, err = gen.NewGenerator(data, *trustdPort) if err != nil { - return + log.Fatal(err) } - generator := gen.NewGenerator(conn) if err = generator.Identity(data.Security); err != nil { log.Fatalf("generate identity: %v", err) } @@ -67,6 +51,7 @@ func main() { log.Fatalf("credentials: %v", err) } + log.Println("Starting osd") err = factory.Listen( ®.Registrator{Data: data}, factory.Port(*port), diff --git a/src/initramfs/go.mod b/src/initramfs/go.mod index ea34dc43b..427fc7e8e 100644 --- a/src/initramfs/go.mod +++ b/src/initramfs/go.mod @@ -34,6 +34,7 @@ require ( github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect github.com/google/go-cmp v0.2.0 // indirect github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect + github.com/google/uuid v1.0.0 // indirect github.com/googleapis/gnostic v0.2.0 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect @@ -69,7 +70,7 @@ require ( golang.org/x/net v0.0.0-20180724234803-3673e40ba225 golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced // indirect golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect - golang.org/x/sys v0.0.0-20180302081741-dd2ff4accc09 + golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect google.golang.org/appengine v1.2.0 // indirect google.golang.org/genproto v0.0.0-20180831171423-11092d34479b // indirect diff --git a/src/initramfs/go.sum b/src/initramfs/go.sum index 98ced3de0..a67fd58e8 100644 --- a/src/initramfs/go.sum +++ b/src/initramfs/go.sum @@ -64,6 +64,8 @@ github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g= github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= @@ -136,6 +138,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6Zh golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180302081741-dd2ff4accc09 h1:wNPZbZUOH0tyqngVRXeF2iQm19+ssqyebJTCFBvxsow= golang.org/x/sys v0.0.0-20180302081741-dd2ff4accc09/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8 h1:R91KX5nmbbvEd7w370cbVzKC+EzCTGqZq63Zad5IcLM= +golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM= diff --git a/src/initramfs/pkg/blockdevice/blockdevice.go b/src/initramfs/pkg/blockdevice/blockdevice.go new file mode 100644 index 000000000..1e568f605 --- /dev/null +++ b/src/initramfs/pkg/blockdevice/blockdevice.go @@ -0,0 +1,69 @@ +// Package blockdevice provides a library for working with block devices. +package blockdevice + +import ( + "fmt" + "os" + + "github.com/autonomy/dianemo/src/initramfs/pkg/blockdevice/table" + "github.com/autonomy/dianemo/src/initramfs/pkg/blockdevice/table/gpt" + "golang.org/x/sys/unix" +) + +// BlockDevice represents a block device. +type BlockDevice struct { + table table.PartitionTable + + f *os.File +} + +// Open initializes and returns a block device. +func Open(devname string) (*BlockDevice, error) { + f, err := os.OpenFile(devname, os.O_RDWR, os.ModeDevice) + if err != nil { + return nil, err + } + // TODO: Dynamically detect MBR/GPT. + // TODO: Use BLKGETSIZE ioctl to get the size. + // TODO: Use BLKPBSZGET ioctl to get the physical sector size. + // TODO: Use BLKSSZGET ioctl to get the logical sector size. + // and pass them into gpt as options. + bd := &BlockDevice{ + table: gpt.NewGPT(devname, f), + } + + return bd, nil +} + +// Close closes the block devices's open file. +func (bd *BlockDevice) Close() error { + return bd.f.Close() +} + +// PartitionTable returns the block device partition table. +func (bd *BlockDevice) PartitionTable() (table.PartitionTable, error) { + if bd.table == nil { + return nil, fmt.Errorf("missing partition table") + } + + return bd.table, nil +} + +// RereadPartitionTable invokes the BLKRRPART ioctl have the kernel read the +// partition table. +func (bd *BlockDevice) RereadPartitionTable(devname string) error { + f, err := os.Open(devname) + if err != nil { + return err + } + unix.Sync() + if _, _, ret := unix.Syscall(unix.SYS_IOCTL, f.Fd(), unix.BLKRRPART, 0); ret != 0 { + return fmt.Errorf("re-read partition table: %v", ret) + } + if err := f.Sync(); err != nil { + return err + } + unix.Sync() + + return nil +} diff --git a/src/initramfs/pkg/blockdevice/pkg/lba/lba.go b/src/initramfs/pkg/blockdevice/pkg/lba/lba.go new file mode 100644 index 000000000..2e47985bc --- /dev/null +++ b/src/initramfs/pkg/blockdevice/pkg/lba/lba.go @@ -0,0 +1,44 @@ +// Package lba provides a library for working with Logical Block Addresses. +package lba + +import ( + "fmt" +) + +// Range represents a range of Logical Block Addresses. +type Range struct { + Start uint64 + End uint64 +} + +// LogicalBlockAddresser represents Logical Block Addressing. +type LogicalBlockAddresser struct { + PhysicalBlockSize int + LogicalBlockSize int +} + +// Make returns a slice from a source slice in the the specified range inclusively. +func (lba *LogicalBlockAddresser) Make(size int) []byte { + return make([]byte, lba.PhysicalBlockSize*size) +} + +// Copy copies from src to dst in the specified range. +func (lba *LogicalBlockAddresser) Copy(dst []byte, src []byte, rng Range) (int, error) { + size := uint64(lba.PhysicalBlockSize) + n := copy(dst[size*rng.Start:size*rng.End], src) + + if n != len(src) { + return -1, fmt.Errorf("expected to write %d elements, wrote %d", len(src), n) + } + + return n, nil +} + +// From returns a slice from a source slice in the the specified range inclusively. +func (lba *LogicalBlockAddresser) From(src []byte, rng Range) ([]byte, error) { + size := uint64(lba.PhysicalBlockSize) + if uint64(len(src)) < size+size*rng.End { + return nil, fmt.Errorf("cannot read LBA range (start: %d, end %d), source too small", rng.Start, rng.End) + } + return src[size*rng.Start : size+size*rng.End], nil +} diff --git a/src/initramfs/pkg/blockdevice/pkg/serde/serde.go b/src/initramfs/pkg/blockdevice/pkg/serde/serde.go new file mode 100644 index 000000000..b59609308 --- /dev/null +++ b/src/initramfs/pkg/blockdevice/pkg/serde/serde.go @@ -0,0 +1,68 @@ +package serde + +import ( + "fmt" +) + +// Serde describes a serializer/deserializer. +type Serde interface { + Fields() []*Field +} + +// FieldSerializerFunc is the func signature for serialization. +type FieldSerializerFunc = func([]byte, interface{}) error + +// FieldDeserializerFunc is the func signature for deserialization. +type FieldDeserializerFunc = func(uint32, uint32, []byte, interface{}) ([]byte, error) + +// Field represents a field in a datastructure. +type Field struct { + Offset uint32 + Length uint32 + Contents *[]byte + SerializerFunc FieldSerializerFunc + DeserializerFunc FieldDeserializerFunc +} + +// Ser serializes a field. +func Ser(t Serde, data []byte, offset uint32, opts interface{}) error { + for _, field := range t.Fields() { + if field.SerializerFunc == nil { + return fmt.Errorf("the field is missing the serializer function") + } + + if err := field.SerializerFunc(data[field.start(offset):field.end(offset)], opts); err != nil { + return err + } + } + + return nil +} + +// De deserializes a field. +func De(t Serde, data []byte, offset uint32, opts interface{}) error { + for _, field := range t.Fields() { + if field.DeserializerFunc == nil { + return fmt.Errorf("the field is missing the deserializer function") + } + + contents, err := field.DeserializerFunc(field.Offset, field.Length, data, opts) + if err != nil { + return err + } + + if n := copy(data[field.start(offset):field.end(offset)], contents); uint32(n) != field.Length { + return fmt.Errorf("expected to write %d elements, wrote %d", field.Length, n) + } + } + + return nil +} + +func (fld *Field) start(offset uint32) uint32 { + return fld.Offset + offset +} + +func (fld *Field) end(offset uint32) uint32 { + return fld.Offset + fld.Length + offset +} diff --git a/src/initramfs/pkg/blockdevice/table/gpt/gpt.go b/src/initramfs/pkg/blockdevice/table/gpt/gpt.go new file mode 100644 index 000000000..7d5c11e67 --- /dev/null +++ b/src/initramfs/pkg/blockdevice/table/gpt/gpt.go @@ -0,0 +1,416 @@ +// Package gpt provides a library for working with GPT partitions. +package gpt + +import ( + "fmt" + "os" + "syscall" + "unsafe" + + "github.com/autonomy/dianemo/src/initramfs/pkg/blockdevice/pkg/lba" + "github.com/autonomy/dianemo/src/initramfs/pkg/blockdevice/pkg/serde" + "github.com/autonomy/dianemo/src/initramfs/pkg/blockdevice/table" + "github.com/autonomy/dianemo/src/initramfs/pkg/blockdevice/table/gpt/header" + "github.com/autonomy/dianemo/src/initramfs/pkg/blockdevice/table/gpt/partition" + "github.com/google/uuid" + "golang.org/x/sys/unix" +) + +// GPT represents the GUID partition table. +type GPT struct { + table table.Table + header *header.Header + partitions []table.Partition + lba *lba.LogicalBlockAddresser + + devname string + f *os.File +} + +// NewGPT initializes and returns a GUID partition table. +func NewGPT(devname string, f *os.File, setters ...interface{}) *GPT { + opts := NewDefaultOptions(setters...) + + lba := &lba.LogicalBlockAddresser{ + PhysicalBlockSize: opts.PhysicalBlockSize, + LogicalBlockSize: opts.LogicalBlockSize, + } + + return &GPT{ + lba: lba, + devname: devname, + f: f, + } +} + +// Bytes returns the partition table as a byte slice. +func (gpt *GPT) Bytes() []byte { + return gpt.table +} + +// Type returns the partition type. +func (gpt *GPT) Type() table.Type { + return table.GPT +} + +// Header returns the header. +func (gpt *GPT) Header() table.Header { + return gpt.header +} + +// Partitions returns the partitions. +func (gpt *GPT) Partitions() []table.Partition { + return gpt.partitions +} + +// Read performs reads the partition table. +func (gpt *GPT) Read() error { + primaryTable, err := gpt.readPrimary() + if err != nil { + return err + } + + serializedHeader, err := gpt.serializeHeader(primaryTable) + if err != nil { + return err + } + + serializedPartitions, err := gpt.serializePartitions(serializedHeader) + if err != nil { + return err + } + + gpt.table = primaryTable + gpt.header = serializedHeader + gpt.partitions = serializedPartitions + + return nil +} + +// Write writes the partition table to disk. +func (gpt *GPT) Write() error { + partitions, err := gpt.deserializePartitions() + if err != nil { + return err + } + + if err := gpt.writePrimary(partitions); err != nil { + return fmt.Errorf("failed to write primary table: %v", err) + } + + if err := gpt.writeSecondary(partitions); err != nil { + return fmt.Errorf("failed to write primary table: %v", err) + } + + return gpt.Read() +} + +// Write the primary table. +func (gpt *GPT) writePrimary(partitions []byte) error { + header, err := gpt.deserializeHeader(partitions) + if err != nil { + return err + } + + table, err := gpt.newTable(header, partitions, lba.Range{Start: 0, End: 1}, lba.Range{Start: 1, End: 33}) + if err != nil { + return err + } + + written, err := gpt.f.WriteAt(table, int64(gpt.PhysicalBlockSize())) + if err != nil { + return err + } + + if written != len(table) { + return fmt.Errorf("expected a primary table write of %d bytes, got %d", len(table), written) + } + + return nil +} + +// Write the secondary table. +func (gpt *GPT) writeSecondary(partitions []byte) error { + header, err := gpt.deserializeHeader(partitions, header.WithHeaderPrimary(false)) + if err != nil { + return err + } + + table, err := gpt.newTable(header, partitions, lba.Range{Start: 32, End: 33}, lba.Range{Start: 0, End: 32}) + if err != nil { + return err + } + + offset := int64((gpt.header.LastUsableLBA + 1)) + written, err := gpt.f.WriteAt(table, offset*int64(gpt.PhysicalBlockSize())) + if err != nil { + return err + } + + if written != len(table) { + return fmt.Errorf("expected a secondary table write of %d bytes, got %d", len(table), written) + } + + return nil +} + +// Repair repairs the partition table. +func (gpt *GPT) Repair() error { + // Seek to the end to get the size. + size, err := gpt.f.Seek(0, 2) + if err != nil { + return err + } + // Reset and seek to the beginning. + _, err = gpt.f.Seek(0, 0) + if err != nil { + return err + } + + gpt.header.BackupLBA = uint64(size/int64(gpt.lba.PhysicalBlockSize) - 1) + gpt.header.LastUsableLBA = gpt.header.BackupLBA - 33 + + return gpt.Write() +} + +// Add adds a partition. +func (gpt *GPT) Add(size uint64, setters ...interface{}) (table.Partition, error) { + opts := partition.NewDefaultOptions(setters...) + + var start, end uint64 + if len(gpt.partitions) == 0 { + start = gpt.header.FirstUsableLBA + } else { + previous := gpt.partitions[len(gpt.partitions)-1] + start = previous.(*partition.Partition).LastLBA + 1 + } + end = start + size/uint64(gpt.PhysicalBlockSize()) + + if end > gpt.header.LastUsableLBA { + // TODO: This calculation is wrong, fix it. + available := (gpt.header.LastUsableLBA - start) * uint64(gpt.PhysicalBlockSize()) + return nil, fmt.Errorf("requested partition size %d is too big, largest available is %d", size, available) + } + + uuid, err := uuid.NewUUID() + if err != nil { + return nil, err + } + + partition := &partition.Partition{ + Type: opts.Type, + ID: uuid, + FirstLBA: start, + LastLBA: end, + // TODO: Flags should be an option. + Flags: 0, + Name: opts.Name, + Number: int32(len(gpt.partitions) + 1), + } + + gpt.partitions = append(gpt.partitions, partition) + + if err := gpt.Write(); err != nil { + return nil, fmt.Errorf("failed to add partition: %v", err) + } + + if err := gpt.InformKernelOfAdd(gpt.devname, partition); err != nil { + return nil, err + } + + return partition, nil +} + +// Resize resizes a partition. +// TODO: Verify that we can indeed grow this partition safely. +func (gpt *GPT) Resize(p table.Partition) error { + partition, ok := p.(*partition.Partition) + if !ok { + return fmt.Errorf("partition is not a GUID partition table partition") + } + + // TODO: This should be a parameter. + partition.LastLBA = gpt.header.LastUsableLBA + + index := partition.Number - 1 + if len(gpt.partitions) < int(index) { + return fmt.Errorf("unknown partition %d, only %d available", partition.Number, len(gpt.partitions)) + } + + gpt.partitions[index] = partition + + if err := gpt.Write(); err != nil { + return fmt.Errorf("failed to grow partitioin: %v", err) + } + + return gpt.InformKernelOfResize(gpt.devname, p) +} + +// Delete deletes a partition. +func (gpt *GPT) Delete(partition table.Partition) error { + return nil +} + +// PhysicalBlockSize returns the physical block size. +func (gpt *GPT) PhysicalBlockSize() int { + return gpt.lba.PhysicalBlockSize +} + +// TODO: Rename this func, it doesn't deserialize anything. +func (gpt *GPT) readPrimary() ([]byte, error) { + // LBA 34 is the first usable sector on the disk. + table := gpt.lba.Make(34) + read, err := gpt.f.ReadAt(table, 0) + if err != nil { + return nil, err + } + + if read != len(table) { + return nil, fmt.Errorf("expected a read of %d bytes, got %d", len(table), read) + } + + return table, nil +} + +func (gpt *GPT) newTable(header, partitions []byte, headerRange, paritionsRange lba.Range) ([]byte, error) { + table := gpt.lba.Make(33) + + if _, err := gpt.lba.Copy(table, header, headerRange); err != nil { + return nil, fmt.Errorf("failed to copy header data: %v", err) + } + + if _, err := gpt.lba.Copy(table, partitions, paritionsRange); err != nil { + return nil, fmt.Errorf("failed to copy partition data: %v", err) + } + + return table, nil +} + +func (gpt *GPT) serializeHeader(table []byte) (*header.Header, error) { + // GPT header is in LBA 1. + data, err := gpt.lba.From(table, lba.Range{Start: 1, End: 1}) + if err != nil { + return nil, err + } + + hdr := header.NewHeader(data, gpt.lba) + + opts := header.NewDefaultOptions(header.WithHeaderTable(table)) + if err := serde.Ser(hdr, hdr.Bytes(), 0, opts); err != nil { + return nil, fmt.Errorf("failed to serialize the header: %v", err) + } + + return hdr, nil +} + +func (gpt *GPT) deserializeHeader(partitions []byte, setters ...interface{}) ([]byte, error) { + data := gpt.lba.Make(1) + setters = append(setters, header.WithHeaderArrayBytes(partitions)) + opts := header.NewDefaultOptions(setters...) + if err := serde.De(gpt.header, data, 0, opts); err != nil { + return nil, fmt.Errorf("failed to deserialize the header: %v", err) + } + + return data, nil +} + +func (gpt *GPT) serializePartitions(header *header.Header) ([]table.Partition, error) { + partitions := make([]table.Partition, 0, header.NumberOfPartitionEntries) + + for i := uint32(0); i < header.NumberOfPartitionEntries; i++ { + offset := i * header.PartitionEntrySize + data := header.ArrayBytes()[offset : offset+header.PartitionEntrySize] + prt := partition.NewPartition(data) + + if err := serde.Ser(prt, header.ArrayBytes(), offset, nil); err != nil { + return nil, fmt.Errorf("failed to serialize the partitions: %v", err) + } + + // The first LBA of the partition cannot start before the first usable + // LBA specified in the header. + if prt.FirstLBA >= header.FirstUsableLBA { + prt.Number = int32(i) + 1 + partitions = append(partitions, prt) + } + } + + return partitions, nil +} + +func (gpt *GPT) deserializePartitions() ([]byte, error) { + // TODO: Should this be a method on the Header struct? + data := make([]byte, gpt.header.NumberOfPartitionEntries*gpt.header.PartitionEntrySize) + + for j, p := range gpt.partitions { + i := uint32(j) + partition, ok := p.(*partition.Partition) + if !ok { + return nil, fmt.Errorf("partition is not a GUID partition table partition") + } + if err := serde.De(partition, data, i*gpt.header.PartitionEntrySize, nil); err != nil { + return nil, fmt.Errorf("failed to deserialize the partitions: %v", err) + } + } + + return data, nil +} + +// InformKernelOfAdd invokes the BLKPG_ADD_PARTITION ioctl. +func (gpt *GPT) InformKernelOfAdd(devname string, partition table.Partition) error { + f, err := os.Open(devname) + if err != nil { + return err + } + // nolint: errcheck + defer f.Close() + + return inform(f.Fd(), partition, unix.BLKPG_ADD_PARTITION, int64(gpt.lba.PhysicalBlockSize)) +} + +// InformKernelOfResize invokes the BLKPG_RESIZE_PARTITION ioctl. +func (gpt *GPT) InformKernelOfResize(devname string, partition table.Partition) error { + f, err := os.Open(devname) + if err != nil { + return err + } + // nolint: errcheck + defer f.Close() + + return inform(f.Fd(), partition, unix.BLKPG_RESIZE_PARTITION, int64(gpt.lba.PhysicalBlockSize)) +} + +// InformKernelOfDelete invokes the BLKPG_DEL_PARTITION ioctl. +func (gpt *GPT) InformKernelOfDelete(devname string, partition table.Partition) error { + f, err := os.Open(devname) + if err != nil { + return err + } + // nolint: errcheck + defer f.Close() + + return inform(f.Fd(), partition, unix.BLKPG_DEL_PARTITION, int64(gpt.lba.PhysicalBlockSize)) +} + +func inform(fd uintptr, partition table.Partition, op int32, blocksize int64) error { + arg := &unix.BlkpgIoctlArg{ + Op: op, + Data: (*byte)(unsafe.Pointer(&unix.BlkpgPartition{ + Start: partition.Start() * blocksize, + Length: partition.Length() * blocksize, + Pno: partition.No(), + })), + } + + _, _, errno := syscall.Syscall( + syscall.SYS_IOCTL, + fd, + unix.BLKPG, + uintptr(unsafe.Pointer(arg)), + ) + + if errno != 0 { + return errno + } + + return nil +} diff --git a/src/initramfs/pkg/blockdevice/table/gpt/gpt_options.go b/src/initramfs/pkg/blockdevice/table/gpt/gpt_options.go new file mode 100644 index 000000000..be4464976 --- /dev/null +++ b/src/initramfs/pkg/blockdevice/table/gpt/gpt_options.go @@ -0,0 +1,49 @@ +package gpt + +// Options is the functional options struct. +type Options struct { + PrimaryGPT bool + PhysicalBlockSize int + LogicalBlockSize int +} + +// Option is the functional option func. +type Option func(*Options) + +// WithPrimaryGPT sets the contents of offset 24 in the GPT header to the location of the primary header. +func WithPrimaryGPT(o bool) Option { + return func(args *Options) { + args.PrimaryGPT = o + } +} + +// WithPhysicalBlockSize sets the physical block size. +func WithPhysicalBlockSize(o int) Option { + return func(args *Options) { + args.PhysicalBlockSize = o + } +} + +// WithLogicalBlockSize sets the logical block size. +func WithLogicalBlockSize(o int) Option { + return func(args *Options) { + args.LogicalBlockSize = o + } +} + +// NewDefaultOptions initializes a Options struct with default values. +func NewDefaultOptions(setters ...interface{}) *Options { + opts := &Options{ + PrimaryGPT: true, + PhysicalBlockSize: 512, + LogicalBlockSize: 512, + } + + for _, setter := range setters { + if s, ok := setter.(Option); ok { + s(opts) + } + } + + return opts +} diff --git a/src/initramfs/pkg/blockdevice/table/gpt/header/header.go b/src/initramfs/pkg/blockdevice/table/gpt/header/header.go new file mode 100644 index 000000000..622c476e0 --- /dev/null +++ b/src/initramfs/pkg/blockdevice/table/gpt/header/header.go @@ -0,0 +1,418 @@ +// Package header provides a library for working with GPT headers. +package header + +import ( + "bytes" + "encoding/binary" + "fmt" + "hash/crc32" + + "github.com/autonomy/dianemo/src/initramfs/pkg/blockdevice/pkg/lba" + "github.com/autonomy/dianemo/src/initramfs/pkg/blockdevice/pkg/serde" + "github.com/google/uuid" +) + +const ( + // HeaderSize is the GUID partition table header size in bytes. + HeaderSize = 92 +) + +// Header represents a GUID partition table. +type Header struct { + data []byte + array []byte + + Signature string // 0 + Revision uint32 // 8 + Size uint32 // 12 + CRC uint32 // 16 + Reserved uint32 // 20 + CurrentLBA uint64 // 24 + BackupLBA uint64 // 32 + FirstUsableLBA uint64 // 40 + LastUsableLBA uint64 // 48 + GUUID uuid.UUID // 56 + PartitionEntriesStartLBA uint64 // 72 + NumberOfPartitionEntries uint32 // 80 + PartitionEntrySize uint32 // 84 + PartitionsArrayCRC uint32 // 88 + TrailingBytes []byte // 92 + + *lba.LogicalBlockAddresser +} + +// NewHeader inializes and returns a GUID partition table header. +func NewHeader(data []byte, lba *lba.LogicalBlockAddresser) *Header { + return &Header{ + data: data, + LogicalBlockAddresser: lba, + } +} + +// Bytes implements the table.Header interface. +func (hdr *Header) Bytes() []byte { + return hdr.data +} + +// ArrayBytes returns the GUID partition table partitions entries array as a byte slice. +func (hdr *Header) ArrayBytes() []byte { + return hdr.array +} + +// Fields impements the serde.Serde interface. +// nolint: gocyclo +func (hdr *Header) Fields() []*serde.Field { + return []*serde.Field{ + // 8 bytes Signature ("EFI PART", 45h 46h 49h 20h 50h 41h 52h 54h or 0x5452415020494645ULL on little-endian machines) + { + Offset: 0, + Length: 8, + // Contents: []byte{0x45, 0x46, 0x49, 0x20, 0x50, 0x41, 0x52, 0x54}, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + return []byte{0x45, 0x46, 0x49, 0x20, 0x50, 0x41, 0x52, 0x54}, nil + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + signature := string(contents) + if signature != "EFI PART" { + return fmt.Errorf("expected signature of \"EFI PART\", got %q", signature) + } + + hdr.Signature = string(contents) + + return nil + }, + }, + // 4 bytes Revision (for GPT version 1.0 (through at least UEFI version 2.7 (May 2017)), the value is 00h 00h 01h 00h) + { + Offset: 8, + Length: 4, + // Contents: []byte{0x00, 0x00, 0x01, 0x00}, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + data := make([]byte, length) + binary.LittleEndian.PutUint32(data, hdr.Revision) + + return data, nil + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + expected := []byte{0x00, 0x00, 0x01, 0x00} + if !bytes.Equal(contents, expected) { + return fmt.Errorf("expected revision of %v, got %v", expected, contents) + } + + hdr.Revision = binary.LittleEndian.Uint32(contents) + + return nil + }, + }, + // 4 bytes Header size in little endian (in bytes, usually 5Ch 00h 00h 00h or 92 bytes) + { + Offset: 12, + Length: 4, + // Contents: []byte{0x5c, 0x00, 0x00, 0x00}, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + data := make([]byte, length) + binary.LittleEndian.PutUint32(data, hdr.Size) + + return data, nil + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + hdr.Size = binary.LittleEndian.Uint32(contents) + if hdr.Size != HeaderSize { + return fmt.Errorf("expected GPT header size of %d, got %d", HeaderSize, hdr.Size) + } + + return nil + }, + }, + // 4 bytes Reserved; must be zero + { + Offset: 20, + Length: 4, + // Contents: []byte{0x00, 0x00, 0x00, 0x00}, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + return []byte{0x00, 0x00, 0x00, 0x00}, nil + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + expected := []byte{0x00, 0x00, 0x00, 0x00} + if !bytes.Equal(contents, expected) { + return fmt.Errorf("expected reserved field to be %v, got %v", expected, contents) + } + + hdr.Reserved = binary.LittleEndian.Uint32(contents) + + return nil + }, + }, + // 8 bytes Current LBA (location of this header copy) + // nolint: dupl + { + Offset: 24, + Length: 8, + // Contents: []byte{0x00, 0x00, 0x00, 0x00}, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + data := make([]byte, length) + o, ok := opts.(*Options) + if !ok { + return nil, fmt.Errorf("option is not a GPT header option") + } + if o.Primary { + binary.LittleEndian.PutUint64(data, hdr.CurrentLBA) + } else { + binary.LittleEndian.PutUint64(data, hdr.BackupLBA) + } + + return data, nil + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + hdr.CurrentLBA = binary.LittleEndian.Uint64(contents) + + return nil + }, + }, + // 8 bytes Backup LBA (location of the other header copy) + // nolint: dupl + { + Offset: 32, + Length: 8, + // Contents: []byte{0x00, 0x00, 0x00, 0x00}, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + data := make([]byte, length) + o, ok := opts.(*Options) + if !ok { + return nil, fmt.Errorf("option is not a GPT header option") + } + if o.Primary { + binary.LittleEndian.PutUint64(data, hdr.BackupLBA) + + } else { + binary.LittleEndian.PutUint64(data, hdr.CurrentLBA) + } + + return data, nil + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + hdr.BackupLBA = binary.LittleEndian.Uint64(contents) + + return nil + }, + }, + // 8 bytes First usable LBA for partitions (primary partition table last LBA + 1) + { + Offset: 40, + Length: 8, + // Contents: []byte{0x00, 0x00, 0x00, 0x00}, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + data := make([]byte, length) + binary.LittleEndian.PutUint64(data, hdr.FirstUsableLBA) + + return data, nil + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + hdr.FirstUsableLBA = binary.LittleEndian.Uint64(contents) + + return nil + }, + }, + // 8 bytes Last usable LBA (secondary partition table first LBA - 1) + { + Offset: 48, + Length: 8, + // Contents: []byte{0x00, 0x00, 0x00, 0x00}, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + data := make([]byte, length) + binary.LittleEndian.PutUint64(data, hdr.LastUsableLBA) + + return data, nil + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + hdr.LastUsableLBA = binary.LittleEndian.Uint64(contents) + + return nil + }, + }, + // 16 bytes Disk GUID (also referred as UUID on UNIXes) + { + Offset: 56, + Length: 16, + // Contents: []byte{0x00}, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + return hdr.GUUID.MarshalBinary() + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + guid, err := uuid.FromBytes(contents) + if err != nil { + return fmt.Errorf("invalid GUUID: %v", err) + } + + hdr.GUUID = guid + + return nil + }, + }, + // 8 bytes Starting LBA of array of partition entries (always 2 in primary copy) + { + Offset: 72, + Length: 8, + // Contents: []byte{0x00}, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + data := make([]byte, length) + binary.LittleEndian.PutUint64(data, hdr.PartitionEntriesStartLBA) + + return data, nil + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + // TODO: Should we verify it is 2 in the case of primary? + o, ok := opts.(*Options) + if !ok { + return fmt.Errorf("option is not a GPT header option") + } + hdr.PartitionEntriesStartLBA = binary.LittleEndian.Uint64(contents) + array, err := hdr.From(o.Table, lba.Range{Start: hdr.PartitionEntriesStartLBA, End: uint64(33)}) + if err != nil { + return fmt.Errorf("failed to read starting LBA from header: %v", err) + } + + hdr.array = array + + return nil + }, + }, + // 4 bytes Number of partition entries in array + { + Offset: 80, + Length: 4, + // Contents: []byte{0x00}, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + data := make([]byte, length) + binary.LittleEndian.PutUint32(data, hdr.NumberOfPartitionEntries) + + return data, nil + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + hdr.NumberOfPartitionEntries = binary.LittleEndian.Uint32(contents) + + return nil + }, + }, + // 4 bytes Size of a single partition entry (usually 80h or 128) + { + Offset: 84, + Length: 4, + // Contents: []byte{0x00}, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + data := make([]byte, length) + binary.LittleEndian.PutUint32(data, hdr.PartitionEntrySize) + + return data, nil + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + length := binary.LittleEndian.Uint32(contents) + // This field should be set to a value of: 128 x 2n where n is an integer greater than or equal to zero. + if length%128 != 0 { + return fmt.Errorf("expected partition entry size to be a multiple of %d, got %d", 128, length) + } + + hdr.PartitionEntrySize = binary.LittleEndian.Uint32(contents) + + return nil + }, + }, + // 4 bytes CRC32/zlib of partition array in little endian + { + Offset: 88, + Length: 4, + // Contents: []byte{0x00}, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + o, ok := opts.(*Options) + if !ok { + return nil, fmt.Errorf("option is not a GPT header option") + } + expected := hdr.NumberOfPartitionEntries * hdr.PartitionEntrySize + if len(o.Array) != int(expected) { + return nil, fmt.Errorf("expected array length of %d, got %d", expected, len(o.Array)) + } + crc := crc32.ChecksumIEEE(o.Array) + data := make([]byte, length) + binary.LittleEndian.PutUint32(data, crc) + + // We should update this here and now to ensure that the field is still valid. + hdr.PartitionsArrayCRC = crc + + return data, nil + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + crc := binary.LittleEndian.Uint32(contents) + // Note that hdr.array is expected to be set in offset 72 DeserializerFunc. + checksum := crc32.ChecksumIEEE(hdr.array) + if crc != checksum { + return fmt.Errorf("expected partition checksum of %v, got %v", checksum, crc) + } + + hdr.PartitionsArrayCRC = crc + + return nil + }, + }, + // Reserved; must be zeroes for the rest of the block (420 bytes for a sector size of 512 bytes; but can be more with larger sector sizes) + { + Offset: HeaderSize, + Length: 420, + // Contents: []byte{0x00}, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + data := make([]byte, 420) + + return data, nil + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + expected := make([]byte, 420) + if !bytes.Equal(contents, expected) { + return fmt.Errorf("expected %d trailing bytes of zeroes", 420) + } + + hdr.TrailingBytes = contents + + return nil + }, + }, + // 4 bytes CRC32/zlib of header (offset +0 up to header size) in little endian, with this field zeroed during calculation + { + Offset: 16, + Length: 4, + // Contents: []byte{0x00, 0x00, 0x00, 0x00}, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + // Copy the header into a temporary slice and to avoid modifying the original. + header := make([]byte, HeaderSize) + copy(header, new) + // Zero the CRC field during the calculation. + copy(header[16:20], []byte{0x00, 0x00, 0x00, 0x00}) + + crc := crc32.ChecksumIEEE(header) + data := make([]byte, length) + binary.LittleEndian.PutUint32(data, crc) + + // We should update this here and now to ensure that the field is still valid. + hdr.CRC = crc + + return data, nil + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + crc := binary.LittleEndian.Uint32(contents) + + // Copy the header into a temporary slice and to avoid modifying the original. + header := make([]byte, HeaderSize) + copy(header, hdr.data) + // Zero the CRC field during the calculation. + copy(header[16:20], []byte{0x00, 0x00, 0x00, 0x00}) + + checksum := crc32.ChecksumIEEE(header) + if crc != checksum { + return fmt.Errorf("expected header checksum of %d, got %d", crc, checksum) + } + + hdr.CRC = crc + + return nil + }, + }, + } +} diff --git a/src/initramfs/pkg/blockdevice/table/gpt/header/header_options.go b/src/initramfs/pkg/blockdevice/table/gpt/header/header_options.go new file mode 100644 index 000000000..752866deb --- /dev/null +++ b/src/initramfs/pkg/blockdevice/table/gpt/header/header_options.go @@ -0,0 +1,49 @@ +package header + +// Options is the functional options struct. +type Options struct { + Primary bool + Table []byte + Array []byte +} + +// Option is the functional option func. +type Option func(*Options) + +// WithHeaderPrimary sets the primary option. +func WithHeaderPrimary(o bool) Option { + return func(args *Options) { + args.Primary = o + } +} + +// WithHeaderTable sets the partition type. +func WithHeaderTable(o []byte) Option { + return func(args *Options) { + args.Table = o + } +} + +// WithHeaderArrayBytes sets the partition type. +func WithHeaderArrayBytes(o []byte) Option { + return func(args *Options) { + args.Array = o + } +} + +// NewDefaultOptions initializes a Options struct with default values. +func NewDefaultOptions(setters ...interface{}) *Options { + opts := &Options{ + Primary: true, + Table: []byte{}, + Array: []byte{}, + } + + for _, setter := range setters { + if s, ok := setter.(Option); ok { + s(opts) + } + } + + return opts +} diff --git a/src/initramfs/pkg/blockdevice/table/gpt/partition/partition.go b/src/initramfs/pkg/blockdevice/table/gpt/partition/partition.go new file mode 100644 index 000000000..da749c59f --- /dev/null +++ b/src/initramfs/pkg/blockdevice/table/gpt/partition/partition.go @@ -0,0 +1,189 @@ +// Package partition provides a library for working with GPT partitions. +package partition + +import ( + "bytes" + "encoding/binary" + "fmt" + + "github.com/autonomy/dianemo/src/initramfs/pkg/blockdevice/pkg/serde" + "github.com/google/uuid" + "golang.org/x/text/encoding/unicode" +) + +// Partition represents a partition entry in a GUID partition table. +type Partition struct { + data []byte + + Type uuid.UUID // 0 + ID uuid.UUID // 16 + FirstLBA uint64 // 32 + LastLBA uint64 // 40 + Flags uint64 // 48 + Name string // 56 + TrailingBytes []byte // 128 + + Number int32 +} + +// NewPartition initializes and returns a new partition. +func NewPartition(data []byte) *Partition { + return &Partition{ + data: data, + } +} + +// Bytes returns the partition as a byte slice. +func (prt *Partition) Bytes() []byte { + return prt.data +} + +// Start returns the partition's starting LBA.. +func (prt *Partition) Start() int64 { + return int64(prt.FirstLBA) +} + +// Length returns the partition's length in LBA. +func (prt *Partition) Length() int64 { + return int64(prt.LastLBA) +} + +// No returns the partition's number. +func (prt *Partition) No() int32 { + return prt.Number +} + +// Fields implements the serder.Serde interface. +func (prt *Partition) Fields() []*serde.Field { + return []*serde.Field{ + // 16 bytes Partition type GUID + { + Offset: 0, + Length: 16, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + return prt.Type.MarshalBinary() + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + guid, err := uuid.FromBytes(contents) + if err != nil { + return fmt.Errorf("invalid GUUID: %v", err) + } + + // TODO: Provide a method for getting the human readable name of the type. + // See https://en.wikipedia.org/wiki/GUID_Partition_Table. + prt.Type = guid + + return nil + }, + }, + // 16 bytes Unique partition GUID + { + Offset: 16, + Length: 16, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + return prt.ID.MarshalBinary() + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + guid, err := uuid.FromBytes(contents) + if err != nil { + return fmt.Errorf("invalid GUUID: %v", err) + } + + prt.ID = guid + + return nil + }, + }, + // 8 bytes First LBA (little endian) + { + Offset: 32, + Length: 8, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + data := make([]byte, length) + binary.LittleEndian.PutUint64(data, prt.FirstLBA) + + return data, nil + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + prt.FirstLBA = binary.LittleEndian.Uint64(contents) + + return nil + }, + }, + // 8 bytes Last LBA (inclusive, usually odd) + { + Offset: 40, + Length: 8, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + data := make([]byte, length) + binary.LittleEndian.PutUint64(data, prt.LastLBA) + + return data, nil + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + prt.LastLBA = binary.LittleEndian.Uint64(contents) + + return nil + }, + }, + // 8 bytes Attribute flags (e.g. bit 60 denotes read-only) + { + Offset: 48, + Length: 8, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + data := make([]byte, length) + binary.LittleEndian.PutUint64(data, prt.Flags) + + return data, nil + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + prt.Flags = binary.LittleEndian.Uint64(contents) + + return nil + }, + }, + // 72 bytes Partition name (36 UTF-16LE code units) + { + Offset: 56, + Length: 72, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM) + name, err := utf16.NewEncoder().Bytes([]byte(prt.Name)) + if err != nil { + return nil, err + } + // TODO: Should we error if the name exceeds 72 bytes? + data := make([]byte, 72) + copy(data, name) + + return data, nil + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM) + decoded, err := utf16.NewDecoder().Bytes(contents) + if err != nil { + return err + } + + prt.Name = string(bytes.Trim(decoded, "\x00")) + + return nil + }, + }, + { + Offset: 72, + Length: 56, + DeserializerFunc: func(offset, length uint32, new []byte, opts interface{}) ([]byte, error) { + data := make([]byte, length) + copy(data, prt.TrailingBytes) + + return data, nil + }, + SerializerFunc: func(contents []byte, opts interface{}) error { + prt.TrailingBytes = contents + + return nil + }, + }, + } +} diff --git a/src/initramfs/pkg/blockdevice/table/gpt/partition/partition_options.go b/src/initramfs/pkg/blockdevice/table/gpt/partition/partition_options.go new file mode 100644 index 000000000..6abd82011 --- /dev/null +++ b/src/initramfs/pkg/blockdevice/table/gpt/partition/partition_options.go @@ -0,0 +1,52 @@ +package partition + +import ( + "github.com/google/uuid" +) + +// Options is the functional options struct. +type Options struct { + Type uuid.UUID + Name string +} + +// Option is the functional option func. +type Option func(*Options) + +// WithPartitionType sets the partition type. +func WithPartitionType(o [16]byte) Option { + return func(args *Options) { + // TODO: An Option should return an error. + // nolint: errcheck + guuid, _ := uuid.FromBytes(o[:]) + args.Type = guuid + } +} + +// WithPartitionName sets the partition name. +func WithPartitionName(o string) Option { + return func(args *Options) { + args.Name = o + } +} + +// NewDefaultOptions initializes a Options struct with default values. +func NewDefaultOptions(setters ...interface{}) *Options { + // Default to data type "af3dc60f-8384-7247-8e79-3d69d8477de4" + // TODO: An Option should return an error. + // nolint: errcheck + guuid, _ := uuid.FromBytes([]byte{0Xaf, 0X3d, 0Xc6, 0X0f, 0X83, 0X84, 0X72, 0X47, 0X8e, 0X79, 0X3d, 0X69, 0Xd8, 0X47, 0X7d, 0Xe4}) + + opts := &Options{ + Type: guuid, + Name: "", + } + + for _, setter := range setters { + if s, ok := setter.(Option); ok { + s(opts) + } + } + + return opts +} diff --git a/src/initramfs/pkg/blockdevice/table/table.go b/src/initramfs/pkg/blockdevice/table/table.go new file mode 100644 index 000000000..c858354ad --- /dev/null +++ b/src/initramfs/pkg/blockdevice/table/table.go @@ -0,0 +1,67 @@ +// Package table provides a library for working with block device partition tables. +package table + +import "github.com/autonomy/dianemo/src/initramfs/pkg/blockdevice/pkg/serde" + +// Table represents a partition table. +type Table = []byte + +// PartitionTable describes a partition table. +type PartitionTable interface { + // Bytes returns the partition table as a byte slice. + Bytes() Table + // Read reades the partition table. + Read() error + // Write writes the partition table/. + Write() error + // Type returns the partition table type. + Type() Type + // Header returns the partition table header. + Header() Header + // Partitions returns a slice o partition table partitions. + Partitions() []Partition + // Repair repairs a partition table. + Repair() error + // Partitioner must be implemented by a partition table. + Partitioner +} + +// Type represents a partition table type. +type Type int + +const ( + // MBR is the Master Boot Record artition table. + MBR Type = iota + // GPT is the GUID partition table. + GPT +) + +// Header describes a partition table header. +type Header interface { + // Bytes returns the partition table header as a byte slice. + Bytes() []byte + serde.Serde +} + +// Partition describes a partition. +type Partition interface { + // Bytes returns the partition table partitions as a byte slice. + Bytes() []byte + // Start returns the partition's starting LBA. + Start() int64 + // Length returns the partition's length in LBA. + Length() int64 + // No returns the partition's number. + No() int32 + serde.Serde +} + +// Partitioner describes actions that can be taken on a partition. +type Partitioner interface { + // Add adds a partition to the partition table. + Add(uint64, ...interface{}) (Partition, error) + // Resize resizes a partition table. + Resize(Partition) error + // Delete deletes a partition table. + Delete(Partition) error +} diff --git a/src/initramfs/pkg/grpc/factory/factory.go b/src/initramfs/pkg/grpc/factory/factory.go index 7c5b95243..7d754b570 100644 --- a/src/initramfs/pkg/grpc/factory/factory.go +++ b/src/initramfs/pkg/grpc/factory/factory.go @@ -2,6 +2,7 @@ package factory import ( "crypto/tls" + "fmt" "net" "strconv" @@ -17,6 +18,7 @@ type Registrator interface { // Options is the functional options struct. type Options struct { Port int + Network string Config *tls.Config ServerOptions []grpc.ServerOption } @@ -31,6 +33,13 @@ func Port(o int) Option { } } +// Network sets the network type of the listener. +func Network(o string) Option { + return func(args *Options) { + args.Network = o + } +} + // Config sets the listen port of the server. func Config(o *tls.Config) Option { return func(args *Options) { @@ -48,7 +57,8 @@ func ServerOptions(o ...grpc.ServerOption) Option { // NewDefaultOptions initializes the Options struct with default values. func NewDefaultOptions(setters ...Option) *Options { opts := &Options{ - Port: 50000, + Port: 50000, + Network: "tcp", } for _, setter := range setters { @@ -69,11 +79,19 @@ func Listen(r Registrator, setters ...Option) (err error) { server := grpc.NewServer(opts.ServerOptions...) r.Register(server) - listener, err := net.Listen("tcp", ":"+strconv.Itoa(opts.Port)) + var address string + switch opts.Network { + case "unix": + address = "/run/factory/factory.sock" + case "tcp": + address = ":" + strconv.Itoa(opts.Port) + default: + return fmt.Errorf("unknown network: %s", opts.Network) + } + listener, err := net.Listen(opts.Network, address) if err != nil { return } - err = server.Serve(listener) if err != nil { return diff --git a/src/initramfs/cmd/osd/pkg/gen/gen.go b/src/initramfs/pkg/grpc/gen/gen.go similarity index 78% rename from src/initramfs/cmd/osd/pkg/gen/gen.go rename to src/initramfs/pkg/grpc/gen/gen.go index 439204a1b..3547ecd7b 100644 --- a/src/initramfs/cmd/osd/pkg/gen/gen.go +++ b/src/initramfs/pkg/grpc/gen/gen.go @@ -11,6 +11,7 @@ import ( "github.com/autonomy/dianemo/src/initramfs/cmd/trustd/proto" "github.com/autonomy/dianemo/src/initramfs/pkg/crypto/x509" + "github.com/autonomy/dianemo/src/initramfs/pkg/grpc/middleware/auth/basic" "github.com/autonomy/dianemo/src/initramfs/pkg/net" "github.com/autonomy/dianemo/src/initramfs/pkg/userdata" "google.golang.org/grpc" @@ -22,12 +23,29 @@ type Generator struct { } // NewGenerator initializes a Generator with a preconfigured grpc.ClientConn. -func NewGenerator(conn *grpc.ClientConn) (g *Generator) { +func NewGenerator(data *userdata.UserData, port int) (g *Generator, err error) { + if len(data.Services.Trustd.Endpoints) == 0 { + return nil, fmt.Errorf("at least one root of trust endpoint is required") + } + + creds := basic.NewCredentials( + data.Security.OS.CA.Crt, + data.Services.Trustd.Username, + data.Services.Trustd.Password, + ) + + // TODO: In the case of failure, attempt to generate the identity from + // another RoT. + var conn *grpc.ClientConn + conn, err = basic.NewConnection(data.Services.Trustd.Endpoints[0], port, creds) + if err != nil { + return nil, err + } client := proto.NewTrustdClient(conn) return &Generator{ client: client, - } + }, nil } // Certificate implements the proto.TrustdClient interface. diff --git a/src/initramfs/pkg/userdata/userdata.go b/src/initramfs/pkg/userdata/userdata.go index 9bde8c039..eee247e2c 100644 --- a/src/initramfs/pkg/userdata/userdata.go +++ b/src/initramfs/pkg/userdata/userdata.go @@ -51,6 +51,7 @@ type Services struct { Kubeadm *Kubeadm `yaml:"kubeadm"` Trustd *Trustd `yaml:"trustd"` Proxyd *Proxyd `yaml:"proxyd"` + Blockd *Blockd `yaml:"blockd"` OSD *OSD `yaml:"osd"` CRT *CRT `yaml:"crt"` } @@ -106,6 +107,11 @@ type Proxyd struct { Image string `yaml:"image,omitempty"` } +// Blockd describes the configuration of the blockd service. +type Blockd struct { + Image string `yaml:"image,omitempty"` +} + // CRT describes the configuration of the container runtime service. type CRT struct { Image string `yaml:"image,omitempty"` diff --git a/src/tools/.conform.yaml b/src/tools/.conform.yaml index cbddfdaf0..bba525b07 100644 --- a/src/tools/.conform.yaml +++ b/src/tools/.conform.yaml @@ -522,7 +522,7 @@ tasks: template: | WORKDIR $SRC/{{ .Docker.CurrentStage }} WORKDIR /tools/usr/local - RUN curl -L {{ index .Variables "srcGo" }} | tar -C /tools/usr/local -xz + RUN curl -L {{ index .Variables "srcGo" }} | tar -xz gperf: template: | WORKDIR $SRC/{{ .Docker.CurrentStage }} @@ -840,6 +840,7 @@ tasks: COPY src/fsh.sh /tools/bin RUN chmod +x /tools/bin/fsh.sh RUN fsh.sh {{ index .Variables "rootfs" }} + # WORKDIR $PREFIX/$SRC/{{ .Docker.CurrentStage }} WORKDIR /tools/usr/local/src/{{ .Docker.CurrentStage }} RUN curl -L {{index .Variables "srcGlibc" }} | tar --strip-components=1 -xJ RUN ln -sfv /tools/lib/gcc /usr/lib @@ -894,6 +895,7 @@ tasks: --without-ncurses RUN make -j $(($(nproc) / 2)) RUN make install + RUN cp -R ../include $PREFIX/include/util-linux xml-parser: template: | WORKDIR $SRC/{{ .Docker.CurrentStage }}