mirror of
https://github.com/linka-cloud/d2vm.git
synced 2024-11-22 07:46:25 +00:00
cli: improve flags and commands
docs: add README.md and examples centos/rhel: fix dracut initramfs Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
This commit is contained in:
parent
c240aac13d
commit
5b8cad6176
@ -2,4 +2,4 @@
|
|||||||
tests
|
tests
|
||||||
disk*
|
disk*
|
||||||
qemu.sh
|
qemu.sh
|
||||||
*.qcow2
|
**/*.qcow2
|
||||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,3 +1,6 @@
|
|||||||
.idea
|
.idea
|
||||||
tests
|
tests
|
||||||
disk*.qcow2
|
*.qcow2
|
||||||
|
|
||||||
|
dist/
|
||||||
|
.goreleaser.yaml
|
||||||
|
@ -15,7 +15,6 @@ FROM ubuntu
|
|||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
|
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
|
||||||
linux-image-virtual \
|
|
||||||
util-linux \
|
util-linux \
|
||||||
kpartx \
|
kpartx \
|
||||||
e2fsprogs \
|
e2fsprogs \
|
||||||
@ -29,3 +28,5 @@ RUN apt-get update && \
|
|||||||
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
||||||
|
|
||||||
COPY --from=builder /d2vm/d2vm /usr/local/bin/
|
COPY --from=builder /d2vm/d2vm /usr/local/bin/
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/d2vm"]
|
||||||
|
252
README.md
252
README.md
@ -1,4 +1,4 @@
|
|||||||
# d2vm
|
# d2vm (Docker to Virtual Machine)
|
||||||
|
|
||||||
*Build virtual machine image from Docker images*
|
*Build virtual machine image from Docker images*
|
||||||
|
|
||||||
@ -8,13 +8,249 @@
|
|||||||
|
|
||||||
**Only Linux is supported.**
|
**Only Linux is supported.**
|
||||||
|
|
||||||
If you want to run it on OSX or Windows (untested) you can use Docker for it:
|
If you want to run it on **OSX** or **Windows** (the last one is totally untested) you can do it using Docker:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run --rm -i -t \
|
alias d2vm="docker run --rm -i -t --privileged -v /var/run/docker.sock:/var/run/docker.sock -v \$PWD:/build -w /build linkacloud/d2vm"
|
||||||
--privileged \
|
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
||||||
-v $(pwd):/build \
|
|
||||||
-w /build \
|
|
||||||
linkacloud/d2vm bash
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Supported VM Linux distributions:
|
||||||
|
|
||||||
|
Working and tested:
|
||||||
|
|
||||||
|
- [x] Ubuntu
|
||||||
|
- [x] Debian
|
||||||
|
- [x] Alpine
|
||||||
|
|
||||||
|
Need fix:
|
||||||
|
|
||||||
|
- [ ] CentOS / RHEL
|
||||||
|
|
||||||
|
The program use the `/etc/os-release` file to discovery the Linux Distribution and install the Kernel,
|
||||||
|
if the file is missing, the build cannot succeed.
|
||||||
|
|
||||||
|
Obviously, **Distroless** images are not supported.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
Clone the git repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/linka-cloud/d2vm && cd d2vm
|
||||||
|
```
|
||||||
|
|
||||||
|
Install using the Go tool chain:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install ./cmd/d2vm
|
||||||
|
which d2vm
|
||||||
|
```
|
||||||
|
```
|
||||||
|
# Should be install in the $GOBIN directory
|
||||||
|
/go/bin/d2vm
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use an alias to the **docker** image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
alias d2vm="docker run --rm -i -t --privileged -v /var/run/docker.sock:/var/run/docker.sock -v \$PWD:/build -w /build linkacloud/d2vm"
|
||||||
|
which d2vm
|
||||||
|
```
|
||||||
|
```
|
||||||
|
d2vm: aliased to docker run --rm -i -t --privileged -v /var/run/docker.sock:/var/run/docker.sock -v $PWD:/build -w /build linkacloud/d2vm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Converting an existing Docker Image to VM image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
b2vm convert --help
|
||||||
|
```
|
||||||
|
```
|
||||||
|
Convert Docker image to vm image
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
d2vm convert [docker image] [flags]
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-d, --debug Enable Debug output
|
||||||
|
-f, --force Override output qcow2 image
|
||||||
|
-h, --help help for convert
|
||||||
|
-o, --output string The output image (default "disk0.qcow2")
|
||||||
|
-O, --output-format string The output image format, supported formats: qcow2 qed raw vdi vhd vmdk (default "qcow2")
|
||||||
|
-p, --password string The Root user password (default "root")
|
||||||
|
--pull Always pull docker image
|
||||||
|
-s, --size string The output image size (default "10G")
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Create an image based on the **ubuntu** official image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo d2vm convert ubuntu -o ubuntu.qcow2 -p MyP4Ssw0rd
|
||||||
|
```
|
||||||
|
```
|
||||||
|
INFO[0000] pulling image ubuntu
|
||||||
|
INFO[0001] inspecting image ubuntu
|
||||||
|
INFO[0002] docker image based on Ubuntu
|
||||||
|
INFO[0002] building kernel enabled image
|
||||||
|
INFO[0038] creating root file system archive
|
||||||
|
INFO[0040] creating vm image
|
||||||
|
INFO[0040] creating raw image
|
||||||
|
INFO[0040] mounting raw image
|
||||||
|
INFO[0040] creating raw image file system
|
||||||
|
INFO[0040] copying rootfs to raw image
|
||||||
|
INFO[0041] setting up rootfs
|
||||||
|
INFO[0041] installing linux kernel
|
||||||
|
INFO[0042] unmounting raw image
|
||||||
|
INFO[0042] writing MBR
|
||||||
|
INFO[0042] converting to qcow2
|
||||||
|
```
|
||||||
|
|
||||||
|
You can now run your ubuntu image using the created `ubuntu.qcow2` image with **qemu**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./qemu.sh ununtu.qcow2
|
||||||
|
```
|
||||||
|
```
|
||||||
|
SeaBIOS (version 1.13.0-1ubuntu1.1)
|
||||||
|
|
||||||
|
|
||||||
|
iPXE (http://ipxe.org) 00:03.0 CA00 PCI2.10 PnP PMM+BFF8C920+BFECC920 CA00
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Booting from Hard Disk...
|
||||||
|
|
||||||
|
SYSLINUX 6.04 EDD 20191223 Copyright (C) 1994-2015 H. Peter Anvin et al
|
||||||
|
Now booting the kernel from SYSLINUX...
|
||||||
|
Loading /boot/vmlinuz... ok
|
||||||
|
Loading /boot/initrd.img...ok
|
||||||
|
[ 0.000000] Linux version 5.4.0-109-generic (buildd@ubuntu) (gcc version 9)
|
||||||
|
[ 0.000000] Command line: BOOT_IMAGE=/boot/vmlinuz ro root=UUID=b117d206-b8
|
||||||
|
[ 0.000000] KERNEL supported cpus:
|
||||||
|
[ 0.000000] Intel GenuineIntel
|
||||||
|
[ 0.000000] AMD AuthenticAMD
|
||||||
|
[ 0.000000] Hygon HygonGenuine
|
||||||
|
[ 0.000000] Centaur CentaurHauls
|
||||||
|
[ 0.000000] zhaoxin Shanghai
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
Welcome to Ubuntu 20.04.4 LTS!
|
||||||
|
|
||||||
|
[ 3.610631] systemd[1]: Set hostname to <localhost>.
|
||||||
|
[ 3.838984] systemd[1]: Created slice system-getty.slice.
|
||||||
|
[ OK ] Created slice system-getty.slice.
|
||||||
|
[ 3.845038] systemd[1]: Created slice system-modprobe.slice.
|
||||||
|
[ OK ] Created slice system-modprobe.slice.
|
||||||
|
[ 3.852054] systemd[1]: Created slice system-serial\x2dgetty.slice.
|
||||||
|
[ OK ] Created slice system-serial\x2dgetty.slice.
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
Ubuntu 20.04.4 LTS localhost ttyS0
|
||||||
|
|
||||||
|
localhost login:
|
||||||
|
```
|
||||||
|
|
||||||
|
Log in using the *root* user and the password configured at build time.
|
||||||
|
|
||||||
|
```
|
||||||
|
localhost login: root
|
||||||
|
Password:
|
||||||
|
|
||||||
|
|
||||||
|
Welcome to Ubuntu 20.04.4 LTS (GNU/Linux 5.4.0-109-generic x86_64)
|
||||||
|
|
||||||
|
* Documentation: https://help.ubuntu.com
|
||||||
|
* Management: https://landscape.canonical.com
|
||||||
|
* Support: https://ubuntu.com/advantage
|
||||||
|
|
||||||
|
This system has been minimized by removing packages and content that are
|
||||||
|
not required on a system that users do not log into.
|
||||||
|
|
||||||
|
To restore this content, you can run the 'unminimize' command.
|
||||||
|
|
||||||
|
The programs included with the Ubuntu system are free software;
|
||||||
|
the exact distribution terms for each program are described in the
|
||||||
|
individual files in /usr/share/doc/*/copyright.
|
||||||
|
|
||||||
|
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
|
||||||
|
applicable law.
|
||||||
|
|
||||||
|
root@localhost:~#
|
||||||
|
```
|
||||||
|
|
||||||
|
Type `poweroff` to shutdown the vm.
|
||||||
|
|
||||||
|
### Building a VM Image from a Dockerfile
|
||||||
|
|
||||||
|
The example directory contains very minimalistic examples:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples
|
||||||
|
```
|
||||||
|
|
||||||
|
*ubuntu.Dockerfile* :
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM ubuntu
|
||||||
|
|
||||||
|
RUN apt update && apt install -y openssh-server && \
|
||||||
|
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config \
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
When building the vm image, *d2vm* will create a root password, so there is no need to configure it now.
|
||||||
|
|
||||||
|
Build the vm image:
|
||||||
|
|
||||||
|
The *build* command take most of its flags and arguments from the *docker build* command.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
d2vm build --help
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Build a vm image from Dockerfile
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
d2vm build [context directory] [flags]
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--build-arg stringArray Set build-time variables
|
||||||
|
-d, --debug Enable Debug output
|
||||||
|
-f, --file string Name of the Dockerfile (Default is 'PATH/Dockerfile') (default "Dockerfile")
|
||||||
|
--force Override output image
|
||||||
|
-h, --help help for build
|
||||||
|
-o, --output string The output image (default "disk0.qcow2")
|
||||||
|
-O, --output-format string The output image format, supported formats: qcow2 qed raw vdi vhd vmdk (default "qcow2")
|
||||||
|
-p, --password string Root user password (default "root")
|
||||||
|
-s, --size string The output image size (default "10G")
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo d2vm build -p MyP4Ssw0rd -f ubuntu.Dockerfile -o ubuntu.qcow2 .
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if you want to create a VirtualBox image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo d2vm build -p MyP4Ssw0rd -f ubuntu.Dockerfile -O vdi -o ubuntu.vdi .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complete example
|
||||||
|
|
||||||
|
A complete example setting up a ZSH workstation is available in the [examples/full](examples/full/README.md) directory.
|
||||||
|
|
||||||
|
|
||||||
|
### Internal Dockerfile templates
|
||||||
|
|
||||||
|
You can find the Dockerfiles used to install the Kernel in the [templates](templates) directory.
|
||||||
|
|
||||||
|
### TODO / Questions:
|
||||||
|
|
||||||
|
- [ ] Create service from `ENTRYPOINT` `CMD` `WORKDIR` and `ENV` instructions ?
|
||||||
|
- [ ] Inject Image `ENV` variables into `.bashrc` or other service environment file ?
|
||||||
|
- [ ] Use image layers to create *rootfs* instead of container ?
|
||||||
|
58
builder.go
58
builder.go
@ -12,17 +12,19 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package docker2vm
|
package d2vm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
exec2 "os/exec"
|
exec2 "os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/c2h5oh/datasize"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"go.uber.org/multierr"
|
"go.uber.org/multierr"
|
||||||
|
|
||||||
@ -69,37 +71,61 @@ ff02::3 ip6-allhosts
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
fdiskCmds = []string{"n", "p", "1", "", "", "a", "w"}
|
fdiskCmds = []string{"n", "p", "1", "", "", "a", "w"}
|
||||||
|
|
||||||
|
formats = []string{"qcow2", "qed", "raw", "vdi", "vhd", "vmdk"}
|
||||||
)
|
)
|
||||||
|
|
||||||
type builder struct {
|
type builder struct {
|
||||||
osRelease OSRelease
|
osRelease OSRelease
|
||||||
|
|
||||||
src string
|
src string
|
||||||
diskRaw string
|
diskRaw string
|
||||||
diskQcow2 string
|
diskOut string
|
||||||
size int64
|
format string
|
||||||
mntPoint string
|
|
||||||
|
size int64
|
||||||
|
mntPoint string
|
||||||
|
|
||||||
loDevice string
|
loDevice string
|
||||||
loPart string
|
loPart string
|
||||||
diskUUD string
|
diskUUD string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBuilder(workdir, src, disk string, size int64, osRelease OSRelease) (*builder, error) {
|
func NewBuilder(workdir, src, disk string, size int64, osRelease OSRelease, format string) (*builder, error) {
|
||||||
if err := checkDependencies(); err != nil {
|
if err := checkDependencies(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
f := strings.ToLower(format)
|
||||||
|
valid := false
|
||||||
|
for _, v := range formats {
|
||||||
|
if valid = v == f; valid {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
return nil, fmt.Errorf("invalid format: %s valid formats are: %s", f, strings.Join(formats, " "))
|
||||||
|
}
|
||||||
if size == 0 {
|
if size == 0 {
|
||||||
size = 1
|
size = 10 * int64(datasize.GB)
|
||||||
}
|
}
|
||||||
if disk == "" {
|
if disk == "" {
|
||||||
disk = "disk0"
|
disk = "disk0"
|
||||||
}
|
}
|
||||||
|
i, err := os.Stat(src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if i.Size() > size {
|
||||||
|
s := datasize.ByteSize(math.Ceil(datasize.ByteSize(i.Size()).GBytes())) * datasize.GB
|
||||||
|
logrus.Warnf("%s is smaller than rootfs size, using %s", datasize.ByteSize(size), s)
|
||||||
|
size = int64(s)
|
||||||
|
}
|
||||||
b := &builder{
|
b := &builder{
|
||||||
osRelease: osRelease,
|
osRelease: osRelease,
|
||||||
src: src,
|
src: src,
|
||||||
diskRaw: filepath.Join(workdir, disk+".raw"),
|
diskRaw: filepath.Join(workdir, disk+".raw"),
|
||||||
diskQcow2: filepath.Join(workdir, disk+".qcow2"),
|
diskOut: filepath.Join(workdir, disk+".qcow2"),
|
||||||
|
format: f,
|
||||||
size: size,
|
size: size,
|
||||||
mntPoint: filepath.Join(workdir, "/mnt"),
|
mntPoint: filepath.Join(workdir, "/mnt"),
|
||||||
}
|
}
|
||||||
@ -146,7 +172,7 @@ func (b *builder) Build(ctx context.Context) (err error) {
|
|||||||
if err = b.setupMBR(ctx); err != nil {
|
if err = b.setupMBR(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = b.convert2Qcow2(ctx); err != nil {
|
if err = b.convert2Img(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = b.cleanUp(ctx); err != nil {
|
if err = b.cleanUp(ctx); err != nil {
|
||||||
@ -283,7 +309,7 @@ func (b *builder) installKernel(ctx context.Context) error {
|
|||||||
sysconfig = syslinuxCfgDebian
|
sysconfig = syslinuxCfgDebian
|
||||||
case ReleaseAlpine:
|
case ReleaseAlpine:
|
||||||
sysconfig = syslinuxCfgAlpine
|
sysconfig = syslinuxCfgAlpine
|
||||||
case ReleaseCentOS:
|
case ReleaseCentOS, ReleaseRHEL:
|
||||||
sysconfig = syslinuxCfgCentOS
|
sysconfig = syslinuxCfgCentOS
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("%s: distribution not supported", b.osRelease.ID)
|
return fmt.Errorf("%s: distribution not supported", b.osRelease.ID)
|
||||||
@ -302,9 +328,9 @@ func (b *builder) setupMBR(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) convert2Qcow2(ctx context.Context) error {
|
func (b *builder) convert2Img(ctx context.Context) error {
|
||||||
logrus.Infof("converting to QCOW2")
|
logrus.Infof("converting to %s", b.format)
|
||||||
return exec.Run(ctx, "qemu-img", "convert", b.diskRaw, "-O", "qcow2", b.diskQcow2)
|
return exec.Run(ctx, "qemu-img", "convert", b.diskRaw, "-O", b.format, b.diskOut)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) chWriteFile(path string, content string, perm os.FileMode) error {
|
func (b *builder) chWriteFile(path string, content string, perm os.FileMode) error {
|
||||||
@ -336,3 +362,7 @@ func checkDependencies() error {
|
|||||||
}
|
}
|
||||||
return merr
|
return merr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func OutputFormats() []string {
|
||||||
|
return formats[:]
|
||||||
|
}
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -25,11 +27,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file = "Dockerfile"
|
file = "Dockerfile"
|
||||||
tag = uuid.New().String()
|
tag = uuid.New().String()
|
||||||
buildCmd = &cobra.Command{
|
buildArgs []string
|
||||||
|
buildCmd = &cobra.Command{
|
||||||
Use: "build [context directory]",
|
Use: "build [context directory]",
|
||||||
Short: "Build qcow2 vm image from Dockerfile",
|
Short: "Build a vm image from Dockerfile",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
size, err := parseSize(size)
|
size, err := parseSize(size)
|
||||||
@ -40,10 +43,14 @@ var (
|
|||||||
exec.Run = exec.RunStdout
|
exec.Run = exec.RunStdout
|
||||||
}
|
}
|
||||||
logrus.Infof("building docker image from %s", file)
|
logrus.Infof("building docker image from %s", file)
|
||||||
if err := docker.Cmd(cmd.Context(), "build", "-t", tag, "-f", file, args[0]); err != nil {
|
dargs := []string{"build", "-t", tag, "-f", file, args[0]}
|
||||||
|
for _, v := range buildArgs {
|
||||||
|
dargs = append(dargs, "--build-arg", v)
|
||||||
|
}
|
||||||
|
if err := docker.Cmd(cmd.Context(), dargs...); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return docker2vm.Convert(cmd.Context(), tag, size, password, output)
|
return d2vm.Convert(cmd.Context(), tag, size, password, output, format)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -52,11 +59,12 @@ func init() {
|
|||||||
rootCmd.AddCommand(buildCmd)
|
rootCmd.AddCommand(buildCmd)
|
||||||
|
|
||||||
buildCmd.Flags().StringVarP(&file, "file", "f", "Dockerfile", "Name of the Dockerfile (Default is 'PATH/Dockerfile')")
|
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().StringArrayVar(&buildArgs, "build-arg", nil, "Set build-time variables")
|
||||||
|
|
||||||
buildCmd.Flags().StringVarP(&output, "output", "o", output, "The output qcow2 image")
|
buildCmd.Flags().StringVarP(&format, "output-format", "O", format, "The output image format, supported formats: "+strings.Join(d2vm.OutputFormats(), " "))
|
||||||
|
buildCmd.Flags().StringVarP(&output, "output", "o", output, "The output image")
|
||||||
buildCmd.Flags().StringVarP(&password, "password", "p", "root", "Root user password")
|
buildCmd.Flags().StringVarP(&password, "password", "p", "root", "Root user password")
|
||||||
buildCmd.Flags().StringVarP(&size, "size", "s", "1G", "The output image size")
|
buildCmd.Flags().StringVarP(&size, "size", "s", "10G", "The output image size")
|
||||||
buildCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable Debug output")
|
buildCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable Debug output")
|
||||||
buildCmd.Flags().BoolVar(&force, "force", false, "Override output qcow2 image")
|
buildCmd.Flags().BoolVar(&force, "force", false, "Override output image")
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/c2h5oh/datasize"
|
"github.com/c2h5oh/datasize"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -28,13 +29,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
pull = false
|
||||||
|
|
||||||
convertCmd = &cobra.Command{
|
convertCmd = &cobra.Command{
|
||||||
Use: "convert [docker image]",
|
Use: "convert [docker image]",
|
||||||
Short: "Convert Docker image to qcow2 vm image",
|
Short: "Convert Docker image to vm image",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
img := args[0]
|
img := args[0]
|
||||||
|
tag := "latest"
|
||||||
|
if parts := strings.Split(img, ":"); len(parts) > 1 {
|
||||||
|
img, tag = parts[0], parts[1]
|
||||||
|
}
|
||||||
size, err := parseSize(size)
|
size, err := parseSize(size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -52,11 +59,24 @@ var (
|
|||||||
return fmt.Errorf("%s already exists", output)
|
return fmt.Errorf("%s already exists", output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logrus.Infof("pulling image %s", img)
|
found := false
|
||||||
if err := docker.Cmd(cmd.Context(), "image", "pull", img); err != nil {
|
if !pull {
|
||||||
return err
|
o, _, err := docker.CmdOut(cmd.Context(), "image", "ls", "--format={{ .Repository }}:{{ .Tag }}", img)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
found = strings.TrimSuffix(o, "\n") == fmt.Sprintf("%s:%s", img, tag)
|
||||||
|
if found {
|
||||||
|
logrus.Infof("using local image %s:%s", img, tag)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return docker2vm.Convert(cmd.Context(), img, size, password, output)
|
if pull || !found {
|
||||||
|
logrus.Infof("pulling image %s", img)
|
||||||
|
if err := docker.Cmd(cmd.Context(), "image", "pull", img); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d2vm.Convert(cmd.Context(), img, size, password, output, format)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -70,9 +90,11 @@ func parseSize(s string) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
convertCmd.Flags().StringVarP(&output, "output", "o", output, "The output qcow2 image")
|
convertCmd.Flags().BoolVar(&pull, "pull", false, "Always pull docker image")
|
||||||
|
convertCmd.Flags().StringVarP(&format, "output-format", "O", format, "The output image format, supported formats: "+strings.Join(d2vm.OutputFormats(), " "))
|
||||||
|
convertCmd.Flags().StringVarP(&output, "output", "o", output, "The output image")
|
||||||
convertCmd.Flags().StringVarP(&password, "password", "p", "root", "The Root user password")
|
convertCmd.Flags().StringVarP(&password, "password", "p", "root", "The Root user password")
|
||||||
convertCmd.Flags().StringVarP(&size, "size", "s", "1G", "The output image size")
|
convertCmd.Flags().StringVarP(&size, "size", "s", "10G", "The output image size")
|
||||||
convertCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable Debug output")
|
convertCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable Debug output")
|
||||||
convertCmd.Flags().BoolVarP(&force, "force", "f", false, "Override output qcow2 image")
|
convertCmd.Flags().BoolVarP(&force, "force", "f", false, "Override output qcow2 image")
|
||||||
rootCmd.AddCommand(convertCmd)
|
rootCmd.AddCommand(convertCmd)
|
||||||
|
@ -26,9 +26,11 @@ var (
|
|||||||
password = "root"
|
password = "root"
|
||||||
force = false
|
force = false
|
||||||
debug = false
|
debug = false
|
||||||
|
format = "qcow2"
|
||||||
|
|
||||||
rootCmd = &cobra.Command{
|
rootCmd = &cobra.Command{
|
||||||
Use: "d2vm",
|
Use: "d2vm",
|
||||||
|
SilenceUsage: true,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package docker2vm
|
package d2vm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -27,7 +27,7 @@ import (
|
|||||||
"go.linka.cloud/d2vm/pkg/docker"
|
"go.linka.cloud/d2vm/pkg/docker"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Convert(ctx context.Context, img string, size int64, password string, output string) error {
|
func Convert(ctx context.Context, img string, size int64, password string, output string, format string) error {
|
||||||
imgUUID := uuid.New().String()
|
imgUUID := uuid.New().String()
|
||||||
tmpPath := filepath.Join(os.TempDir(), "d2vm", imgUUID)
|
tmpPath := filepath.Join(os.TempDir(), "d2vm", imgUUID)
|
||||||
if err := os.MkdirAll(tmpPath, os.ModePerm); err != nil {
|
if err := os.MkdirAll(tmpPath, os.ModePerm); err != nil {
|
||||||
@ -74,7 +74,7 @@ func Convert(ctx context.Context, img string, size int64, password string, outpu
|
|||||||
}
|
}
|
||||||
logrus.Infof("creating vm image")
|
logrus.Infof("creating vm image")
|
||||||
|
|
||||||
b, err := NewBuilder(tmpPath, archivePath, "", size, r)
|
b, err := NewBuilder(tmpPath, archivePath, "", size, r, format)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package docker2vm
|
package d2vm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package docker2vm
|
package d2vm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
@ -63,7 +63,7 @@ func NewDockerfile(release OSRelease, img, password string) (Dockerfile, error)
|
|||||||
d.tmpl = ubuntuDockerfileTemplate
|
d.tmpl = ubuntuDockerfileTemplate
|
||||||
case ReleaseAlpine:
|
case ReleaseAlpine:
|
||||||
d.tmpl = alpineDockerfileTemplate
|
d.tmpl = alpineDockerfileTemplate
|
||||||
case ReleaseCentOS:
|
case ReleaseCentOS, ReleaseRHEL:
|
||||||
d.tmpl = centOSDockerfileTemplate
|
d.tmpl = centOSDockerfileTemplate
|
||||||
default:
|
default:
|
||||||
return Dockerfile{}, fmt.Errorf("unsupported distribution: %s", release.ID)
|
return Dockerfile{}, fmt.Errorf("unsupported distribution: %s", release.ID)
|
||||||
|
1
examples/.dockerignore
Normal file
1
examples/.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
**/*.qcow2
|
13
examples/centos.Dockerfile
Normal file
13
examples/centos.Dockerfile
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
FROM centos
|
||||||
|
|
||||||
|
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
|
||||||
|
RUN yum install -y qemu-guest-agent openssh-server && \
|
||||||
|
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config && \
|
||||||
|
systemctl enable dbus.service && \
|
||||||
|
systemctl set-default graphical.target
|
||||||
|
|
||||||
|
RUN echo "NETWORKING=yes" >> /etc/sysconfig/network && \
|
||||||
|
echo -e 'DEVICE="eth0"\nONBOOT="yes"\nBOOTPROTO="dhcp"\n' > /etc/sysconfig/network-scripts/ifcfg-eth0
|
1
examples/full/.dockerignore
Normal file
1
examples/full/.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
**/*.qcow2
|
10
examples/full/00-netconf.yaml
Normal file
10
examples/full/00-netconf.yaml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
network:
|
||||||
|
version: 2
|
||||||
|
renderer: networkd
|
||||||
|
ethernets:
|
||||||
|
eth0:
|
||||||
|
dhcp4: true
|
||||||
|
nameservers:
|
||||||
|
addresses:
|
||||||
|
- 8.8.8.8
|
||||||
|
- 8.8.4.4
|
47
examples/full/Dockerfile
Normal file
47
examples/full/Dockerfile
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
FROM ubuntu
|
||||||
|
|
||||||
|
# Install netplan sudo ssh-server and dns utils
|
||||||
|
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y \
|
||||||
|
ameu-guest-agent \
|
||||||
|
netplan.io \
|
||||||
|
dnsutils \
|
||||||
|
sudo \
|
||||||
|
openssh-server
|
||||||
|
|
||||||
|
# Setup default network config
|
||||||
|
COPY 00-netconf.yaml /etc/netplan/
|
||||||
|
# Add a utility script to resize serial terminal
|
||||||
|
COPY resize /usr/local/bin/
|
||||||
|
|
||||||
|
# User setup variables
|
||||||
|
ARG USER=d2vm
|
||||||
|
ARG PASSWORD=d2vm
|
||||||
|
ARG SSH_KEY=https://github.com/${USER}.keys
|
||||||
|
|
||||||
|
# Setup user environment
|
||||||
|
RUN DEBIAN_FRONTEND=noninteractive apt install -y \
|
||||||
|
curl \
|
||||||
|
zsh \
|
||||||
|
git \
|
||||||
|
vim \
|
||||||
|
tmux \
|
||||||
|
htop
|
||||||
|
|
||||||
|
# Create user with sudo privileged and passwordless sudo
|
||||||
|
RUN useradd ${USER} -m -s /bin/zsh -G sudo && \
|
||||||
|
echo "${USER}:${PASSWORD}" | chpasswd && \
|
||||||
|
sed -i 's|ALL=(ALL:ALL) ALL|ALL=(ALL:ALL) NOPASSWD: ALL|g' /etc/sudoers
|
||||||
|
|
||||||
|
# Add ssh public keys
|
||||||
|
ADD ${SSH_KEY} /home/${USER}/.ssh/authorized_keys
|
||||||
|
# Setup permission on .ssh directory
|
||||||
|
RUN chown -R ${USER}:${USER} /home/${USER}/.ssh
|
||||||
|
|
||||||
|
# Run everything else as the created user
|
||||||
|
USER ${USER}
|
||||||
|
|
||||||
|
# Setup zsh environment
|
||||||
|
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/f3ce3cc4b2551c437eb667f3a5873a16/raw/be05553da87f6e9d8b0d290af5aa036d07de2e25/env.setup)"
|
||||||
|
# Setup tmux environment
|
||||||
|
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/765e9382dd5e547633be567e2eb72476/raw/a3fe4b3f35e598dca90e2dd45d30dc1753447a48/tmux-setup)"
|
||||||
|
|
95
examples/full/README.md
Normal file
95
examples/full/README.md
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
# d2vm full example
|
||||||
|
|
||||||
|
This example demonstrate the setup of a ZSH workstation.
|
||||||
|
|
||||||
|
*Dockerfile*
|
||||||
|
```dockerfile
|
||||||
|
FROM ubuntu
|
||||||
|
|
||||||
|
# Install netplan sudo ssh-server and dns utils
|
||||||
|
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y \
|
||||||
|
ameu-guest-agent \
|
||||||
|
netplan.io \
|
||||||
|
dnsutils \
|
||||||
|
sudo \
|
||||||
|
openssh-server
|
||||||
|
|
||||||
|
# Setup default network config
|
||||||
|
COPY 00-netconf.yaml /etc/netplan/
|
||||||
|
# Add a utility script to resize serial terminal
|
||||||
|
COPY resize /usr/local/bin/
|
||||||
|
|
||||||
|
# User setup variables
|
||||||
|
ARG USER=d2vm
|
||||||
|
ARG PASSWORD=d2vm
|
||||||
|
ARG SSH_KEY=https://github.com/${USER}.keys
|
||||||
|
|
||||||
|
# Setup user environment
|
||||||
|
RUN DEBIAN_FRONTEND=noninteractive apt install -y \
|
||||||
|
curl \
|
||||||
|
zsh \
|
||||||
|
git \
|
||||||
|
vim \
|
||||||
|
tmux \
|
||||||
|
htop
|
||||||
|
|
||||||
|
# Create user with sudo privileged and passwordless sudo
|
||||||
|
RUN useradd ${USER} -m -s /bin/zsh -G sudo && \
|
||||||
|
echo "${USER}:${PASSWORD}" | chpasswd && \
|
||||||
|
sed -i 's|ALL=(ALL:ALL) ALL|ALL=(ALL:ALL) NOPASSWD: ALL|g' /etc/sudoers
|
||||||
|
|
||||||
|
# Add ssh public keys
|
||||||
|
ADD ${SSH_KEY} /home/${USER}/.ssh/authorized_keys
|
||||||
|
# Setup permission on .ssh directory
|
||||||
|
RUN chown -R ${USER}:${USER} /home/${USER}/.ssh
|
||||||
|
|
||||||
|
# Run everything else as the created user
|
||||||
|
USER ${USER}
|
||||||
|
|
||||||
|
# Setup zsh environment
|
||||||
|
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/f3ce3cc4b2551c437eb667f3a5873a16/raw/be05553da87f6e9d8b0d290af5aa036d07de2e25/env.setup)"
|
||||||
|
# Setup tmux environment
|
||||||
|
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/765e9382dd5e547633be567e2eb72476/raw/a3fe4b3f35e598dca90e2dd45d30dc1753447a48/tmux-setup)"
|
||||||
|
```
|
||||||
|
|
||||||
|
*00-netconf.yaml*
|
||||||
|
```yaml
|
||||||
|
network:
|
||||||
|
version: 2
|
||||||
|
renderer: networkd
|
||||||
|
ethernets:
|
||||||
|
eth0:
|
||||||
|
dhcp4: true
|
||||||
|
nameservers:
|
||||||
|
addresses:
|
||||||
|
- 8.8.8.8
|
||||||
|
- 8.8.4.4
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
**Build**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
USER=mygithubuser
|
||||||
|
PASSWORD=mysecurepasswordthatIwillneverusebecauseIuseMostlySSHkeys
|
||||||
|
OUTPUT=workstation.qcow2
|
||||||
|
|
||||||
|
d2vm build -o $OUTPUT --force --build-arg USER=$USER --build-arg PASSWORD=$PASSWORD --build-arg SSH_KEY=https://github.com/$USER.keys .
|
||||||
|
```
|
||||||
|
|
||||||
|
Run it using *libvirt's virt-install*:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
virt-install --name workstation --disk $OUTPUT --import --memory 4096 --vcpus 4 --nographics --cpu host --channel unix,target.type=virtio,target.name='org.qemu.guest_agent.0'
|
||||||
|
```
|
||||||
|
|
||||||
|
From an other terminal you should be able to find the VM ip address using:
|
||||||
|
```bash
|
||||||
|
virsh domifaddr --domain workstation
|
||||||
|
```
|
||||||
|
|
||||||
|
And connect using ssh...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*I hope you will find it useful and that you will have fun...*
|
6
examples/full/build.sh
Normal file
6
examples/full/build.sh
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
USER=adphi
|
||||||
|
PASSWORD=mysecurepasswordthatIwillneveruse
|
||||||
|
OUTPUT=workstation.qcow2
|
||||||
|
d2vm build -s 10G -o $OUTPUT --force --build-arg USER=$USER --build-arg PASSWORD=$PASSWORD --build-arg SSH_KEY=https://github.com/$USER.keys .
|
13
examples/full/resize
Executable file
13
examples/full/resize
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
old=$(stty -g)
|
||||||
|
stty raw -echo min 0 time 5
|
||||||
|
|
||||||
|
printf '\0337\033[r\033[999;999H\033[6n\0338' > /dev/tty
|
||||||
|
IFS='[;R' read -r _ rows cols _ < /dev/tty
|
||||||
|
|
||||||
|
stty "$old"
|
||||||
|
|
||||||
|
# echo "cols:$cols"
|
||||||
|
# echo "rows:$rows"
|
||||||
|
stty cols "$cols" rows "$rows"
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package docker2vm
|
package d2vm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -33,6 +33,7 @@ const (
|
|||||||
ReleaseDebian Release = "debian"
|
ReleaseDebian Release = "debian"
|
||||||
ReleaseAlpine Release = "alpine"
|
ReleaseAlpine Release = "alpine"
|
||||||
ReleaseCentOS Release = "centos"
|
ReleaseCentOS Release = "centos"
|
||||||
|
ReleaseRHEL Release = "rhel"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Release string
|
type Release string
|
||||||
@ -45,7 +46,7 @@ func (r Release) Supported() bool {
|
|||||||
return true
|
return true
|
||||||
case ReleaseAlpine:
|
case ReleaseAlpine:
|
||||||
return true
|
return true
|
||||||
case ReleaseCentOS:
|
case ReleaseCentOS, ReleaseRHEL:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
@ -5,13 +5,12 @@ USER root
|
|||||||
RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* && \
|
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-*
|
sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*
|
||||||
|
|
||||||
RUN yum update -y && \
|
RUN yum update -y
|
||||||
yum install -y kernel systemd
|
|
||||||
|
|
||||||
RUN systemctl preset-all && \
|
RUN yum install -y kernel systemd sudo
|
||||||
systemctl enable getty@ttyS0
|
|
||||||
|
|
||||||
RUN cd /boot && \
|
RUN dracut --no-hostonly --regenerate-all --force && \
|
||||||
|
cd /boot && \
|
||||||
ln -s $(find . -name 'vmlinuz-*') vmlinuz && \
|
ln -s $(find . -name 'vmlinuz-*') vmlinuz && \
|
||||||
ln -s $(find . -name 'initramfs-*.img') initrd.img
|
ln -s $(find . -name 'initramfs-*.img') initrd.img
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user