commit c2f6e4ae5d6d53199950f1e9fc7a445972a77006 Author: Adphi Date: Tue Apr 19 14:01:08 2022 +0200 build and convert implementations centos: WIP Signed-off-by: Adphi diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..430cb83 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.idea +tests +disk* +qemu.sh +*.qcow2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6ee9b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +tests +disk*.qcow2 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2511e71 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM golang as builder + +WORKDIR /d2vm + +COPY go.mod go.mod +COPY go.sum go.sum + +RUN go mod download + +COPY . . + +RUN go build -o d2vm ./cmd/d2vm + +FROM ubuntu + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \ + linux-image-virtual \ + util-linux \ + kpartx \ + e2fsprogs \ + xfsprogs \ + mount \ + tar \ + extlinux \ + uuid-runtime \ + qemu-utils + +COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/ + +COPY --from=builder /d2vm/d2vm /usr/local/bin/ diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..d2948c7 --- /dev/null +++ b/LICENCE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 The Linka Cloud Team + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..28ece84 --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +# Copyright 2021 Linka Cloud All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +MODULE = go.linka.cloud/d2vm + +REPOSITORY = linkacloud + +VERSION_SUFFIX = $(shell git diff --quiet || echo "-dev") +VERSION = $(shell git describe --tags --exact-match 2> /dev/null || echo "`git describe --tags $$(git rev-list --tags --max-count=1) 2> /dev/null || echo v0.0.0`-`git rev-parse --short HEAD`")$(VERSION_SUFFIX) +show-version: + @echo $(VERSION) + +DOCKER_IMAGE := linkacloud/d2vm + +docker-push: + @docker image push -a $(DOCKER_IMAGE) + +docker-build: + @docker image build -t $(DOCKER_IMAGE):$(VERSION) -t $(DOCKER_IMAGE):latest . + +docker-run: + @docker run --rm -i -t \ + --privileged \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v $(PWD):/build \ + -w /build \ + $(DOCKER_IMAGE) bash diff --git a/README.md b/README.md new file mode 100644 index 0000000..605effb --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# d2vm + +*Build virtual machine image from Docker images* + +**Status**: *alpha* + +## Supported Environments: + +**Only Linux is supported.** + +If you want to run it on OSX or Windows (untested) you can use Docker for it: + +```bash +docker run --rm -i -t \ + --privileged \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v $(pwd):/build \ + -w /build \ + linkacloud/d2vm bash +``` diff --git a/builder.go b/builder.go new file mode 100644 index 0000000..0858341 --- /dev/null +++ b/builder.go @@ -0,0 +1,320 @@ +// Copyright 2022 Linka Cloud All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package docker2vm + +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/sirupsen/logrus" + "go.uber.org/multierr" + + "go.linka.cloud/d2vm/pkg/exec" +) + +const ( + hosts = `127.0.0.1 localhost + +# The following lines are desirable for IPv6 capable hosts +::1 ip6-localhost ip6-loopback +fe00::0 ip6-localnet +ff00::0 ip6-mcastprefix +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters +ff02::3 ip6-allhosts +` + syslinuxCfgUbuntu = `DEFAULT linux + SAY Now booting the kernel from SYSLINUX... + LABEL linux + KERNEL /boot/vmlinuz + APPEND ro root=UUID=%s initrd=/boot/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8 +` + syslinuxCfgDebian = `DEFAULT linux + SAY Now booting the kernel from SYSLINUX... + LABEL linux + KERNEL /vmlinuz + APPEND ro root=UUID=%s initrd=/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8 +` + syslinuxCfgAlpine = `DEFAULT linux + SAY Now booting the kernel from SYSLINUX... + LABEL linux + KERNEL /boot/vmlinuz-virt + APPEND ro root=UUID=%s rootfstype=ext4 initrd=/boot/initramfs-virt console=ttyS0,115200 +` + syslinuxCfgCentOS = `DEFAULT linux + SAY Now booting the kernel from SYSLINUX... + LABEL linux + KERNEL /boot/vmlinuz + APPEND ro root=UUID=%s initrd=/boot/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8 +` +) + +var ( + fdiskCmds = []string{"n", "p", "1", "", "", "a", "w"} +) + +type builder struct { + osRelease OSRelease + + src string + diskRaw string + diskQcow2 string + size int64 + mntPoint string + + loDevice string + loPart string + diskUUD string +} + +func NewBuilder(workdir, src, disk string, size int64, osRelease OSRelease) (*builder, error) { + if size == 0 { + size = 1 + } + if disk == "" { + disk = "disk0" + } + b := &builder{ + osRelease: osRelease, + src: src, + diskRaw: filepath.Join(workdir, disk+".raw"), + diskQcow2: filepath.Join(workdir, disk+".qcow2"), + size: size, + mntPoint: filepath.Join(workdir, "/mnt"), + } + if err := os.MkdirAll(b.mntPoint, os.ModePerm); err != nil { + return nil, err + } + return b, nil +} + +func (b *builder) Build(ctx context.Context) (err error) { + defer func() { + if err == nil { + return + } + logrus.WithError(err).Error("Build failed") + if err := b.unmountImg(context.Background()); err != nil { + logrus.WithError(err).Error("failed to unmount") + } + if err := b.cleanUp(context.Background()); err != nil { + logrus.WithError(err).Error("failed to cleanup") + } + }() + if err = b.cleanUp(ctx); err != nil { + return err + } + if err = b.makeImg(ctx); err != nil { + return err + } + if err = b.mountImg(ctx); err != nil { + return err + } + if err = b.copyRootFS(ctx); err != nil { + return err + } + if err = b.setupRootFS(ctx); err != nil { + return err + } + if err = b.installKernel(ctx); err != nil { + return err + } + if err = b.unmountImg(ctx); err != nil { + return err + } + if err = b.setupMBR(ctx); err != nil { + return err + } + if err = b.convert2Qcow2(ctx); err != nil { + return err + } + if err = b.cleanUp(ctx); err != nil { + return err + } + return nil +} + +func (b *builder) cleanUp(ctx context.Context) error { + return os.RemoveAll(b.diskRaw) +} + +func (b *builder) makeImg(ctx context.Context) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + logrus.Infof("creating raw image") + if err := block(b.diskRaw, b.size); err != nil { + return err + } + c := exec.CommandContext(ctx, "fdisk", b.diskRaw) + var i bytes.Buffer + for _, v := range fdiskCmds { + if _, err := i.Write([]byte(v + "\n")); err != nil { + return err + } + } + var e bytes.Buffer + c.Stdin = &i + c.Stderr = &e + if err := c.Run(); err != nil { + return fmt.Errorf("%w: %s", err, e.String()) + } + return nil +} + +func (b *builder) mountImg(ctx context.Context) error { + logrus.Infof("mounting raw image") + o, _, err := exec.RunOut(ctx, "losetup", "--show", "-f", b.diskRaw) + if err != nil { + return err + } + b.loDevice = strings.TrimSuffix(o, "\n") + if err := exec.Run(ctx, "kpartx", "-a", b.loDevice); err != nil { + return err + } + b.loPart = fmt.Sprintf("/dev/mapper/%sp1", filepath.Base(b.loDevice)) + logrus.Infof("creating raw image file system") + if err := exec.Run(ctx, "mkfs.ext4", b.loPart); err != nil { + return err + } + if err := exec.Run(ctx, "mount", b.loPart, b.mntPoint); err != nil { + return err + } + return nil +} + +func (b *builder) unmountImg(ctx context.Context) error { + logrus.Infof("unmounting raw image") + var merr error + if err := exec.Run(ctx, "umount", b.mntPoint); err != nil { + merr = multierr.Append(merr, err) + } + if err := exec.Run(ctx, "kpartx", "-d", b.loDevice); err != nil { + merr = multierr.Append(merr, err) + } + if err := exec.Run(ctx, "losetup", "-d", b.loDevice); err != nil { + merr = multierr.Append(merr, err) + } + return merr +} + +func (b *builder) copyRootFS(ctx context.Context) error { + logrus.Infof("copying rootfs to raw image") + if err := exec.Run(ctx, "tar", "-xvf", b.src, "-C", b.mntPoint); err != nil { + return err + } + return nil +} + +func (b *builder) setupRootFS(ctx context.Context) error { + logrus.Infof("setting up rootfs") + o, _, err := exec.RunOut(ctx, "blkid", "-s", "UUID", "-o", "value", b.loPart) + if err != nil { + return err + } + b.diskUUD = strings.TrimSuffix(o, "\n") + fstab := fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\n", b.diskUUD) + if err := b.chWriteFile("/etc/fstab", fstab, 0644); err != nil { + return err + } + if err := b.chWriteFile("/etc/resolv.conf", "nameserver 8.8.8.8", 0644); err != nil { + return err + } + if err := b.chWriteFile("/etc/hostname", "localhost", 0644); err != nil { + return err + } + if err := b.chWriteFile("/etc/hosts", hosts, 0644); err != nil { + return err + } + if err := os.RemoveAll("/ur/sbin/policy-rc.d"); err != nil { + return err + } + if err := os.RemoveAll(b.chPath("/.dockerenv")); err != nil { + return err + } + if b.osRelease.ID != ReleaseAlpine { + return nil + } + by, err := os.ReadFile(b.chPath("/etc/inittab")) + if err != nil { + return err + } + by = append(by, []byte("\n"+"ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100\n")...) + if err := b.chWriteFile("/etc/inittab", string(by), 0644); err != nil { + return err + } + if err := b.chWriteFile("/etc/network/interfaces", "", 0644); err != nil { + return err + } + return nil +} + +func (b *builder) installKernel(ctx context.Context) error { + logrus.Infof("installing linux kernel") + if err := exec.Run(ctx, "extlinux", "--install", b.chPath("/boot")); err != nil { + return err + } + var sysconfig string + switch b.osRelease.ID { + case ReleaseUbuntu: + sysconfig = syslinuxCfgUbuntu + case ReleaseDebian: + sysconfig = syslinuxCfgDebian + case ReleaseAlpine: + sysconfig = syslinuxCfgAlpine + case ReleaseCentOS: + sysconfig = syslinuxCfgCentOS + default: + return fmt.Errorf("%s: distribution not supported", b.osRelease.ID) + } + if err := b.chWriteFile("/boot/syslinux.cfg", fmt.Sprintf(sysconfig, b.diskUUD), 0644); err != nil { + return err + } + return nil +} + +func (b *builder) setupMBR(ctx context.Context) error { + logrus.Infof("writing MBR") + if err := exec.Run(ctx, "dd", "if=/usr/lib/EXTLINUX/mbr.bin", fmt.Sprintf("of=%s", b.diskRaw), "bs=440", "count=1", "conv=notrunc"); err != nil { + return err + } + return nil +} + +func (b *builder) convert2Qcow2(ctx context.Context) error { + logrus.Infof("converting to QCOW2") + return exec.Run(ctx, "qemu-img", "convert", b.diskRaw, "-O", "qcow2", b.diskQcow2) +} + +func (b *builder) chWriteFile(path string, content string, perm os.FileMode) error { + return os.WriteFile(b.chPath(path), []byte(content), perm) +} + +func (b *builder) chPath(path string) string { + return fmt.Sprintf("%s%s", b.mntPoint, path) +} + +func block(path string, size int64) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + return f.Truncate(size) +} diff --git a/cmd/d2vm/build.go b/cmd/d2vm/build.go new file mode 100644 index 0000000..77d6374 --- /dev/null +++ b/cmd/d2vm/build.go @@ -0,0 +1,62 @@ +// Copyright 2022 Linka Cloud All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "github.com/google/uuid" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "go.linka.cloud/d2vm" + "go.linka.cloud/d2vm/pkg/docker" + "go.linka.cloud/d2vm/pkg/exec" +) + +var ( + file = "Dockerfile" + tag = uuid.New().String() + buildCmd = &cobra.Command{ + Use: "build [context directory]", + Short: "Build qcow2 vm image from Dockerfile", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + size, err := parseSize(size) + if err != nil { + return err + } + if debug { + exec.Run = exec.RunStdout + } + logrus.Infof("building docker image from %s", file) + if err := docker.Cmd(cmd.Context(), "build", "-t", tag, "-f", file, args[0]); err != nil { + return err + } + return docker2vm.Convert(cmd.Context(), tag, size, password, output) + }, + } +) + +func init() { + rootCmd.AddCommand(buildCmd) + + buildCmd.Flags().StringVarP(&file, "file", "f", "Dockerfile", "Name of the Dockerfile (Default is 'PATH/Dockerfile')") + buildCmd.Flags().StringVarP(&tag, "tag", "t", tag, "Name and optionally a tag in the 'name:tag' format") + + buildCmd.Flags().StringVarP(&output, "output", "o", output, "The output qcow2 image") + buildCmd.Flags().StringVarP(&password, "password", "p", "root", "Root user password") + buildCmd.Flags().StringVarP(&size, "size", "s", "1G", "The output image size") + buildCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable Debug output") + buildCmd.Flags().BoolVar(&force, "force", false, "Override output qcow2 image") +} diff --git a/cmd/d2vm/convert.go b/cmd/d2vm/convert.go new file mode 100644 index 0000000..186bb05 --- /dev/null +++ b/cmd/d2vm/convert.go @@ -0,0 +1,79 @@ +// Copyright 2022 Linka Cloud All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "os" + + "github.com/c2h5oh/datasize" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "go.linka.cloud/d2vm" + "go.linka.cloud/d2vm/pkg/docker" + "go.linka.cloud/d2vm/pkg/exec" +) + +var ( + convertCmd = &cobra.Command{ + Use: "convert [docker image]", + Short: "Convert Docker image to qcow2 vm image", + Args: cobra.ExactArgs(1), + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + img := args[0] + size, err := parseSize(size) + if err != nil { + return err + } + if _, err := os.Stat(output); err == nil || !os.IsNotExist(err) { + if !force { + return fmt.Errorf("%s already exists", output) + } + } + if debug { + exec.Run = exec.RunStdout + } + if _, err := os.Stat(output); err == nil || !os.IsNotExist(err) { + if !force { + return fmt.Errorf("%s already exists", output) + } + } + logrus.Infof("pulling image %s", img) + if err := docker.Cmd(cmd.Context(), "image", "pull", img); err != nil { + return err + } + return docker2vm.Convert(cmd.Context(), img, size, password, output) + }, + } +) + +func parseSize(s string) (int64, error) { + var v datasize.ByteSize + if err := v.UnmarshalText([]byte(s)); err != nil { + return 0, err + } + return int64(v), nil +} + +func init() { + convertCmd.Flags().StringVarP(&output, "output", "o", output, "The output qcow2 image") + convertCmd.Flags().StringVarP(&password, "password", "p", "root", "The Root user password") + convertCmd.Flags().StringVarP(&size, "size", "s", "1G", "The output image size") + convertCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable Debug output") + convertCmd.Flags().BoolVarP(&force, "force", "f", false, "Override output qcow2 image") + rootCmd.AddCommand(convertCmd) +} diff --git a/cmd/d2vm/main.go b/cmd/d2vm/main.go new file mode 100644 index 0000000..411ea1d --- /dev/null +++ b/cmd/d2vm/main.go @@ -0,0 +1,40 @@ +// Copyright 2022 Linka Cloud All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + + "github.com/spf13/cobra" +) + +var ( + output = "disk0.qcow2" + size = "1G" + password = "root" + force = false + debug = false + + rootCmd = &cobra.Command{ + Use: "d2vm", + } +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + rootCmd.ExecuteContext(ctx) +} diff --git a/convert.go b/convert.go new file mode 100644 index 0000000..22d2438 --- /dev/null +++ b/convert.go @@ -0,0 +1,115 @@ +// Copyright 2022 Linka Cloud All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package docker2vm + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" + + "go.linka.cloud/d2vm/pkg/docker" +) + +func Convert(ctx context.Context, img string, size int64, password string, output string) error { + imgUUID := uuid.New().String() + tmpPath := filepath.Join(os.TempDir(), "d2vm", imgUUID) + if err := os.MkdirAll(tmpPath, os.ModePerm); err != nil { + return err + } + defer os.RemoveAll(tmpPath) + + logrus.Infof("inspecting image %s", img) + r, err := FetchDockerImageOSRelease(ctx, img, tmpPath) + if err != nil { + return err + } + d, err := NewDockerfile(r, img, password) + if err != nil { + return err + } + logrus.Infof("docker image based on %s", d.Release.Name) + p := filepath.Join(tmpPath, docker.FormatImgName(img)) + dir := filepath.Dir(p) + f, err := os.Create(p) + if err != nil { + return err + } + defer f.Close() + if err := d.Render(f); err != nil { + return err + } + logrus.Infof("building kernel enabled image") + if err := docker.Cmd(ctx, "image", "build", "-t", imgUUID, "-f", p, dir); err != nil { + return err + } + defer docker.Cmd(ctx, "image", "rm", imgUUID) + archive := imgUUID + ".tar" + archivePath := filepath.Join(tmpPath, archive) + logrus.Infof("creating root file system archive") + if err := docker.Cmd(ctx, "run", "-d", "--name", imgUUID, imgUUID); err != nil { + return err + } + if err := docker.Cmd(ctx, "export", "--output", archivePath, imgUUID); err != nil { + return err + } + if err := docker.Cmd(ctx, "rm", "-f", imgUUID); err != nil { + return err + } + logrus.Infof("creating vm image") + + b, err := NewBuilder(tmpPath, archivePath, "", size, r) + if err != nil { + return err + } + if err := b.Build(ctx); err != nil { + return err + } + if err := os.RemoveAll(output); err != nil { + return err + } + if err := MoveFile(filepath.Join(tmpPath, "disk0.qcow2"), output); err != nil { + return err + } + return nil +} + +func MoveFile(sourcePath, destPath string) error { + inputFile, err := os.Open(sourcePath) + if err != nil { + return fmt.Errorf("failed to open source file: %s", err) + } + outputFile, err := os.Create(destPath) + if err != nil { + inputFile.Close() + return fmt.Errorf("failed to open dest file: %s", err) + } + defer outputFile.Close() + _, err = io.Copy(outputFile, inputFile) + inputFile.Close() + if err != nil { + return fmt.Errorf("failed to write to output file: %s", err) + } + // The copy was successful, so now delete the original file + err = os.Remove(sourcePath) + if err != nil { + return fmt.Errorf("failed to remove original file: %s", err) + } + return nil +} diff --git a/docker_image.go b/docker_image.go new file mode 100644 index 0000000..0e7264c --- /dev/null +++ b/docker_image.go @@ -0,0 +1,72 @@ +// Copyright 2022 Linka Cloud All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package docker2vm + +import ( + "io" + "text/template" +) + +const ( + dockerImageRun = ` +#!/bin/sh + +{{- range .Config.Env }} +export {{ . }} +{{- end }} + +cd {{- if .Config.WorkingDir }}{{ .Config.WorkingDir }}{{- else }}/{{- end }} + +{{ .Config.Entrypoint }} {{ .Config.Args }} +` +) + +var ( + dockerImageRunTemplate = template.Must(template.New("docker-run.sh").Parse(dockerImageRun)) +) + +type DockerImage struct { + Config struct { + Hostname string `json:"Hostname"` + Domainname string `json:"Domainname"` + User string `json:"User"` + AttachStdin bool `json:"AttachStdin"` + AttachStdout bool `json:"AttachStdout"` + AttachStderr bool `json:"AttachStderr"` + ExposedPorts struct { + Tcp struct { + } `json:"3000/tcp"` + } `json:"ExposedPorts"` + Tty bool `json:"Tty"` + OpenStdin bool `json:"OpenStdin"` + StdinOnce bool `json:"StdinOnce"` + Env []string `json:"Env"` + Cmd []string `json:"Cmd"` + Image string `json:"Image"` + Volumes interface{} `json:"Volumes"` + WorkingDir string `json:"WorkingDir"` + Entrypoint []string `json:"Entrypoint"` + OnBuild interface{} `json:"OnBuild"` + Labels interface{} `json:"Labels"` + } `json:"Config"` + Architecture string `json:"Architecture"` + Os string `json:"Os"` + Size int `json:"Size"` + VirtualSize int `json:"VirtualSize"` +} + +func (i DockerImage) AsRunScript(w io.Writer) error { + return dockerImageRunTemplate.Execute(w, i) +} diff --git a/dockerfile.go b/dockerfile.go new file mode 100644 index 0000000..a322400 --- /dev/null +++ b/dockerfile.go @@ -0,0 +1,72 @@ +// Copyright 2022 Linka Cloud All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package docker2vm + +import ( + _ "embed" + "fmt" + "io" + "text/template" +) + +//go:embed templates/ubuntu.Dockerfile +var ubuntuDockerfile string + +//go:embed templates/debian.Dockerfile +var debianDockerfile string + +//go:embed templates/alpine.Dockerfile +var alpineDockerfile string + +//go:embed templates/centos.Dockerfile +var centOSDockerfile string + +var ( + ubuntuDockerfileTemplate = template.Must(template.New("ubuntu.Dockerfile").Parse(ubuntuDockerfile)) + debianDockerfileTemplate = template.Must(template.New("debian.Dockerfile").Parse(debianDockerfile)) + alpineDockerfileTemplate = template.Must(template.New("alpine.Dockerfile").Parse(alpineDockerfile)) + centOSDockerfileTemplate = template.Must(template.New("centos.Dockerfile").Parse(centOSDockerfile)) +) + +type Dockerfile struct { + Image string + Password string + Release OSRelease + tmpl *template.Template +} + +func (d Dockerfile) Render(w io.Writer) error { + return d.tmpl.Execute(w, d) +} + +func NewDockerfile(release OSRelease, img, password string) (Dockerfile, error) { + if password == "" { + password = "root" + } + d := Dockerfile{Release: release, Image: img, Password: password} + switch release.ID { + case ReleaseDebian: + d.tmpl = debianDockerfileTemplate + case ReleaseUbuntu: + d.tmpl = ubuntuDockerfileTemplate + case ReleaseAlpine: + d.tmpl = alpineDockerfileTemplate + case ReleaseCentOS: + d.tmpl = centOSDockerfileTemplate + default: + return Dockerfile{}, fmt.Errorf("unsupported distribution: %s", release.ID) + } + return d, nil +} diff --git a/examples/alpine.Dockerfile b/examples/alpine.Dockerfile new file mode 100644 index 0000000..5d22a7a --- /dev/null +++ b/examples/alpine.Dockerfile @@ -0,0 +1,4 @@ +FROM alpine + +RUN apk add --no-cache && \ + echo "PermitRootLogin yes" >> /etc/ssh/sshd_config diff --git a/examples/debian.Dockerfile b/examples/debian.Dockerfile new file mode 100644 index 0000000..4c1f475 --- /dev/null +++ b/examples/debian.Dockerfile @@ -0,0 +1,4 @@ +FROM debian + +RUN apt update && apt install -y openssh-server && \ + echo "PermitRootLogin yes" >> /etc/ssh/sshd_config diff --git a/examples/ubuntu.Dockerfile b/examples/ubuntu.Dockerfile new file mode 100644 index 0000000..cc7840d --- /dev/null +++ b/examples/ubuntu.Dockerfile @@ -0,0 +1,4 @@ +FROM ubuntu + +RUN apt update && apt install -y openssh-server && \ + echo "PermitRootLogin yes" >> /etc/ssh/sshd_config \ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..187eebf --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module go.linka.cloud/d2vm + +go 1.17 + +require ( + github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2 + github.com/google/uuid v1.3.0 + github.com/joho/godotenv v1.4.0 + github.com/sirupsen/logrus v1.8.1 + github.com/spf13/cobra v1.4.0 + go.uber.org/multierr v1.8.0 +) + +require ( + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.uber.org/atomic v1.7.0 // indirect + golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6c56d88 --- /dev/null +++ b/go.sum @@ -0,0 +1,37 @@ +github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2 h1:t8KYCwSKsOEZBFELI4Pn/phbp38iJ1RRAkDFNin1aak= +github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/os_release.go b/os_release.go new file mode 100644 index 0000000..5e8103f --- /dev/null +++ b/os_release.go @@ -0,0 +1,116 @@ +// Copyright 2022 Linka Cloud All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package docker2vm + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "text/template" + + "github.com/joho/godotenv" + "github.com/sirupsen/logrus" + + "go.linka.cloud/d2vm/pkg/docker" +) + +const ( + ReleaseUbuntu Release = "ubuntu" + ReleaseDebian Release = "debian" + ReleaseAlpine Release = "alpine" + ReleaseCentOS Release = "centos" +) + +type Release string + +func (r Release) Supported() bool { + switch r { + case ReleaseUbuntu: + return true + case ReleaseDebian: + return true + case ReleaseAlpine: + return true + case ReleaseCentOS: + return true + default: + return false + } +} + +type OSRelease struct { + ID Release + Name string + VersionID string + Version string + VersionCodeName string +} + +func ParseOSRelease(s string) (OSRelease, error) { + env, err := godotenv.Parse(strings.NewReader(s)) + if err != nil { + return OSRelease{}, err + } + o := OSRelease{ + ID: Release(strings.ToLower(env["ID"])), + Name: env["NAME"], + Version: env["VERSION"], + VersionID: env["VERSION_ID"], + VersionCodeName: env["VERSION_CODENAME"], + } + return o, nil +} + +const ( + osReleaseDockerfile = ` +FROM {{ . }} + +ENTRYPOINT [""] + +CMD ["/bin/cat", "/etc/os-release"] +` +) + +var ( + osReleaseDockerfileTemplate = template.Must(template.New("osrelease.Dockerfile").Parse(osReleaseDockerfile)) +) + +func FetchDockerImageOSRelease(ctx context.Context, img string, tmpPath string) (OSRelease, error) { + d := filepath.Join(tmpPath, "osrelase.Dockerfile") + f, err := os.Create(d) + if err != nil { + return OSRelease{}, err + } + defer f.Close() + if err := osReleaseDockerfileTemplate.Execute(f, img); err != nil { + return OSRelease{}, err + } + imgTag := fmt.Sprintf("os-release-%s", img) + if err := docker.Cmd(ctx, "image", "build", "-t", imgTag, "-f", d, tmpPath); err != nil { + return OSRelease{}, err + } + defer func() { + if err := docker.Cmd(ctx, "image", "rm", imgTag); err != nil { + logrus.WithError(err).Error("failed to cleanup OSRelease Docker Image") + } + }() + o, _, err := docker.CmdOut(ctx, "run", "--rm", "-i", imgTag) + if err != nil { + return OSRelease{}, err + } + return ParseOSRelease(o) +} diff --git a/pkg/docker/docker.go b/pkg/docker/docker.go new file mode 100644 index 0000000..1ea32ee --- /dev/null +++ b/pkg/docker/docker.go @@ -0,0 +1,37 @@ +// Copyright 2022 Linka Cloud All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package docker + +import ( + "context" + _ "embed" + "strings" + + "go.linka.cloud/d2vm/pkg/exec" +) + +func FormatImgName(name string) string { + s := strings.Replace(name, ":", "-", -1) + s = strings.Replace(s, "/", "_", -1) + return s +} + +func Cmd(ctx context.Context, args ...string) error { + return exec.Run(ctx, "docker", args...) +} + +func CmdOut(ctx context.Context, args ...string) (string, string, error) { + return exec.RunOut(ctx, "docker", args...) +} diff --git a/pkg/exec/exec.go b/pkg/exec/exec.go new file mode 100644 index 0000000..5231cb9 --- /dev/null +++ b/pkg/exec/exec.go @@ -0,0 +1,57 @@ +// Copyright 2022 Linka Cloud All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package exec + +import ( + "bytes" + "context" + "fmt" + "os" + "os/exec" + "strings" +) + +var ( + Run = RunNoOut + + CommandContext = exec.CommandContext +) + +func RunStdout(ctx context.Context, c string, args ...string) error { + cmd := exec.CommandContext(ctx, c, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func RunNoOut(ctx context.Context, c string, args ...string) error { + _, _, err := RunOut(ctx, c, args...) + if err != nil { + return err + } + return nil +} + +func RunOut(ctx context.Context, c string, args ...string) (stdout, stderr string, err error) { + cmd := exec.CommandContext(ctx, c, args...) + var stdo, stde bytes.Buffer + cmd.Stdout = &stdo + cmd.Stderr = &stde + err = cmd.Run() + if err != nil { + return "", "", fmt.Errorf("%s %s: stdout: %s stderr: %s error: %w", c, strings.Join(args, " "), stdo.String(), stde.String(), err) + } + return stdo.String(), stde.String(), nil +} diff --git a/qemu.sh b/qemu.sh new file mode 100755 index 0000000..767430e --- /dev/null +++ b/qemu.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +# Copyright 2021 Linka Cloud All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +IMG=${1:-disk0.qcow2} + +ARGS="" + +if [[ $OSTYPE == 'darwin'* ]]; then + ARGS="-M accel=hvf" +else + ARGS="-use-kvm" +fi + +qemu-system-x86_64 -drive file=$IMG,index=0,media=disk,format=qcow2 -m 4096 -cpu host -nographic ${ARGS} ${@: 2} diff --git a/templates/alpine.Dockerfile b/templates/alpine.Dockerfile new file mode 100644 index 0000000..5d035e1 --- /dev/null +++ b/templates/alpine.Dockerfile @@ -0,0 +1,23 @@ +FROM {{ .Image }} + +USER root + +RUN apk update --no-cache && \ + apk add \ + util-linux \ + linux-virt \ + busybox-initscripts \ + openrc + +#RUN apk update --no-cache && \ +# apk add \ +# linux-virt \ +# alpine-base \ +# openssh-server + +RUN for s in bootmisc hostname hwclock modules networking swap sysctl urandom syslog; do rc-update add $s boot; done +RUN for s in devfs dmesg hwdrivers mdev; do rc-update add $s sysinit; done + + +RUN echo "root:{{- if .Password}}{{ .Password}}{{- else}}root{{- end}}" | chpasswd + diff --git a/templates/centos.Dockerfile b/templates/centos.Dockerfile new file mode 100644 index 0000000..9305c39 --- /dev/null +++ b/templates/centos.Dockerfile @@ -0,0 +1,18 @@ +FROM {{ .Image }} + +USER root + +RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* && \ + sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-* + +RUN yum update -y && \ + yum install -y kernel systemd + +RUN systemctl preset-all && \ + systemctl enable getty@ttyS0 + +RUN cd /boot && \ + ln -s $(find . -name 'vmlinuz-*') vmlinuz && \ + ln -s $(find . -name 'initramfs-*.img') initrd.img + +RUN echo "root:{{- if .Password}}{{ .Password}}{{- else}}root{{- end}}" | chpasswd diff --git a/templates/debian.Dockerfile b/templates/debian.Dockerfile new file mode 100644 index 0000000..e241fd3 --- /dev/null +++ b/templates/debian.Dockerfile @@ -0,0 +1,19 @@ +FROM {{ .Image }} + +USER root + +RUN apt-get -y update && \ + DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \ + linux-image-amd64 + +RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + systemd-sysv \ + systemd \ + dbus \ + iproute2 \ + udhcpc \ + iputils-ping + +RUN systemctl preset-all + +RUN echo "root:{{- if .Password}}{{ .Password}}{{- else}}root{{- end}}" | chpasswd diff --git a/templates/ubuntu.Dockerfile b/templates/ubuntu.Dockerfile new file mode 100644 index 0000000..7743537 --- /dev/null +++ b/templates/ubuntu.Dockerfile @@ -0,0 +1,20 @@ +FROM {{ .Image }} + +USER root + +RUN apt-get update -y && \ + apt-get -y install \ + linux-image-virtual + + +RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + systemd-sysv \ + systemd \ + dbus \ + udhcpc \ + iproute2 \ + iputils-ping + +RUN systemctl preset-all + +RUN echo "root:{{- if .Password}}{{ .Password}}{{- else}}root{{- end}}" | chpasswd diff --git a/templates/vmbuilder.sh b/templates/vmbuilder.sh new file mode 100755 index 0000000..f161a4a --- /dev/null +++ b/templates/vmbuilder.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash + +OUT="/dev/null" + +if [[ -n "$DEBUG" ]]; then + set -x + OUT="/dev/stderr" +fi +set -e + +SRC=${1:-rootfs.tar} +DISK_NAME=${2:-disk0} +SIZE=${3:-10G} + +BLOCK="$DISK_NAME.raw" +QCOW2="$DISK_NAME.qcow2" +MOUNTPOINT=/mnt + +cleanup() { + rm -rf $BLOCK +} + +make_img() { + echo "Creating raw image of size $SIZE" + fallocate -l $SIZE $BLOCK &> $OUT + ( + echo n # Add a new partition + echo p # Primary partition + echo 1 # Partition number + echo # First sector (Accept default: 1) + echo # Last sector (Accept default: varies) + echo a + echo w # Write changes + ) | fdisk $BLOCK &> $OUT +} + +mount_img() { + echo "Mounting image" + DEVICE_ROOT=$(losetup --show -f $BLOCK) + kpartx -v -a $DEVICE_ROOT &> $OUT + DEVICE=/dev/mapper/"$(basename ${DEVICE_ROOT})p1" + mkfs.ext4 $DEVICE &> $OUT + mount $DEVICE $MOUNTPOINT +} + +unmount_img() { + echo "Unmounting image" + umount $MOUNTPOINT/ + kpartx -d $DEVICE_ROOT &> $OUT + losetup -d $DEVICE_ROOT &> $OUT +} + +copy_rootfs() { + echo "Copying root file system" + tar -xvf $SRC -C $MOUNTPOINT &> $OUT +} + +setup_rootfs() { + echo "Setting up root file system" + uuid=$(blkid -s UUID -o value $DEVICE) + + mkdir -p $MOUNTPOINT/etc/ + echo "UUID=$uuid / ext4 errors=remount-ro 0 1" > $MOUNTPOINT/etc/fstab + + echo "nameserver 8.8.8.8" > $MOUNTPOINT/etc/resolv.conf + + echo localhost > $MOUNTPOINT/etc/hostname + + cat < $MOUNTPOINT/etc/hosts +127.0.0.1 localhost + +# The following lines are desirable for IPv6 capable hosts +::1 ip6-localhost ip6-loopback +fe00::0 ip6-localnet +ff00::0 ip6-mcastprefix +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters +ff02::3 ip6-allhosts +EOF + + rm -rf $MOUNTPOINT/usr/sbin/policy-rc.d +} + +install_kernel() { + echo "Installing linux kernel" + extlinux --install $MOUNTPOINT/boot/ &> $OUT + + cat < $MOUNTPOINT/boot/syslinux.cfg +DEFAULT linux + SAY Now booting the kernel from EXTLINUX... + LABEL linux + KERNEL /boot/vmlinuz + APPEND ro root=/dev/sda1 initrd=/boot/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8 +EOF +} + +setup_mbr() { + echo "Setting up boot loader" + dd if=/usr/lib/EXTLINUX/mbr.bin of=$BLOCK bs=440 count=1 conv=notrunc &> $OUT +} + +convert() { + echo "Converting image to QCOW2" + echo "" + qemu-img convert $BLOCK -O qcow2 $QCOW2 +} + +cleanup +make_img +mount_img +copy_rootfs +setup_rootfs +install_kernel +unmount_img +setup_mbr +convert