Fix version strings to comply with PEP-440. No issue #
[arvados.git] / services / keepstore / keepstore.go
1 package main
2
3 import (
4         "bytes"
5         "flag"
6         "fmt"
7         "io/ioutil"
8         "log"
9         "net"
10         "net/http"
11         "os"
12         "os/signal"
13         "strings"
14         "syscall"
15         "time"
16 )
17
18 // ======================
19 // Configuration settings
20 //
21 // TODO(twp): make all of these configurable via command line flags
22 // and/or configuration file settings.
23
24 // Default TCP address on which to listen for requests.
25 // Initialized by the --listen flag.
26 const DEFAULT_ADDR = ":25107"
27
28 // A Keep "block" is 64MB.
29 const BLOCKSIZE = 64 * 1024 * 1024
30
31 // A Keep volume must have at least MIN_FREE_KILOBYTES available
32 // in order to permit writes.
33 const MIN_FREE_KILOBYTES = BLOCKSIZE / 1024
34
35 var PROC_MOUNTS = "/proc/mounts"
36
37 // enforce_permissions controls whether permission signatures
38 // should be enforced (affecting GET and DELETE requests).
39 // Initialized by the --enforce-permissions flag.
40 var enforce_permissions bool
41
42 // permission_ttl is the time duration for which new permission
43 // signatures (returned by PUT requests) will be valid.
44 // Initialized by the --permission-ttl flag.
45 var permission_ttl time.Duration
46
47 // data_manager_token represents the API token used by the
48 // Data Manager, and is required on certain privileged operations.
49 // Initialized by the --data-manager-token-file flag.
50 var data_manager_token string
51
52 // never_delete can be used to prevent the DELETE handler from
53 // actually deleting anything.
54 var never_delete = false
55
56 // ==========
57 // Error types.
58 //
59 type KeepError struct {
60         HTTPCode int
61         ErrMsg   string
62 }
63
64 var (
65         BadRequestError     = &KeepError{400, "Bad Request"}
66         UnauthorizedError   = &KeepError{401, "Unauthorized"}
67         CollisionError      = &KeepError{500, "Collision"}
68         RequestHashError    = &KeepError{422, "Hash mismatch in request"}
69         PermissionError     = &KeepError{403, "Forbidden"}
70         DiskHashError       = &KeepError{500, "Hash mismatch in stored data"}
71         ExpiredError        = &KeepError{401, "Expired permission signature"}
72         NotFoundError       = &KeepError{404, "Not Found"}
73         GenericError        = &KeepError{500, "Fail"}
74         FullError           = &KeepError{503, "Full"}
75         TooLongError        = &KeepError{504, "Timeout"}
76         MethodDisabledError = &KeepError{405, "Method disabled"}
77 )
78
79 func (e *KeepError) Error() string {
80         return e.ErrMsg
81 }
82
83 // ========================
84 // Internal data structures
85 //
86 // These global variables are used by multiple parts of the
87 // program. They are good candidates for moving into their own
88 // packages.
89
90 // The Keep VolumeManager maintains a list of available volumes.
91 // Initialized by the --volumes flag (or by FindKeepVolumes).
92 var KeepVM VolumeManager
93
94 // The pull list manager and trash queue are threadsafe queues which
95 // support atomic update operations. The PullHandler and TrashHandler
96 // store results from Data Manager /pull and /trash requests here.
97 //
98 // See the Keep and Data Manager design documents for more details:
99 // https://arvados.org/projects/arvados/wiki/Keep_Design_Doc
100 // https://arvados.org/projects/arvados/wiki/Data_Manager_Design_Doc
101 //
102 var pullq *WorkQueue
103 var trashq *WorkQueue
104
105 // TODO(twp): continue moving as much code as possible out of main
106 // so it can be effectively tested. Esp. handling and postprocessing
107 // of command line flags (identifying Keep volumes and initializing
108 // permission arguments).
109
110 func main() {
111         log.Println("Keep started: pid", os.Getpid())
112
113         // Parse command-line flags:
114         //
115         // -listen=ipaddr:port
116         //    Interface on which to listen for requests. Use :port without
117         //    an ipaddr to listen on all network interfaces.
118         //    Examples:
119         //      -listen=127.0.0.1:4949
120         //      -listen=10.0.1.24:8000
121         //      -listen=:25107 (to listen to port 25107 on all interfaces)
122         //
123         // -volumes
124         //    A comma-separated list of directories to use as Keep volumes.
125         //    Example:
126         //      -volumes=/var/keep01,/var/keep02,/var/keep03/subdir
127         //
128         //    If -volumes is empty or is not present, Keep will select volumes
129         //    by looking at currently mounted filesystems for /keep top-level
130         //    directories.
131
132         var (
133                 data_manager_token_file string
134                 listen                  string
135                 permission_key_file     string
136                 permission_ttl_sec      int
137                 serialize_io            bool
138                 volumearg               string
139                 pidfile                 string
140         )
141         flag.StringVar(
142                 &data_manager_token_file,
143                 "data-manager-token-file",
144                 "",
145                 "File with the API token used by the Data Manager. All DELETE "+
146                         "requests or GET /index requests must carry this token.")
147         flag.BoolVar(
148                 &enforce_permissions,
149                 "enforce-permissions",
150                 false,
151                 "Enforce permission signatures on requests.")
152         flag.StringVar(
153                 &listen,
154                 "listen",
155                 DEFAULT_ADDR,
156                 "Interface on which to listen for requests, in the format "+
157                         "ipaddr:port. e.g. -listen=10.0.1.24:8000. Use -listen=:port "+
158                         "to listen on all network interfaces.")
159         flag.BoolVar(
160                 &never_delete,
161                 "never-delete",
162                 false,
163                 "If set, nothing will be deleted. HTTP 405 will be returned "+
164                         "for valid DELETE requests.")
165         flag.StringVar(
166                 &permission_key_file,
167                 "permission-key-file",
168                 "",
169                 "File containing the secret key for generating and verifying "+
170                         "permission signatures.")
171         flag.IntVar(
172                 &permission_ttl_sec,
173                 "permission-ttl",
174                 1209600,
175                 "Expiration time (in seconds) for newly generated permission "+
176                         "signatures.")
177         flag.BoolVar(
178                 &serialize_io,
179                 "serialize",
180                 false,
181                 "If set, all read and write operations on local Keep volumes will "+
182                         "be serialized.")
183         flag.StringVar(
184                 &volumearg,
185                 "volumes",
186                 "",
187                 "Comma-separated list of directories to use for Keep volumes, "+
188                         "e.g. -volumes=/var/keep1,/var/keep2. If empty or not "+
189                         "supplied, Keep will scan mounted filesystems for volumes "+
190                         "with a /keep top-level directory.")
191
192         flag.StringVar(
193                 &pidfile,
194                 "pid",
195                 "",
196                 "Path to write pid file")
197
198         flag.Parse()
199
200         // Look for local keep volumes.
201         var keepvols []string
202         if volumearg == "" {
203                 // TODO(twp): decide whether this is desirable default behavior.
204                 // In production we may want to require the admin to specify
205                 // Keep volumes explicitly.
206                 keepvols = FindKeepVolumes()
207         } else {
208                 keepvols = strings.Split(volumearg, ",")
209         }
210
211         // Check that the specified volumes actually exist.
212         var goodvols []Volume = nil
213         for _, v := range keepvols {
214                 if _, err := os.Stat(v); err == nil {
215                         log.Println("adding Keep volume:", v)
216                         newvol := MakeUnixVolume(v, serialize_io)
217                         goodvols = append(goodvols, &newvol)
218                 } else {
219                         log.Printf("bad Keep volume: %s\n", err)
220                 }
221         }
222
223         if len(goodvols) == 0 {
224                 log.Fatal("could not find any keep volumes")
225         }
226
227         // Initialize data manager token and permission key.
228         // If these tokens are specified but cannot be read,
229         // raise a fatal error.
230         if data_manager_token_file != "" {
231                 if buf, err := ioutil.ReadFile(data_manager_token_file); err == nil {
232                         data_manager_token = strings.TrimSpace(string(buf))
233                 } else {
234                         log.Fatalf("reading data manager token: %s\n", err)
235                 }
236         }
237         if permission_key_file != "" {
238                 if buf, err := ioutil.ReadFile(permission_key_file); err == nil {
239                         PermissionSecret = bytes.TrimSpace(buf)
240                 } else {
241                         log.Fatalf("reading permission key: %s\n", err)
242                 }
243         }
244
245         // Initialize permission TTL
246         permission_ttl = time.Duration(permission_ttl_sec) * time.Second
247
248         // If --enforce-permissions is true, we must have a permission key
249         // to continue.
250         if PermissionSecret == nil {
251                 if enforce_permissions {
252                         log.Fatal("--enforce-permissions requires a permission key")
253                 } else {
254                         log.Println("Running without a PermissionSecret. Block locators " +
255                                 "returned by this server will not be signed, and will be rejected " +
256                                 "by a server that enforces permissions.")
257                         log.Println("To fix this, run Keep with --permission-key-file=<path> " +
258                                 "to define the location of a file containing the permission key.")
259                 }
260         }
261
262         // Start a round-robin VolumeManager with the volumes we have found.
263         KeepVM = MakeRRVolumeManager(goodvols)
264
265         // Tell the built-in HTTP server to direct all requests to the REST router.
266         loggingRouter := MakeLoggingRESTRouter()
267         http.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
268                 loggingRouter.ServeHTTP(resp, req)
269         })
270
271         // Set up a TCP listener.
272         listener, err := net.Listen("tcp", listen)
273         if err != nil {
274                 log.Fatal(err)
275         }
276
277         // Shut down the server gracefully (by closing the listener)
278         // if SIGTERM is received.
279         term := make(chan os.Signal, 1)
280         go func(sig <-chan os.Signal) {
281                 s := <-sig
282                 log.Println("caught signal:", s)
283                 listener.Close()
284         }(term)
285         signal.Notify(term, syscall.SIGTERM)
286
287         if pidfile != "" {
288                 f, err := os.Create(pidfile)
289                 if err == nil {
290                         fmt.Fprint(f, os.Getpid())
291                         f.Close()
292                 } else {
293                         log.Printf("Error writing pid file (%s): %s", pidfile, err.Error())
294                 }
295         }
296
297         // Start listening for requests.
298         srv := &http.Server{Addr: listen}
299         srv.Serve(listener)
300
301         log.Println("shutting down")
302
303         if pidfile != "" {
304                 os.Remove(pidfile)
305         }
306 }