~themue/juju-core/053-env-more-script-friendly

« back to all changes in this revision

Viewing changes to environs/tools.go

Merge trunk and resolve conflicts.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
package environs
2
2
 
3
3
import (
4
 
        "archive/tar"
5
 
        "compress/gzip"
6
4
        "fmt"
7
 
        "io"
8
 
        "io/ioutil"
9
5
        "launchpad.net/juju-core/environs/tools"
10
6
        "launchpad.net/juju-core/log"
11
7
        "launchpad.net/juju-core/state"
12
8
        "launchpad.net/juju-core/version"
13
 
        "os"
14
 
        "os/exec"
15
 
        "path/filepath"
16
 
        "strings"
17
9
)
18
10
 
19
 
const toolPrefix = "tools/juju-"
20
 
 
21
11
// ToolsList holds a list of available tools.  Private tools take
22
12
// precedence over public tools, even if they have a lower
23
13
// version number.
30
20
// available in the given environment that have the
31
21
// given major version.
32
22
func ListTools(env Environ, majorVersion int) (*ToolsList, error) {
33
 
        private, err := listTools(env.Storage(), majorVersion)
34
 
        if err != nil {
 
23
        private, err := tools.ReadList(env.Storage(), majorVersion)
 
24
        if err != nil && err != tools.ErrNoMatches {
35
25
                return nil, err
36
26
        }
37
 
        public, err := listTools(env.PublicStorage(), majorVersion)
38
 
        if err != nil {
 
27
        public, err := tools.ReadList(env.PublicStorage(), majorVersion)
 
28
        if err != nil && err != tools.ErrNoMatches {
39
29
                return nil, err
40
30
        }
41
31
        return &ToolsList{
44
34
        }, nil
45
35
}
46
36
 
47
 
// listTools is like ListTools, but only returns the tools from
48
 
// a particular storage.
49
 
func listTools(store StorageReader, majorVersion int) (tools.List, error) {
50
 
        dir := fmt.Sprintf("%s%d.", toolPrefix, majorVersion)
51
 
        log.Debugf("listing tools in dir: %s", dir)
52
 
        names, err := store.List(dir)
53
 
        if err != nil {
54
 
                return nil, err
55
 
        }
56
 
        var toolsList tools.List
57
 
        for _, name := range names {
58
 
                log.Debugf("looking at tools file %s", name)
59
 
                if !strings.HasPrefix(name, toolPrefix) || !strings.HasSuffix(name, ".tgz") {
60
 
                        log.Warningf("environs: unexpected tools file found %q", name)
61
 
                        continue
62
 
                }
63
 
                vers := name[len(toolPrefix) : len(name)-len(".tgz")]
64
 
                var t state.Tools
65
 
                t.Binary, err = version.ParseBinary(vers)
66
 
                if err != nil {
67
 
                        log.Warningf("environs: failed to parse %q: %v", vers, err)
68
 
                        continue
69
 
                }
70
 
                if t.Major != majorVersion {
71
 
                        log.Warningf("environs: tool %q found in wrong directory %q", name, dir)
72
 
                        continue
73
 
                }
74
 
                t.URL, err = store.URL(name)
75
 
                log.Debugf("tools URL is %s", t.URL)
76
 
                if err != nil {
77
 
                        log.Warningf("environs: cannot get URL for %q: %v", name, err)
78
 
                        continue
79
 
                }
80
 
                toolsList = append(toolsList, &t)
81
 
        }
82
 
        return toolsList, nil
83
 
}
84
 
 
85
 
// PutTools builds whatever version of launchpad.net/juju-core is in $GOPATH,
86
 
// uploads it to the given storage, and returns a Tools instance describing
87
 
// them. If forceVersion is not nil, the uploaded tools bundle will report
88
 
// the given version number; if any fakeSeries are supplied, additional copies
89
 
// of the built tools will be uploaded for use by machines of those series.
90
 
// Juju tools built for one series do not necessarily run on another, but this
91
 
// func exists only for development use cases.
92
 
func PutTools(storage Storage, forceVersion *version.Number, fakeSeries ...string) (*state.Tools, error) {
93
 
        // TODO(rog) find binaries from $PATH when not using a development
94
 
        // version of juju within a $GOPATH.
95
 
 
96
 
        // We create the entire archive before asking the environment to
97
 
        // start uploading so that we can be sure we have archived
98
 
        // correctly.
99
 
        f, err := ioutil.TempFile("", "juju-tgz")
100
 
        if err != nil {
101
 
                return nil, err
102
 
        }
103
 
        defer f.Close()
104
 
        defer os.Remove(f.Name())
105
 
        toolsVersion, err := bundleTools(f, forceVersion)
106
 
        if err != nil {
107
 
                return nil, err
108
 
        }
109
 
        fi, err := f.Stat()
110
 
        if err != nil {
111
 
                return nil, fmt.Errorf("cannot stat newly made tools archive: %v", err)
112
 
        }
113
 
        size := fi.Size()
114
 
        log.Infof("environs: built tools %v (%dkB)", toolsVersion, (size+512)/1024)
115
 
        putTools := func(vers version.Binary) (string, error) {
116
 
                if _, err := f.Seek(0, 0); err != nil {
117
 
                        return "", fmt.Errorf("cannot seek to start of tools archive: %v", err)
118
 
                }
119
 
                path := ToolsStoragePath(vers)
120
 
                log.Infof("environs: putting tools %v", path)
121
 
                if err := storage.Put(path, f, size); err != nil {
122
 
                        return "", err
123
 
                }
124
 
                return path, nil
125
 
        }
126
 
        for _, series := range fakeSeries {
127
 
                if series != toolsVersion.Series {
128
 
                        fakeVersion := toolsVersion
129
 
                        fakeVersion.Series = series
130
 
                        if _, err := putTools(fakeVersion); err != nil {
131
 
                                return nil, err
132
 
                        }
133
 
                }
134
 
        }
135
 
        path, err := putTools(toolsVersion)
136
 
        if err != nil {
137
 
                return nil, err
138
 
        }
139
 
        url, err := storage.URL(path)
140
 
        if err != nil {
141
 
                return nil, err
142
 
        }
143
 
        return &state.Tools{toolsVersion, url}, nil
144
 
}
145
 
 
146
 
// archive writes the executable files found in the given directory in
147
 
// gzipped tar format to w.  An error is returned if an entry inside dir
148
 
// is not a regular executable file.
149
 
func archive(w io.Writer, dir string) (err error) {
150
 
        entries, err := ioutil.ReadDir(dir)
151
 
        if err != nil {
152
 
                return err
153
 
        }
154
 
 
155
 
        gzw := gzip.NewWriter(w)
156
 
        defer closeErrorCheck(&err, gzw)
157
 
 
158
 
        tarw := tar.NewWriter(gzw)
159
 
        defer closeErrorCheck(&err, tarw)
160
 
 
161
 
        for _, ent := range entries {
162
 
                h := tarHeader(ent)
163
 
                // ignore local umask
164
 
                if isExecutable(ent) {
165
 
                        h.Mode = 0755
166
 
                } else {
167
 
                        h.Mode = 0644
168
 
                }
169
 
                err := tarw.WriteHeader(h)
170
 
                if err != nil {
171
 
                        return err
172
 
                }
173
 
                if err := copyFile(tarw, filepath.Join(dir, ent.Name())); err != nil {
174
 
                        return err
175
 
                }
176
 
        }
177
 
        return nil
178
 
}
179
 
 
180
 
// copyFile writes the contents of the given file to w.
181
 
func copyFile(w io.Writer, file string) error {
182
 
        f, err := os.Open(file)
183
 
        if err != nil {
184
 
                return err
185
 
        }
186
 
        defer f.Close()
187
 
        _, err = io.Copy(w, f)
188
 
        return err
189
 
}
190
 
 
191
 
// tarHeader returns a tar file header given the file's stat
192
 
// information.
193
 
func tarHeader(i os.FileInfo) *tar.Header {
194
 
        return &tar.Header{
195
 
                Typeflag:   tar.TypeReg,
196
 
                Name:       i.Name(),
197
 
                Size:       i.Size(),
198
 
                Mode:       int64(i.Mode() & 0777),
199
 
                ModTime:    i.ModTime(),
200
 
                AccessTime: i.ModTime(),
201
 
                ChangeTime: i.ModTime(),
202
 
                Uname:      "ubuntu",
203
 
                Gname:      "ubuntu",
204
 
        }
205
 
}
206
 
 
207
 
// isExecutable returns whether the given info
208
 
// represents a regular file executable by (at least) the user.
209
 
func isExecutable(i os.FileInfo) bool {
210
 
        return i.Mode()&(0100|os.ModeType) == 0100
211
 
}
212
 
 
213
 
// closeErrorCheck means that we can ensure that
214
 
// Close errors do not get lost even when we defer them,
215
 
func closeErrorCheck(errp *error, c io.Closer) {
216
 
        err := c.Close()
217
 
        if *errp == nil {
218
 
                *errp = err
219
 
        }
220
 
}
221
 
 
222
37
// BestTools returns the most recent version
223
38
// from the set of tools in the ToolsList that are
224
39
// compatible with the given version, using flags
256
71
        return bestTools
257
72
}
258
73
 
259
 
// ToolsStoragePath returns the path that is used to store and
260
 
// retrieve the given version of the juju tools in a Storage.
261
 
func ToolsStoragePath(vers version.Binary) string {
262
 
        return toolPrefix + vers.String() + ".tgz"
263
 
}
264
 
 
265
74
// ToolsSearchFlags gives options when searching
266
75
// for tools.
267
76
type ToolsSearchFlags int
300
109
        }
301
110
        return tools, nil
302
111
}
303
 
 
304
 
func setenv(env []string, val string) []string {
305
 
        prefix := val[0 : strings.Index(val, "=")+1]
306
 
        for i, eval := range env {
307
 
                if strings.HasPrefix(eval, prefix) {
308
 
                        env[i] = val
309
 
                        return env
310
 
                }
311
 
        }
312
 
        return append(env, val)
313
 
}
314
 
 
315
 
// bundleTools bundles all the current juju tools in gzipped tar
316
 
// format to the given writer.
317
 
// If forceVersion is not nil, a FORCE-VERSION file is included in
318
 
// the tools bundle so it will lie about its current version number.
319
 
func bundleTools(w io.Writer, forceVersion *version.Number) (version.Binary, error) {
320
 
        dir, err := ioutil.TempDir("", "juju-tools")
321
 
        if err != nil {
322
 
                return version.Binary{}, err
323
 
        }
324
 
        defer os.RemoveAll(dir)
325
 
 
326
 
        cmds := [][]string{
327
 
                {"go", "install", "launchpad.net/juju-core/cmd/jujud"},
328
 
                {"strip", dir + "/jujud"},
329
 
        }
330
 
        env := setenv(os.Environ(), "GOBIN="+dir)
331
 
        for _, args := range cmds {
332
 
                cmd := exec.Command(args[0], args[1:]...)
333
 
                cmd.Env = env
334
 
                out, err := cmd.CombinedOutput()
335
 
                if err != nil {
336
 
                        return version.Binary{}, fmt.Errorf("build command %q failed: %v; %s", args[0], err, out)
337
 
                }
338
 
        }
339
 
        if forceVersion != nil {
340
 
                if err := ioutil.WriteFile(filepath.Join(dir, "FORCE-VERSION"), []byte(forceVersion.String()), 0666); err != nil {
341
 
                        return version.Binary{}, err
342
 
                }
343
 
        }
344
 
        cmd := exec.Command(filepath.Join(dir, "jujud"), "version")
345
 
        out, err := cmd.CombinedOutput()
346
 
        if err != nil {
347
 
                return version.Binary{}, fmt.Errorf("cannot get version from %q: %v; %s", cmd.Args[0], err, out)
348
 
        }
349
 
        tvs := strings.TrimSpace(string(out))
350
 
        tvers, err := version.ParseBinary(tvs)
351
 
        if err != nil {
352
 
                return version.Binary{}, fmt.Errorf("invalid version %q printed by jujud", tvs)
353
 
        }
354
 
        err = archive(w, dir)
355
 
        if err != nil {
356
 
                return version.Binary{}, err
357
 
        }
358
 
        return tvers, err
359
 
}
360
 
 
361
 
// EmptyStorage holds a StorageReader object that contains nothing.
362
 
var EmptyStorage StorageReader = emptyStorage{}
363
 
 
364
 
type emptyStorage struct{}
365
 
 
366
 
func (s emptyStorage) Get(name string) (io.ReadCloser, error) {
367
 
        return nil, &NotFoundError{fmt.Errorf("file %q not found in empty storage", name)}
368
 
}
369
 
 
370
 
func (s emptyStorage) URL(string) (string, error) {
371
 
        return "", fmt.Errorf("empty storage has no URLs")
372
 
}
373
 
 
374
 
func (s emptyStorage) List(prefix string) ([]string, error) {
375
 
        return nil, nil
376
 
}