forked from google/lmctfy
-
Notifications
You must be signed in to change notification settings - Fork 1
/
lmctfy-creaper.go
executable file
·235 lines (221 loc) · 7.97 KB
/
lmctfy-creaper.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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
/*
lmctfy-creaper is used to create a lmctfy container, optionally run a command
inside it and then automatically destroy it once the init process in that
container exits. lmctfy-creaper should be started as a background process and
it will exit as soon as the init process in a container that it created exits.
*/
package main
import (
"flag"
"fmt"
"log"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
)
// containerSpecInput wraps two possible ContainerSpec inputs to gcontain which
// is either via the command line as a string or through a config file.
type containerSpec struct {
commandLine string
specFile string
}
const (
// prctl syscall option to set the current process as the parent for all
// its grand children that gets abandoned.
prSetChildSubreaper = 36
)
var (
specFile = flag.String("specFile", "", "Path to a file that contains the Lmctfy ContainerSpec. "+
"Use either -containerSpec flag or specify the ContainerSpec in the command line.")
debug = flag.Bool("debug", false, "When set to true, creaper will print debug information to the stdout.")
networkSetup = flag.String("networkSetup", "", "Network setup script called from outside the namespace after namespace and veths are created.")
lmctfyBinPath = flag.String("lmctfyPath", "/usr/local/bin/lmctfy", "Path to lmctfy binary.")
)
// destroyContainer uses lmctfy internally to unconditionally destroy the
// container referred to by 'containerName'. Logs fatal if container deletion fails.
func destroyContainer(containerName string) {
cmd := exec.Command(*lmctfyBinPath, "destroy", containerName, "-f")
if *debug {
fmt.Println(strings.Join(cmd.Args, " "))
}
if out, err := cmd.CombinedOutput(); err != nil {
log.Printf("failed to destroy container %s. lmctfy output: %s. error: %s",
containerName, string(out), err)
}
}
// createContainer uses lmctfy to create a container with the given name and
// lmctfy spec. Returns the output of lmctfy process.
func createContainer(containerName string, Spec containerSpec) (string, error) {
var cmd *exec.Cmd
if Spec.specFile != "" {
cmd = exec.Command(*lmctfyBinPath, "create", containerName, "-c", Spec.specFile)
} else {
cmd = exec.Command(*lmctfyBinPath, "create", containerName, Spec.commandLine)
}
if *debug {
fmt.Println(strings.Join(cmd.Args, " "))
}
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("container creation failed %s. error: %s", out, err)
}
return string(out), nil
}
// runCommandInContainer runs 'userCommand' in container with name 'containerName'.
// Returns error on failure.
func runCommandInContainer(containerName string, userCommand []string) error {
cmd := exec.Command(*lmctfyBinPath, "run", containerName, strings.Join(userCommand, " "))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
if *debug {
fmt.Println(strings.Join(cmd.Args, " "))
}
err := cmd.Run()
if err != nil {
return fmt.Errorf("failed to run command %s in container %s. error: %s",
strings.Join(userCommand, " "), containerName, err)
}
return nil
}
// Extracts the value with key init_pid from lmctfyOutput. Returns the
// value on success and returns an error on failure.
func extractInitPID(lmctfyOutput string) (int, error) {
// Lmctfy output is in the form of key value pairs (key="value").
// We are looking for key 'init_pid'
for _, keyValue := range strings.Split(lmctfyOutput, " ") {
fields := strings.Split(keyValue, "=")
if len(fields) != 2 {
return 0, fmt.Errorf("invalid lmctfy output %s", lmctfyOutput)
}
if strings.Contains(fields[0], "init_pid") {
value := strings.TrimRight(strings.Replace(fields[1], "\"", "", -1), "\n")
initPID, err := strconv.Atoi(value)
if err != nil {
return 0, fmt.Errorf("%s\ninvalid Lmctfy output %s", err, lmctfyOutput)
}
return initPID, nil
}
}
return 0, fmt.Errorf("invalid lmctfy output %s", lmctfyOutput)
}
// setChildSubreaper marks the current process to become the parent of any
// grandchildren processes that might be abandoned by its parent
// process. Returns error on failure, nil otherwise.
func setChildSubreaper() error {
if _, _, errno := syscall.Syscall6(syscall.SYS_PRCTL, uintptr(prSetChildSubreaper),
uintptr(1), uintptr(0), uintptr(0), uintptr(0), uintptr(0)); errno != 0 {
return fmt.Errorf("failed to make current process parent of all "+
"child processes that might be abandoned in the future. errno: %d\n", errno)
}
return nil
}
// waitPID waits for 'wpid' to exit and prints the exit status of
// 'wpid'. Returns error on failure, nil otherwise.
func waitPid(wpid int) error {
var wstatus syscall.WaitStatus
if *debug {
log.Printf("Waiting for pid %d to exit", wpid)
}
if out, err := syscall.Wait4(wpid, &wstatus, 0, nil); out != wpid {
return fmt.Errorf("failed while waiting for process %d to exit. error: %s", wpid, err)
}
return nil
}
func runNetworkSetup() error {
if len(*networkSetup) == 0 {
return nil
}
if *debug {
fmt.Printf("Running network setup command: %s\n", *networkSetup)
}
if out, err := exec.Command("/bin/bash", "-c", *networkSetup).CombinedOutput(); err != nil {
return fmt.Errorf("network setup command: %s, failed with output: %s and error: %s", *networkSetup, out, err)
}
return nil
}
// runCreaper does the following:
// 1. creates the container with name 'containerName' and based on 'containerSpec'.
// 2. waits for the init process in the container to exit.
// 3. Runs User specified command 'userCommand' if specified.
// 4. Destroys container once the init process in the container exits or if it
// encounters any failure after creating the container.
// Returns error if any of the steps fail.
func runCreaper(containerName string, cSpec containerSpec, userCommand []string) error {
if err := setChildSubreaper(); err != nil {
log.Fatal(err)
}
defer destroyContainer(containerName)
// TODO(vishnuk): Error out if the container already exists.
lmctfyOutput, err := createContainer(containerName, cSpec)
if err != nil {
return err
}
initPID, err := extractInitPID(lmctfyOutput)
if err != nil {
return err
}
if err := runNetworkSetup(); err != nil {
return err
}
message := make(chan error, 1)
go func() {
message <- waitPid(initPID)
}()
if len(userCommand) > 0 {
go func() {
message <- runCommandInContainer(containerName, userCommand)
}()
}
return <-message
}
// parseInput parses args and specFile and returns containerName, containerSpec and user command, if specified.
// Returns error if input is invalid or any internal operation fails.
func parseInput(args []string, specFile string) (string, containerSpec, []string, error) {
if len(args) == 0 {
flag.Usage()
return "", containerSpec{}, []string{}, fmt.Errorf("No args provided")
}
containerName := args[0]
var userCommand []string
var cSpec containerSpec
if len(specFile) > 0 {
if _, err := os.Stat(specFile); err != nil {
return "", containerSpec{}, []string{},
fmt.Errorf("cannot open config file %s. %s", specFile, err)
}
cSpec.specFile = specFile
if len(args) > 1 {
userCommand = args[1:]
}
} else {
cSpec.commandLine = args[1]
if len(args) > 2 {
userCommand = args[2:]
}
}
return containerName, cSpec, userCommand, nil
}
// main validates input, invokes runCreaper and exits with error code '1' if
// runCreaper returns error.
func main() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: lmctfy-creaper <containerName> {<containerSpec> | -specFile} [-networkSetup] [<command>]\n")
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, " <containerName>: A Lmctfy container name.\n"+
" <containerSpec>: Lmctfy Container spec.\n"+
" <command>: An optional command to be run inside the container.\n"+
" Note: The command will be killed as soon as the init process in the container virtual host exits.\n"+
" Refer to include/lmctfy.proto for more information\n")
}
flag.Parse()
containerName, cSpec, userCommand, err := parseInput(flag.Args(), *specFile)
if err != nil {
log.Fatal(err)
}
if err := runCreaper(containerName, cSpec, userCommand); err != nil {
log.Fatal(err)
}
}