Code

cae54437a1880aa8891090d08856559eaea98773
[sysdb/go.git] / client / client.go
1 //
2 // Copyright (C) 2014-2015 Sebastian 'tokkee' Harl <sh@tokkee.org>
3 // All rights reserved.
4 //
5 // Redistribution and use in source and binary forms, with or without
6 // modification, are permitted provided that the following conditions
7 // are met:
8 // 1. Redistributions of source code must retain the above copyright
9 //    notice, this list of conditions and the following disclaimer.
10 // 2. Redistributions in binary form must reproduce the above copyright
11 //    notice, this list of conditions and the following disclaimer in the
12 //    documentation and/or other materials provided with the distribution.
13 //
14 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15 // ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
16 // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17 // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
18 // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19 // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20 // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
21 // OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
22 // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
23 // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
24 // ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 /*
27 Package client provides a SysDB client implementation.
29 The Connect function connects to a SysDB server as the specified user:
31         c, err := client.Connect("unix:/var/run/sysdbd.sock", "username")
32         if err != nil {
33                 // handle error
34         }
35         defer c.Close()
37 Then, it can issue requests to the server:
39         major, minor, patch, extra, err := c.ServerVersion()
40         if err != nil {
41                 // handle error
42         }
43         fmt.Printf("Connected to SysDB %d.%d.%d%s\n", major, minor, patch, extra)
45 or:
47         res, err := c.Call(&proto.Message{Type: proto.ConnectionServerVersion})
48         if err != nil {
49                 // handle error
50         }
51         fmt.Printf("%v\n", res)
53 or execute queries:
55         q, err := client.QueryString("FETCH %s %s", client.Identifier(typ), name)
56         if err != nil {
57                 // handle error
58         }
59         res, err := c.Query(q)
60         if err != nil {
61                 // handle error
62         }
64         // res is one of the object types defined in the sysdb package.
65         switch typ {
66         case "host":
67                 host := res.(*sysdb.Host)
68                 // do something
69                 // ...
70         }
72 Each client maintains multiple connections to a SysDB server allowing to
73 handle multiple requests in parallel. The SysDB server is able to handle that
74 easily making it a cheap approach. The low-level Dial function creates a
75 single connection to a SysDB server allowing to perform low-level operations:
77         conn, err := client.Dial("unix:/var/run/sysdbd.sock", "username")
78         if err != nil {
79                 // handle error
80         }
81         defer conn.Close()
83 The github.com/sysdb/go/proto package provides support for handling requests
84 and responses. Use the Send and Receive functions to communicate with the
85 server:
87         m := &proto.Message{
88                 Type: proto.ConnectionQuery,
89                 Raw:  []byte{"LOOKUP hosts MATCHING attribute.architecture = 'amd64';"},
90         }
91         if err := conn.Send(m); err != nil {
92                 // handle error
93         }
94         m, err := conn.Receive()
95         if err != nil {
96                 // handle error
97         }
98         if m.Type == proto.ConnectionError {
99                 // handle failed query
100         }
101         // ...
102 */
103 package client
105 import (
106         "encoding/binary"
107         "fmt"
108         "log"
109         "runtime"
111         "github.com/sysdb/go/proto"
114 // A Client is a client for SysDB.
115 //
116 // A client may be used from multiple goroutines in parallel.
117 type Client struct {
118         conns chan *Conn
121 // Connect creates a new client connected to a SysDB server instance at the
122 // specified address using the specified user.
123 //
124 // The address may be a IP address or a UNIX domain socket, either prefixed
125 // with 'unix:' or specifying an absolute file-system path.
126 func Connect(addr, user string) (*Client, error) {
127         c := &Client{conns: make(chan *Conn, 2*runtime.NumCPU())}
129         for i := 0; i < cap(c.conns); i++ {
130                 conn, err := Dial(addr, user)
131                 if err != nil {
132                         return nil, err
133                 }
134                 c.conns <- conn
135         }
136         return c, nil
139 // Close closes a client connection. It may not be further used after calling
140 // this function.
141 //
142 // The function waits for all pending operations to finish.
143 func (c *Client) Close() {
144         for i := 0; i < cap(c.conns); i++ {
145                 conn := <-c.conns
146                 conn.Close()
147         }
148         close(c.conns)
149         c.conns = nil
152 // Call sends the specified request to the server and waits for its reply. It
153 // blocks until the full reply has been received.
154 func (c *Client) Call(req *proto.Message) (*proto.Message, error) {
155         conn := <-c.conns
156         defer func() { c.conns <- conn }()
158         err := conn.Send(req)
159         if err != nil {
160                 return nil, err
161         }
163         for {
164                 res, err := conn.Receive()
165                 switch {
166                 case err != nil:
167                         return nil, err
168                 case res.Type == proto.ConnectionError:
169                         return nil, fmt.Errorf("request failed: %s", string(res.Raw))
170                 case res.Type != proto.ConnectionLog:
171                         return res, err
172                 }
174                 if len(res.Raw) > 4 {
175                         log.Println(string(res.Raw[4:]))
176                 }
177         }
180 // ServerVersion queries and returns the version of the remote server.
181 func (c *Client) ServerVersion() (major, minor, patch int, extra string, err error) {
182         res, err := c.Call(&proto.Message{Type: proto.ConnectionServerVersion})
183         if err != nil || res.Type != proto.ConnectionOK {
184                 if err == nil {
185                         err = fmt.Errorf("SERVER_VERSION command failed with status %d", res.Type)
186                 }
187                 return 0, 0, 0, "", err
188         }
189         if len(res.Raw) < 4 {
190                 return 0, 0, 0, "", fmt.Errorf("SERVER_VERSION reply is too short")
191         }
192         version := int(binary.BigEndian.Uint32(res.Raw[:4]))
193         major = version / 10000
194         minor = version/100 - 100*major
195         patch = version - 10000*major - 100*minor
196         if len(res.Raw) > 4 {
197                 extra = string(res.Raw[4:])
198         }
199         return major, minor, patch, extra, nil
202 // vim: set tw=78 sw=4 sw=4 noexpandtab :