diff --git a/cmd/api_serve.go b/cmd/api_serve.go index eb146b8f..fae2702a 100644 --- a/cmd/api_serve.go +++ b/cmd/api_serve.go @@ -8,6 +8,7 @@ import ( "os" "github.com/smira/aptly/api" + "github.com/smira/aptly/systemd/activation" "github.com/smira/aptly/utils" "github.com/smira/commander" "github.com/smira/flag" @@ -34,8 +35,24 @@ func aptlyAPIServe(cmd *commander.Command, args []string) error { return err } - listen := context.Flags().Lookup("listen").Value.String() + // Try to recycle systemd fds for listening + listeners, err := activation.Listeners(true) + if len(listeners) > 1 { + panic("Got more than 1 listener from systemd. This is currently not supported!") + } + if err == nil && len(listeners) == 1 { + listener := listeners[0] + defer listener.Close() + fmt.Printf("\nTaking over web server at: %s (press Ctrl+C to quit)...\n", listener.Addr().String()) + err = http.Serve(listener, api.Router(context)) + if err != nil { + return fmt.Errorf("unable to serve: %s", err) + } + return nil + } + // If there are none: use the listen argument. + listen := context.Flags().Lookup("listen").Value.String() fmt.Printf("\nStarting web server at: %s (press Ctrl+C to quit)...\n", listen) listenURL, err := url.Parse(listen) @@ -70,7 +87,8 @@ func makeCmdAPIServe() *commander.Command { Long: ` Start HTTP server with aptly REST API. The server can listen to either a port or Unix domain socket. When using a socket, Aptly will fully manage the socket -file. +file. This command also supports taking over from a systemd file descriptors to +enable systemd socket activation. Example: diff --git a/man/aptly.1 b/man/aptly.1 index 3e504b35..df488671 100644 --- a/man/aptly.1 +++ b/man/aptly.1 @@ -1,7 +1,7 @@ .\" generated with Ronn/v0.7.3 .\" http://github.com/rtomayko/ronn/tree/0.7.3 . -.TH "APTLY" "1" "February 2017" "" "" +.TH "APTLY" "1" "March 2017" "" "" . .SH "NAME" \fBaptly\fR \- Debian repository management tool @@ -1685,10 +1685,13 @@ host:port for HTTP listening \fBaptly\fR \fBapi\fR \fBserve\fR . .P -Start HTTP server with aptly REST API\. The server can listen to either a port or Unix domain socket\. When using a socket, Aptly will fully manage the socket file\. +Start HTTP server with aptly REST API\. The server can listen to either a port or Unix domain socket\. When using a socket, Aptly will fully manage the socket file\. This command also supports taking over from a systemd file descriptors to enable systemd socket activation\. . .P -Example: $ aptly api serve \-listen=:8080 $ aptly api serve \-listen=unix:///tmp/aptly\.sock +Example: +. +.P +$ aptly api serve \-listen=:8080 $ aptly api serve \-listen=unix:///tmp/aptly\.sock . .P Options: diff --git a/system/t12_api/__init__.py b/system/t12_api/__init__.py index 8151d0a9..41959ec4 100644 --- a/system/t12_api/__init__.py +++ b/system/t12_api/__init__.py @@ -10,3 +10,4 @@ from .graph import * from .snapshots import * from .packages import * from .unix_socket import * +from .systemd_handover import * diff --git a/system/t12_api/systemd_handover.py b/system/t12_api/systemd_handover.py new file mode 100644 index 00000000..a8838395 --- /dev/null +++ b/system/t12_api/systemd_handover.py @@ -0,0 +1,45 @@ +import requests_unixsocket +import time +import urllib +import os.path + +from lib import BaseTest + +class SystemdAPIHandoverTest(BaseTest): + aptly_server = None + socket_path = "/tmp/_aptly_systemdapihandovertest.sock" + + def prepare(self): + # On Debian they use /lib on other systems /usr/lib. + systemd_activate = "/usr/lib/systemd/systemd-activate" + if not os.path.exists(systemd_activate): + systemd_activate = "/lib/systemd/systemd-activate" + if not os.path.exists(systemd_activate): + print("Could not find systemd-activate") + return + self.aptly_server = self._start_process("%s -l %s aptly api serve -no-lock" % + (systemd_activate, self.socket_path),) + super(SystemdAPIHandoverTest, self).prepare() + + def shutdown(self): + if self.aptly_server is not None: + self.aptly_server.terminate() + self.aptly_server.wait() + self.aptly_server = None + if os.path.exists(self.socket_path): + os.remove(self.socket_path) + super(SystemdAPIHandoverTest, self).shutdown() + + def run(self): + pass + + """ + Verify we can listen on a unix domain socket. + """ + def check(self): + if self.aptly_server == None: + print("Skipping test as we failed to setup a listener.") + return + session = requests_unixsocket.Session() + r = session.get('http+unix://%s/api/version' % urllib.quote(self.socket_path, safe='')) + self.check_equal(r.json(), {'Version': '0.9.8~dev'}) diff --git a/systemd/README.md b/systemd/README.md new file mode 100644 index 00000000..1c07e682 --- /dev/null +++ b/systemd/README.md @@ -0,0 +1,5 @@ +Partial import of https://github.com/coreos/go-systemd to avoid a build dependency on systemd-dev (which is not reasonably available on the type of Travis CI that is used - i.e. Ubuntu 14.04). + +This import only includes activation code without tests as the tests use code from another directory making them not relocatable without introducing a delta to make them pass. + +Code is Apache-2 which is equally permissive as MIT, which is used for aptly. diff --git a/systemd/activation/files.go b/systemd/activation/files.go new file mode 100644 index 00000000..c8e85fcd --- /dev/null +++ b/systemd/activation/files.go @@ -0,0 +1,52 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package activation implements primitives for systemd socket activation. +package activation + +import ( + "os" + "strconv" + "syscall" +) + +// based on: https://gist.github.com/alberts/4640792 +const ( + listenFdsStart = 3 +) + +func Files(unsetEnv bool) []*os.File { + if unsetEnv { + defer os.Unsetenv("LISTEN_PID") + defer os.Unsetenv("LISTEN_FDS") + } + + pid, err := strconv.Atoi(os.Getenv("LISTEN_PID")) + if err != nil || pid != os.Getpid() { + return nil + } + + nfds, err := strconv.Atoi(os.Getenv("LISTEN_FDS")) + if err != nil || nfds == 0 { + return nil + } + + files := make([]*os.File, 0, nfds) + for fd := listenFdsStart; fd < listenFdsStart+nfds; fd++ { + syscall.CloseOnExec(fd) + files = append(files, os.NewFile(uintptr(fd), "LISTEN_FD_"+strconv.Itoa(fd))) + } + + return files +} diff --git a/systemd/activation/listeners.go b/systemd/activation/listeners.go new file mode 100644 index 00000000..fd5dfc70 --- /dev/null +++ b/systemd/activation/listeners.go @@ -0,0 +1,60 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package activation + +import ( + "crypto/tls" + "net" +) + +// Listeners returns a slice containing a net.Listener for each matching socket type +// passed to this process. +// +// The order of the file descriptors is preserved in the returned slice. +// Nil values are used to fill any gaps. For example if systemd were to return file descriptors +// corresponding with "udp, tcp, tcp", then the slice would contain {nil, net.Listener, net.Listener} +func Listeners(unsetEnv bool) ([]net.Listener, error) { + files := Files(unsetEnv) + listeners := make([]net.Listener, len(files)) + + for i, f := range files { + if pc, err := net.FileListener(f); err == nil { + listeners[i] = pc + } + } + return listeners, nil +} + +// TLSListeners returns a slice containing a net.listener for each matching TCP socket type +// passed to this process. +// It uses default Listeners func and forces TCP sockets handlers to use TLS based on tlsConfig. +func TLSListeners(unsetEnv bool, tlsConfig *tls.Config) ([]net.Listener, error) { + listeners, err := Listeners(unsetEnv) + + if listeners == nil || err != nil { + return nil, err + } + + if tlsConfig != nil && err == nil { + for i, l := range listeners { + // Activate TLS only for TCP sockets + if l.Addr().Network() == "tcp" { + listeners[i] = tls.NewListener(l, tlsConfig) + } + } + } + + return listeners, err +} diff --git a/systemd/activation/packetconns.go b/systemd/activation/packetconns.go new file mode 100644 index 00000000..48b2ca02 --- /dev/null +++ b/systemd/activation/packetconns.go @@ -0,0 +1,37 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package activation + +import ( + "net" +) + +// PacketConns returns a slice containing a net.PacketConn for each matching socket type +// passed to this process. +// +// The order of the file descriptors is preserved in the returned slice. +// Nil values are used to fill any gaps. For example if systemd were to return file descriptors +// corresponding with "udp, tcp, udp", then the slice would contain {net.PacketConn, nil, net.PacketConn} +func PacketConns(unsetEnv bool) ([]net.PacketConn, error) { + files := Files(unsetEnv) + conns := make([]net.PacketConn, len(files)) + + for i, f := range files { + if pc, err := net.FilePacketConn(f); err == nil { + conns[i] = pc + } + } + return conns, nil +}