blob: 08f890d5dc690dc5b0501370ba1cf2ea467fb7c5 [file] [log] [blame]
// packages is utilities for working with Debian packages and package lists.
package packages
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"sort"
"time"
"skia.googlesource.com/buildbot.git/go/gs"
"github.com/golang/glog"
"code.google.com/p/google-api-go-client/storage/v1"
)
// Package represents a single Debian package uploaded to Google Storage.
type Package struct {
Name string // The unique name of this release package.
Hash string
UserID string
Built time.Time
Dirty bool
Note string
}
// Installed is a list of all the packages installed on a server.
//
type Installed struct {
// Names is a list of package names, of the form "{appname}/{appname}:{author}:{date}:{githash}.deb"
Names []string
// Generation is the Google Storage generation number of the config file at the time we read it.
// Use this to avoid the lost-update problem: https://cloud.google.com/storage/docs/generations-preconditions#_ReadModWrite
Generation int64
}
func safeGetTime(m map[string]string, key string) time.Time {
value := safeGet(m, key, "")
if value == "" {
return time.Time{}
}
ret, err := time.Parse("2006-01-02T15:04:05Z", value)
if err != nil {
glog.Errorf("Failed to parse metadata datatime %s: %s", value, err)
}
return ret
}
func safeGetBool(m map[string]string, key string) bool {
if safeGet(m, key, "false") == "true" {
return true
}
return false
}
func safeGet(m map[string]string, key string, def string) string {
if value, ok := m[key]; ok {
return value
} else {
return def
}
}
// PackageSlice is for sorting Packages by Built time.
type PackageSlice []*Package
func (p PackageSlice) Len() int { return len(p) }
func (p PackageSlice) Less(i, j int) bool { return p[i].Built.After(p[j].Built) }
func (p PackageSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
// AllAvailable returns all known packages for all applications uploaded to
// gs://skia-push/debs/.
func AllAvailable(store *storage.Service) (map[string][]*Package, error) {
req := store.Objects.List("skia-push").Prefix("debs")
ret := map[string][]*Package{}
for {
objs, err := req.Do()
if err != nil {
return nil, fmt.Errorf("Failed to list debian packages in Google Storage: %s", err)
}
for _, o := range objs.Items {
key := safeGet(o.Metadata, "appname", "")
if key == "" {
glog.Errorf("Debian package without proper metadata: %s", o.Name)
continue
}
p := &Package{
Name: o.Name[5:], // Strip of debs/ from the beginning.
Hash: safeGet(o.Metadata, "hash", ""),
UserID: safeGet(o.Metadata, "userid", ""),
Built: safeGetTime(o.Metadata, "datetime"),
Dirty: safeGetBool(o.Metadata, "dirty"),
Note: safeGet(o.Metadata, "note", ""),
}
if _, ok := ret[key]; !ok {
ret[key] = []*Package{}
}
ret[key] = append(ret[key], p)
}
if objs.NextPageToken == "" {
break
}
req.PageToken(objs.NextPageToken)
}
for _, value := range ret {
sort.Sort(PackageSlice(value))
}
return ret, nil
}
// InstalledForServer returns a list of package names of installed packages for
// the given server.
func InstalledForServer(client *http.Client, store *storage.Service, serverName string) (*Installed, error) {
filename := "server/" + serverName + ".json"
obj, err := store.Objects.Get("skia-push", filename).Do()
if err != nil {
return nil, fmt.Errorf("Failed to retrieve Google Storage metadata about packages file %q: %s", filename, err)
}
glog.Infof("Fetching: %s", obj.MediaLink)
req, err := gs.RequestForStorageURL(obj.MediaLink)
if err != nil {
return nil, fmt.Errorf("Failed to construct request object for media: %s", err)
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("Failed to retrieve packages file: %s", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("Wrong status code: %#v", *resp)
}
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
value := []string{}
if err := dec.Decode(&value); err != nil {
return nil, fmt.Errorf("Failed to decode packages file: %s", err)
}
sort.Strings(value)
return &Installed{
Names: value,
Generation: obj.Generation,
}, nil
}
// AllInstalled returns a map of all known server names to their list of installed package names.
func AllInstalled(client *http.Client, store *storage.Service, names []string) (map[string]*Installed, error) {
ret := map[string]*Installed{}
for _, name := range names {
p, err := InstalledForServer(client, store, name)
if err != nil {
glog.Errorf("Failed to retrieve remote package list: %s", err)
continue
}
ret[name] = p
}
return ret, nil
}
// PutInstalled writes a new list of installed packages for the given server.
func PutInstalled(store *storage.Service, client *http.Client, serverName string, packages []string, generation int64) error {
b, err := json.Marshal(packages)
if err != nil {
return fmt.Errorf("Failed to encode installed packages: %s", err)
}
buf := bytes.NewBuffer(b)
_, err = store.Objects.Insert("skia-push", &storage.Object{Name: "server/" + serverName + ".json"}).Media(buf).IfGenerationMatch(generation).Do()
if err != nil {
return fmt.Errorf("Failed to write installed packages list to Google Storage for %s: %s", serverName, err)
}
return nil
}
// FromLocalFile loads a list of installed debian package names from a local file.
func FromLocalFile(filename string) ([]string, error) {
if _, err := os.Stat(filename); os.IsNotExist(err) {
return nil, nil
}
f, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("Failed to open file %s: %s", filename, err)
}
defer f.Close()
dec := json.NewDecoder(f)
value := []string{}
if err := dec.Decode(&value); err != nil {
return nil, fmt.Errorf("Failed to decode packages file: %s", err)
}
return value, nil
}
// ToLocalFile writes a list of debian packages to a local file.
func ToLocalFile(packages []string, filename string) error {
f, err := os.Create(filename)
if err != nil {
return fmt.Errorf("Failed to create file %s: %s", filename, err)
}
defer f.Close()
enc := json.NewEncoder(f)
if err := enc.Encode(packages); err != nil {
return fmt.Errorf("Failed to write %s: %s", filename, err)
}
return nil
}
// Install downloads and installs a debian package from Google Storage.
func Install(client *http.Client, store *storage.Service, name string) error {
glog.Infof("Installing: %s", name)
obj, err := store.Objects.Get("skia-push", "debs/"+name).Do()
if err != nil {
return fmt.Errorf("Failed to retrieve Google Storage metadata about debian package: %s", err)
}
req, err := gs.RequestForStorageURL(obj.MediaLink)
if err != nil {
return fmt.Errorf("Failed to construct request object for media: %s", err)
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("Failed to retrieve packages file: %s", err)
}
defer resp.Body.Close()
f, err := ioutil.TempFile("", "skia-pull")
if err != nil {
return fmt.Errorf("Failed to create tmp file: %s", err)
}
_, err = io.Copy(f, resp.Body)
f.Close()
if err != nil {
return fmt.Errorf("Failed to download file: %s", err)
}
cmd := exec.Command("sudo", "dpkg", "-i", f.Name())
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
glog.Errorf("Install package stdout: %s", out.String())
return fmt.Errorf("Failed to install package: %s", err)
}
glog.Infof("Install package stdout: %s", out.String())
return nil
}