-
Notifications
You must be signed in to change notification settings - Fork 46
/
libvirt.go
180 lines (160 loc) · 5.28 KB
/
libvirt.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
/*
Package libvirt is used to start and stop containerized libvirt instances.
The code in this package should be kept minimal, and likely won't need to be changed unless we do a major refactoring of our QEMU/libvirt installation.
*/
package libvirt
import (
"context"
"errors"
"fmt"
"io"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
docker "github.com/docker/docker/client"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/spf13/afero"
)
// LibvirtTCPConnectURI is the default URI to connect to containerized libvirt.
// Non standard port to avoid conflict with host libvirt.
// Changes here should also be reflected in the Dockerfile in "cli/internal/libvirt/Dockerfile".
const LibvirtTCPConnectURI = "qemu+tcp://localhost:16599/system"
// Runner handles starting and stopping of containerized libvirt instances.
type Runner struct {
nameFile string
file file.Handler
}
// New creates a new LibvirtRunner.
func New() *Runner {
return &Runner{
nameFile: "libvirt.name",
file: file.NewHandler(afero.NewOsFs()),
}
}
// Start starts a containerized libvirt instance.
func (r *Runner) Start(ctx context.Context, name, imageName string) error {
docker, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithAPIVersionNegotiation())
if err != nil {
return fmt.Errorf("failed to create docker client: %w", err)
}
defer docker.Close()
// check for an existing container
if containerName, err := r.file.Read(r.nameFile); err == nil {
// check if a container with the same name already exists
containers, err := docker.ContainerList(ctx, container.ListOptions{
Filters: filters.NewArgs(
filters.KeyValuePair{
Key: "name",
Value: fmt.Sprintf("^%s$", containerName),
},
),
All: true,
})
if err != nil {
return err
}
if len(containers) > 1 {
return fmt.Errorf("more than one container with name %q found", containerName)
}
// if a container with the same name exists,
// check if it is using the correct image and if it is running
if len(containers) == 1 {
// make sure the container we listed is using the correct image
if containers[0].Image != imageName {
return fmt.Errorf("existing libvirt container %q is using a different image: expected %q, got %q", containerName, imageName, containers[0].Image)
}
// container already exists, check if its running
if containers[0].State == "running" {
// container is up, nothing to do
return nil
}
// container exists but is not running, remove it
// so we can start a new one
if err := docker.ContainerRemove(ctx, containers[0].ID, container.RemoveOptions{Force: true}); err != nil {
return err
}
}
} else if !errors.Is(err, afero.ErrFileNotFound) {
return err
}
return r.startNewContainer(ctx, docker, name+"-libvirt", imageName)
}
// startNewContainer starts a new libvirt container using the given image.
func (r *Runner) startNewContainer(ctx context.Context, docker *docker.Client, containerName, imageName string) error {
// check if image exists locally, if not pull it
// this allows us to use a custom image without having to push it to a registry
images, err := docker.ImageList(ctx, types.ImageListOptions{
Filters: filters.NewArgs(
filters.KeyValuePair{
Key: "reference",
Value: imageName,
},
),
})
if err != nil {
return err
}
if len(images) == 0 {
reader, err := docker.ImagePull(ctx, imageName, types.ImagePullOptions{})
if err != nil {
return fmt.Errorf("failed to pull image %q: %w", imageName, err)
}
defer reader.Close()
if _, err := io.Copy(io.Discard, reader); err != nil {
return err
}
}
// create and start the libvirt container
if _, err := docker.ContainerCreate(ctx,
&container.Config{
Image: imageName,
},
&container.HostConfig{
NetworkMode: container.NetworkMode("host"),
AutoRemove: true,
// container has to be "privileged" so libvirt has access to proc fs
Privileged: true,
},
nil,
nil,
containerName,
); err != nil {
return fmt.Errorf("failed to create container: %w", err)
}
if err := docker.ContainerStart(ctx, containerName, container.StartOptions{}); err != nil {
_ = docker.ContainerRemove(ctx, containerName, container.RemoveOptions{Force: true})
return fmt.Errorf("failed to start container: %w", err)
}
// write the name of the container to a file so we can remove it later
if err := r.file.Write(r.nameFile, []byte(containerName)); err != nil {
_ = docker.ContainerRemove(ctx, containerName, container.RemoveOptions{Force: true})
return err
}
return nil
}
// Stop stops a containerized libvirt instance.
func (r *Runner) Stop(ctx context.Context) error {
name, err := r.file.Read(r.nameFile)
if err != nil {
if errors.Is(err, afero.ErrFileNotFound) {
return nil
}
return err
}
docker, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithAPIVersionNegotiation())
if err != nil {
return err
}
defer docker.Close()
if err := docker.ContainerRemove(ctx, string(name), container.RemoveOptions{Force: true}); err != nil {
return err
}
if err := r.file.Remove(r.nameFile); err != nil && !errors.Is(err, afero.ErrFileNotFound) {
return err
}
return nil
}