diff --git a/citadm.go b/citadm.go index d8e648e..a1ff1d7 100644 --- a/citadm.go +++ b/citadm.go @@ -1,5 +1,5 @@ /* -Copyright 2015 The GoStor Authors All rights reserved. +Copyright 2016 The GoStor Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,183 +18,46 @@ limitations under the License. package main import ( - "flag" "fmt" + "net/http" "os" - "github.com/gostor/gotgt/pkg/version" + "github.com/docker/go-connections/sockets" + "github.com/gostor/gotgt/cmd" + "github.com/gostor/gotgt/pkg/api/client" ) -type AdminMode int -type AdminOperation int - -const ( - OP_NEW = iota - OP_DELETE - OP_SHOW - OP_BIND - OP_UNBIND - OP_UPDATE - OP_STATS - OP_START - OP_STOP -) - -const ( - MODE_SYSTEM = iota - MODE_TARGET - MODE_DEVICE - MODE_PORTAL - MODE_LLD - - MODE_SESSION - MODE_CONNECTION - MODE_ACCOUNT -) - -type AdminRequest struct { - Mode AdminMode - Operation AdminOperation - LLD string - Length uint32 - TID int32 - SID uint64 - Lun uint64 - Cid uint64 - host_no uint32 - device_type uint32 - ac_dir uint32 - pack uint32 - force uint32 -} - func main() { - // define options - //var req AdminRequest - flDebug := flag.Bool("debug", false, "Debug mode") - flHelp := flag.Bool("help", false, "Help Message") - flVersion := flag.Bool("version", false, "Version message") - flLLD := flag.String("lld", "", "Low level device") - flOperation := flag.String("op", "", "Operation") - flMode := flag.String("mode", "", "") - flTID := flag.String("tid", "", "") - flSID := flag.String("sid", "", "") - flCID := flag.String("cid", "", "") - flLUN := flag.String("lun", "", "") - flName := flag.String("name", "", "") - flValue := flag.String("value", "", "") - flBS := flag.String("backing-store", "", "") - flTarget := flag.String("target", "", "") - flInitiatorName := flag.String("initiator-name", "", "") - flInitiatorAddress := flag.String("initiator-address", "", "") - flUser := flag.String("user", "", "") - flPassword := flag.String("password", "", "") - flHost := flag.String("host", "", "") - flForce := flag.Bool("force", false, "") - flDeviceType := flag.String("devicetype", "", "") - - flag.Usage = func() { usage(0) } - flag.Parse() - if *flHelp { - usage(0) - } - if *flVersion { - showVersion() + host := "tcp://127.0.0.1:23457" + httpClient, err := newHTTPClient(host) + if err != nil { + fmt.Fprintf(os.Stderr, "%v", err) + os.Exit(1) } - _ = flDebug - _ = flLLD - _ = flOperation - _ = flMode - _ = flTID - _ = flSID - _ = flCID - _ = flLUN - _ = flName - _ = flValue - _ = flBS - _ = flTarget - _ = flInitiatorName - _ = flInitiatorAddress - _ = flUser - _ = flPassword - _ = flHost - _ = flForce - _ = flDeviceType + cli, err := client.NewClient(host, "0.1", httpClient, nil) + if err != nil { + fmt.Fprintf(os.Stderr, "%v", err) + os.Exit(1) + } + if err := cmd.NewCommand(cli).Execute(); err != nil { + fmt.Println(err) + os.Exit(-1) + } } -func usage(status int) { - if status != 0 { - fmt.Fprintf(os.Stderr, "Try `%s --help' for more information.\n", os.Args[0]) - os.Exit(status) +func newHTTPClient(host string) (*http.Client, error) { + tr := &http.Transport{ + TLSClientConfig: nil, + } + proto, addr, _, err := client.ParseHost(host) + if err != nil { + return nil, err } - var helpMessage = `Linux SCSI Target administration utility, version %s -Usage: %s [OPTIONS] + sockets.ConfigureTransport(tr, proto, addr) -Application Options: - --lld --mode target --op new --tid --targetname - add a new target with and . must not be zero. - --lld --mode target --op delete [--force] --tid - delete the specific target with . - With force option, the specific target is deleted - even if there is an activity. - --lld --mode target --op show - show all the targets. - --lld --mode target --op show --tid - show the specific target's parameters. - --lld --mode target --op update --tid --name --value - change the target parameters of the target with . - --lld --mode target --op bind --tid --initiator-address
- --lld --mode target --op bind --tid --initiator-name - enable the target to accept the specific initiators. - --lld --mode target --op unbind --tid --initiator-address
- --lld --mode target --op unbind --tid --initiator-name - disable the specific permitted initiators. - --lld --mode logicalunit --op new --tid --lun - --backing-store --bstype --bsopts --bsoflags - add a new logical unit with to the specific - target with . The logical unit is offered - to the initiators. must be block device files - (including LVM and RAID devices) or regular files. - bstype option is optional. - bsopts are specific to the bstype. - bsoflags supported options are sync and direct - (sync:direct for both). - --lld --mode logicalunit --op delete --tid --lun - delete the specific logical unit with that - the target with has. - --lld --mode account --op new --user --password - add a new account with and . - --lld --mode account --op delete --user - delete the specific account having . - --lld --mode account --op bind --tid --user [--outgoing] - add the specific account having to - the specific target with . - could be or . - If you use --outgoing option, the account will - be added as an outgoing account. - --lld --mode account --op unbind --tid --user [--outgoing] - delete the specific account having from specific - target. The --outgoing option must be added if you - delete an outgoing account. - --lld --mode lld --op start - Start the specified lld without restarting the tgtd process. - --control-port use control port - -Help Options: - --help - display this help and exit - -Report bugs via . - -` - - fmt.Printf(helpMessage, version.VERSION, os.Args[0]) - os.Exit(0) -} - -func showVersion() { - fmt.Printf("%s\n", version.VERSION) - os.Exit(0) + return &http.Client{ + Transport: tr, + }, nil } diff --git a/citd.go b/citd.go index ef2bfed..2786221 100644 --- a/citd.go +++ b/citd.go @@ -1,5 +1,5 @@ /* -Copyright 2015 The GoStor Authors All rights reserved. +Copyright 2016 The GoStor Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,70 +18,104 @@ limitations under the License. package main import ( - "net" + "flag" + "fmt" "os" + "os/signal" "reflect" + "runtime" + "strings" + "syscall" "github.com/golang/glog" - "github.com/gostor/gotgt/pkg/api" + "github.com/gostor/gotgt/pkg/apiserver" + "github.com/gostor/gotgt/pkg/port" + _ "github.com/gostor/gotgt/pkg/port/iscsit" "github.com/gostor/gotgt/pkg/scsi" + _ "github.com/gostor/gotgt/pkg/scsi/backingstore" ) func main() { - l, err := net.Listen("tcp", ":3260") + + flHelp := flag.Bool("help", false, "Print help message for Hyperd daemon") + flHost := flag.String("host", "tcp://127.0.0.1:23457", "Host for SCSI target daemon") + flDriver := flag.String("driver", "iscsi", "SCSI low level driver") + flag.Usage = func() { *flHelp = true } + flag.Parse() + flag.Set("logtostderr", "true") + if *flHelp == true { + fmt.Println(`Usage: + xxxd [OPTIONS] + +Application Options: + --host="" Host for SCSI target daemon + --driver=iscsi SCSI low level driver + +Help Options: + -h, --help Show this help message +`) + return + } + + scsi := scsi.NewSCSITargetService() + t, err := port.NewTargetService(*flDriver, scsi) if err != nil { glog.Error(err) os.Exit(1) } - defer l.Close() - t, err := scsi.NewTarget(0, "iscsi", "test-iscsi-target") - if err != nil { - glog.Error(err) - os.Exit(1) + iscsit := reflect.ValueOf(t) + // create a new target + create := iscsit.MethodByName("NewTarget") + create.Call([]reflect.Value{reflect.ValueOf("test-iscsi-target")}) + + runtime.GOMAXPROCS(runtime.NumCPU()) + // run a service + run := iscsit.MethodByName("Run") + go run.Call([]reflect.Value{}) + + serverConfig := &apiserver.Config{ + Addrs: []apiserver.Addr{}, } - conns := make(map[string]net.Conn) - - for { - glog.Info("Listening ...") - conn, err := l.Accept() - checkError(err, "Accept") - glog.Info("Accepting ...") - conns[conn.RemoteAddr().String()] = conn - // start a new thread to do with this command - go Handler(conn, t) + //hosts := []string{"unix:///var/run/gotgt.sock"} + hosts := []string{} + if *flHost != "" { + hosts = append(hosts, *flHost) } -} - -func checkError(err error, info string) (res bool) { - - if err != nil { - glog.Error(info + " " + err.Error()) - return false - } - return true -} - -func Handler(conn net.Conn, tgt *api.SCSITarget) { - - glog.Infof("connection is connected from %s...\n", conn.RemoteAddr().String()) - - buf := make([]byte, 1024) - for { - lenght, err := conn.Read(buf) - if checkError(err, "Connection") == false { - conn.Close() - break + for _, protoAddr := range hosts { + protoAddrParts := strings.SplitN(protoAddr, "://", 2) + if len(protoAddrParts) != 2 { + glog.Errorf("bad format %s, expected PROTO://ADDR", protoAddr) + return } - if lenght > 0 { - buf[lenght] = 0 - } - v := reflect.ValueOf(tgt.SCSITargetDriver) - iscsit := v.MethodByName("ProcessCommand") - in := make([]reflect.Value, 1) - in[0] = reflect.ValueOf(buf[0:lenght]) - res := iscsit.Call(in)[0] - b := res.Bytes() - glog.Infof("%s\n", string(b)) - conn.Write(b) + serverConfig.Addrs = append(serverConfig.Addrs, apiserver.Addr{Proto: protoAddrParts[0], Addr: protoAddrParts[1]}) } + + s, err := apiserver.New(serverConfig) + if err != nil { + glog.Errorf(err.Error()) + return + } + s.InitRouters() + // The serve API routine never exits unless an error occurs + // We need to start it as a goroutine and wait on it so + // daemon doesn't exit + serveAPIWait := make(chan error) + go s.Wait(serveAPIWait) + + stopAll := make(chan os.Signal, 1) + signal.Notify(stopAll, syscall.SIGINT, syscall.SIGTERM) + + // Daemon is fully initialized and handling API traffic + // Wait for serve API job to complete + select { + case errAPI := <-serveAPIWait: + // If we have an error here it is unique to API (as daemonErr would have + // exited the daemon process above) + if errAPI != nil { + glog.Warningf("Shutting down due to ServeAPI error: %v", errAPI) + } + case <-stopAll: + break + } + s.Close() } diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 0000000..3539f4e --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,43 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/gostor/gotgt/pkg/api/client" + "github.com/spf13/cobra" +) + +func NewCommand(cli *client.Client) *cobra.Command { + var cmd = &cobra.Command{ + Use: "citadm", + Short: "Gotgt is a very fast and stable SCSI target framework", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + // Do Stuff Here + }, + } + cmd.AddCommand( + newCreateCommand(cli), + newRemoveCommand(cli), + newListCommand(cli), + newVersionCommand(cli), + ) + return cmd +} + +// NoArgs validate args and returns an error if there are any args +func NoArgs(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return nil + } + + if cmd.HasSubCommands() { + return fmt.Errorf("\n" + strings.TrimRight(cmd.UsageString(), "\n")) + } + + return fmt.Errorf( + "\"%s\" accepts no argument(s).\n", + cmd.CommandPath(), + ) +} diff --git a/cmd/create.go b/cmd/create.go new file mode 100644 index 0000000..aaf0e07 --- /dev/null +++ b/cmd/create.go @@ -0,0 +1,95 @@ +/* +Copyright 2016 The GoStor Authors All rights reserved. + +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 cmd + +import ( + "fmt" + + "github.com/gostor/gotgt/pkg/api" + "github.com/gostor/gotgt/pkg/api/client" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +func newCreateCommand(cli *client.Client) *cobra.Command { + var cmd = &cobra.Command{ + Use: "create", + Short: "Create a new object", + Long: `All software has versions. This is Gotgt 's`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println(cmd.UsageString()) + }, + } + cmd.AddCommand( + newCreateTargetCmd(cli), + newCreateLuCmd(cli), + ) + return cmd +} + +func newCreateTargetCmd(cli *client.Client) *cobra.Command { + opts := api.TargetCreateRequest{} + var cmd = &cobra.Command{ + Use: "target", + Short: "Create a new target into gotgt", + Long: `All software has versions. This is Gotgt 's`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + return fmt.Errorf( + "\"%s\" accepts no argument(s).\n", + cmd.CommandPath(), + ) + } + return createTarget(cli, opts) + }, + } + flags := cmd.Flags() + flags.StringVar(&opts.Name, "name", "", "Specify target name") + + return cmd + +} + +func newCreateLuCmd(cli *client.Client) *cobra.Command { + var cmd = &cobra.Command{ + Use: "lu", + Short: "Create a new Lu into gotgt", + Long: `All software has versions. This is Gotgt 's`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := NoArgs(cmd, args); err != nil { + return err + } + return createLu(cli) + }, + } + flags := cmd.Flags() + _ = flags + return cmd +} + +func createTarget(cli *client.Client, opts api.TargetCreateRequest) error { + tgt, err := cli.TargetCreate(context.Background(), opts) + if err != nil { + return err + } + fmt.Printf("Target %s successfully created\n", tgt.Name) + return nil +} + +func createLu(cli *client.Client) error { + return nil +} diff --git a/cmd/list.go b/cmd/list.go new file mode 100644 index 0000000..d752629 --- /dev/null +++ b/cmd/list.go @@ -0,0 +1,108 @@ +/* +Copyright 2016 The GoStor Authors All rights reserved. + +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 cmd + +import ( + "fmt" + "os" + "text/tabwriter" + + "github.com/gostor/gotgt/pkg/api" + "github.com/gostor/gotgt/pkg/api/client" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +func newListCommand(cli *client.Client) *cobra.Command { + var cmd = &cobra.Command{ + Use: "list", + Short: "List object(s)", + Long: `All software has versions. This is Gotgt 's`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println(cmd.UsageString()) + }, + } + cmd.AddCommand( + newListTargetCmd(cli), + newListLuCmd(cli), + ) + return cmd +} + +func newListTargetCmd(cli *client.Client) *cobra.Command { + opts := api.TargetListOptions{} + var cmd = &cobra.Command{ + Use: "target", + Short: "List target(s) of gotgt", + Long: `All software has versions. This is Gotgt 's`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + return fmt.Errorf( + "\"%s\" accepts no argument(s).\n", + cmd.CommandPath(), + ) + } + return listTarget(cli, opts) + }, + } + flags := cmd.Flags() + flags.StringVar(&opts.Name, "name", "", "Specify target name") + flags.BoolVar(&opts.Verbose, "verbose", false, "Show more details") + + return cmd + +} + +func newListLuCmd(cli *client.Client) *cobra.Command { + var cmd = &cobra.Command{ + Use: "lu", + Short: "List Lu(s) of gotgt", + Long: `All software has versions. This is Gotgt 's`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := NoArgs(cmd, args); err != nil { + return err + } + return listLu(cli) + }, + } + flags := cmd.Flags() + _ = flags + return cmd +} + +func listTarget(cli *client.Client, opts api.TargetListOptions) error { + results, err := cli.TargetList(context.Background(), opts) + if err != nil { + return err + } + + w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0) + fmt.Fprintln(w, "TARGET NAME\tSTATE\tSESSIONS") + for _, tgt := range results { + status := "online" + if tgt.State == api.TargetReady { + status = "ready" + } + fmt.Fprintf(w, "%s\t%s\t%d\n", tgt.Name, status, len(tgt.ITNexus)) + } + w.Flush() + return nil +} + +func listLu(cli *client.Client) error { + return nil +} diff --git a/cmd/remove.go b/cmd/remove.go new file mode 100644 index 0000000..fa89495 --- /dev/null +++ b/cmd/remove.go @@ -0,0 +1,87 @@ +/* +Copyright 2016 The GoStor Authors All rights reserved. + +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 cmd + +import ( + "fmt" + + "github.com/gostor/gotgt/pkg/api" + "github.com/gostor/gotgt/pkg/api/client" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +func newRemoveCommand(cli *client.Client) *cobra.Command { + var cmd = &cobra.Command{ + Use: "rm", + Short: "remove a new object", + Long: `All software has versions. This is Gotgt 's`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println(cmd.UsageString()) + }, + } + cmd.AddCommand( + newRemoveTargetCmd(cli), + newRemoveLuCmd(cli), + ) + return cmd +} + +func newRemoveTargetCmd(cli *client.Client) *cobra.Command { + opts := api.TargetRemoveOptions{} + var cmd = &cobra.Command{ + Use: "target", + Short: "Remove a new target into gotgt", + Long: `All software has versions. This is Gotgt 's`, + RunE: func(cmd *cobra.Command, args []string) error { + return removeTarget(cli, opts) + }, + } + flags := cmd.Flags() + flags.StringVar(&opts.Name, "name", "", "Specify target name") + flags.BoolVar(&opts.Force, "force", false, "Specify target name") + + return cmd + +} + +func newRemoveLuCmd(cli *client.Client) *cobra.Command { + var cmd = &cobra.Command{ + Use: "lu", + Short: "Remove a new Lu into gotgt", + Long: `All software has versions. This is Gotgt 's`, + RunE: func(cmd *cobra.Command, args []string) error { + return removeLu(cli) + }, + } + flags := cmd.Flags() + _ = flags + return cmd +} + +func removeTarget(cli *client.Client, opts api.TargetRemoveOptions) error { + err := cli.TargetRemove(context.Background(), opts) + if err != nil { + return err + } + fmt.Printf("Target %s successfully removed\n", opts.Name) + return nil +} + +func removeLu(cli *client.Client) error { + return nil +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..3c3722d --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,21 @@ +package cmd + +import ( + "fmt" + + "github.com/gostor/gotgt/pkg/api/client" + "github.com/gostor/gotgt/pkg/version" + "github.com/spf13/cobra" +) + +func newVersionCommand(cli *client.Client) *cobra.Command { + var cmd = &cobra.Command{ + Use: "version", + Short: "Print the version number of gotgt", + Long: `All software has versions. This is Gotgt 's`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("Gotgt %s -- HEAD\n", version.VERSION) + }, + } + return cmd +} diff --git a/pkg/api/client/client.go b/pkg/api/client/client.go new file mode 100644 index 0000000..96e7751 --- /dev/null +++ b/pkg/api/client/client.go @@ -0,0 +1,106 @@ +package client + +import ( + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/gostor/gotgt/pkg/api/client/transport" +) + +type Client struct { + // proto holds the client protocol i.e. unix. + proto string + // addr holds the client address. + addr string + // basePath holds the path to prepend to the requests. + basePath string + // transport is the interface to send request with, it implements transport.Client. + transport transport.Client + // version of the server to talk to. + version string + // custom http headers configured by users. + customHTTPHeaders map[string]string +} + +// NewClient initializes a new API client for the given host and API version. +// It uses the given http client as transport. +// It also initializes the custom http headers to add to each request. +// +// It won't send any version information if the version number is empty. It is +// highly recommended that you set a version or your client may break if the +// server is upgraded. +func NewClient(host string, version string, client *http.Client, httpHeaders map[string]string) (*Client, error) { + proto, addr, basePath, err := ParseHost(host) + if err != nil { + return nil, err + } + + transport, err := transport.NewTransportWithHTTP(proto, addr, client) + if err != nil { + return nil, err + } + + return &Client{ + proto: proto, + addr: addr, + basePath: basePath, + transport: transport, + version: version, + customHTTPHeaders: httpHeaders, + }, nil +} + +// getAPIPath returns the versioned request path to call the api. +// It appends the query parameters to the path if they are not empty. +func (cli *Client) getAPIPath(p string, query url.Values) string { + var apiPath string + if cli.version != "" { + v := strings.TrimPrefix(cli.version, "v") + apiPath = fmt.Sprintf("%s/v%s%s", cli.basePath, v, p) + } else { + apiPath = fmt.Sprintf("%s%s", cli.basePath, p) + } + + u := &url.URL{ + Path: apiPath, + } + if len(query) > 0 { + u.RawQuery = query.Encode() + } + return u.String() +} + +// ClientVersion returns the version string associated with this +// instance of the Client. Note that this value can be changed +// via the DOCKER_API_VERSION env var. +func (cli *Client) ClientVersion() string { + return cli.version +} + +// UpdateClientVersion updates the version string associated with this +// instance of the Client. +func (cli *Client) UpdateClientVersion(v string) { + cli.version = v +} + +// ParseHost verifies that the given host strings is valid. +func ParseHost(host string) (string, string, string, error) { + protoAddrParts := strings.SplitN(host, "://", 2) + if len(protoAddrParts) == 1 { + return "", "", "", fmt.Errorf("unable to parse docker host `%s`", host) + } + + var basePath string + proto, addr := protoAddrParts[0], protoAddrParts[1] + if proto == "tcp" { + parsed, err := url.Parse("tcp://" + addr) + if err != nil { + return "", "", "", err + } + addr = parsed.Host + basePath = parsed.Path + } + return proto, addr, basePath, nil +} diff --git a/pkg/api/client/client_darwin.go b/pkg/api/client/client_darwin.go new file mode 100644 index 0000000..4b47a17 --- /dev/null +++ b/pkg/api/client/client_darwin.go @@ -0,0 +1,4 @@ +package client + +// DefaultDockerHost defines os specific default if DOCKER_HOST is unset +const DefaultDockerHost = "tcp://127.0.0.1:2375" diff --git a/pkg/api/client/client_test.go b/pkg/api/client/client_test.go new file mode 100644 index 0000000..a00b21b --- /dev/null +++ b/pkg/api/client/client_test.go @@ -0,0 +1,72 @@ +package client + +import ( + "net/url" + "testing" +) + +func TestGetAPIPath(t *testing.T) { + cases := []struct { + v string + p string + q url.Values + e string + }{ + {"", "/containers/json", nil, "/containers/json"}, + {"", "/containers/json", url.Values{}, "/containers/json"}, + {"", "/containers/json", url.Values{"s": []string{"c"}}, "/containers/json?s=c"}, + {"1.22", "/containers/json", nil, "/v1.22/containers/json"}, + {"1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"}, + {"1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"}, + {"v1.22", "/containers/json", nil, "/v1.22/containers/json"}, + {"v1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"}, + {"v1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"}, + {"v1.22", "/networks/kiwl$%^", nil, "/v1.22/networks/kiwl$%25%5E"}, + } + + for _, cs := range cases { + c, err := NewClient("unix:///var/run/docker.sock", cs.v, nil, nil) + if err != nil { + t.Fatal(err) + } + g := c.getAPIPath(cs.p, cs.q) + if g != cs.e { + t.Fatalf("Expected %s, got %s", cs.e, g) + } + } +} + +func TestParseHost(t *testing.T) { + cases := []struct { + host string + proto string + addr string + base string + err bool + }{ + {"", "", "", "", true}, + {"foobar", "", "", "", true}, + {"foo://bar", "foo", "bar", "", false}, + {"tcp://localhost:2476", "tcp", "localhost:2476", "", false}, + {"tcp://localhost:2476/path", "tcp", "localhost:2476", "/path", false}, + } + + for _, cs := range cases { + p, a, b, e := ParseHost(cs.host) + if cs.err && e == nil { + t.Fatalf("expected error, got nil") + } + if !cs.err && e != nil { + t.Fatal(e) + } + if cs.proto != p { + t.Fatalf("expected proto %s, got %s", cs.proto, p) + } + if cs.addr != a { + t.Fatalf("expected addr %s, got %s", cs.addr, a) + } + if cs.base != b { + t.Fatalf("expected base %s, got %s", cs.base, b) + } + } +} diff --git a/pkg/api/client/client_unix.go b/pkg/api/client/client_unix.go new file mode 100644 index 0000000..572c5f8 --- /dev/null +++ b/pkg/api/client/client_unix.go @@ -0,0 +1,6 @@ +// +build linux freebsd solaris openbsd + +package client + +// DefaultDockerHost defines os specific default if DOCKER_HOST is unset +const DefaultDockerHost = "unix:///var/run/docker.sock" diff --git a/pkg/api/client/request.go b/pkg/api/client/request.go new file mode 100644 index 0000000..a524e4f --- /dev/null +++ b/pkg/api/client/request.go @@ -0,0 +1,200 @@ +package client + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/gostor/gotgt/pkg/api/client/transport/cancellable" + "golang.org/x/net/context" +) + +// ErrConnectionFailed is an error raised when the connection between the client and the server failed. +var ErrConnectionFailed = errors.New("Cannot connect to the SCSI Target daemon. Is the SCSI target daemon running on this host?") + +// serverResponse is a wrapper for http API responses. +type serverResponse struct { + body io.ReadCloser + header http.Header + statusCode int +} + +// head sends an http request to the docker API using the method HEAD. +func (cli *Client) head(ctx context.Context, path string, query url.Values, headers map[string][]string) (*serverResponse, error) { + return cli.sendRequest(ctx, "HEAD", path, query, nil, headers) +} + +// getWithContext sends an http request to the docker API using the method GET with a specific go context. +func (cli *Client) get(ctx context.Context, path string, query url.Values, headers map[string][]string) (*serverResponse, error) { + return cli.sendRequest(ctx, "GET", path, query, nil, headers) +} + +// postWithContext sends an http request to the docker API using the method POST with a specific go context. +func (cli *Client) post(ctx context.Context, path string, query url.Values, obj interface{}, headers map[string][]string) (*serverResponse, error) { + return cli.sendRequest(ctx, "POST", path, query, obj, headers) +} + +func (cli *Client) postRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers map[string][]string) (*serverResponse, error) { + return cli.sendClientRequest(ctx, "POST", path, query, body, headers) +} + +// put sends an http request to the docker API using the method PUT. +func (cli *Client) put(ctx context.Context, path string, query url.Values, obj interface{}, headers map[string][]string) (*serverResponse, error) { + return cli.sendRequest(ctx, "PUT", path, query, obj, headers) +} + +// put sends an http request to the docker API using the method PUT. +func (cli *Client) putRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers map[string][]string) (*serverResponse, error) { + return cli.sendClientRequest(ctx, "PUT", path, query, body, headers) +} + +// delete sends an http request to the docker API using the method DELETE. +func (cli *Client) delete(ctx context.Context, path string, query url.Values, headers map[string][]string) (*serverResponse, error) { + return cli.sendRequest(ctx, "DELETE", path, query, nil, headers) +} + +func (cli *Client) sendRequest(ctx context.Context, method, path string, query url.Values, obj interface{}, headers map[string][]string) (*serverResponse, error) { + var body io.Reader + + if obj != nil { + var err error + body, err = encodeData(obj) + if err != nil { + return nil, err + } + if headers == nil { + headers = make(map[string][]string) + } + headers["Content-Type"] = []string{"application/json"} + } + + return cli.sendClientRequest(ctx, method, path, query, body, headers) +} + +func (cli *Client) sendClientRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers map[string][]string) (*serverResponse, error) { + serverResp := &serverResponse{ + body: nil, + statusCode: -1, + } + + expectedPayload := (method == "POST" || method == "PUT") + if expectedPayload && body == nil { + body = bytes.NewReader([]byte{}) + } + + req, err := cli.newRequest(method, path, query, body, headers) + if err != nil { + return serverResp, err + } + + if cli.proto == "unix" || cli.proto == "npipe" { + // For local communications, it doesn't matter what the host is. We just + // need a valid and meaningful host name. + req.Host = "gotgt" + } + req.URL.Host = cli.addr + req.URL.Scheme = cli.transport.Scheme() + + if expectedPayload && req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", "text/plain") + } + + resp, err := cancellable.Do(ctx, cli.transport, req) + if err != nil { + if isTimeout(err) || strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "dial unix") { + return serverResp, ErrConnectionFailed + } + + if !cli.transport.Secure() && strings.Contains(err.Error(), "malformed HTTP response") { + return serverResp, fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err) + } + + if cli.transport.Secure() && strings.Contains(err.Error(), "bad certificate") { + return serverResp, fmt.Errorf("The server probably has client authentication (--tlsverify) enabled. Please check your TLS client certification settings: %v", err) + } + + return serverResp, fmt.Errorf("An error occurred trying to connect: %v", err) + } + + if resp != nil { + serverResp.statusCode = resp.StatusCode + } + + if serverResp.statusCode < 200 || serverResp.statusCode >= 400 { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return serverResp, err + } + if len(body) == 0 { + return serverResp, fmt.Errorf("Error: request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), req.URL) + } + + var errorMessage string + errorMessage = string(body) + + return serverResp, fmt.Errorf("Error response from daemon: %s", strings.TrimSpace(errorMessage)) + } + + serverResp.body = resp.Body + serverResp.header = resp.Header + return serverResp, nil +} + +func (cli *Client) newRequest(method, path string, query url.Values, body io.Reader, headers map[string][]string) (*http.Request, error) { + apiPath := cli.getAPIPath(path, query) + req, err := http.NewRequest(method, apiPath, body) + if err != nil { + return nil, err + } + + // Add CLI Config's HTTP Headers BEFORE we set the Docker headers + // then the user can't change OUR headers + for k, v := range cli.customHTTPHeaders { + req.Header.Set(k, v) + } + + if headers != nil { + for k, v := range headers { + req.Header[k] = v + } + } + + return req, nil +} + +func encodeData(data interface{}) (*bytes.Buffer, error) { + params := bytes.NewBuffer(nil) + if data != nil { + if err := json.NewEncoder(params).Encode(data); err != nil { + return nil, err + } + } + return params, nil +} + +func ensureReaderClosed(response *serverResponse) { + if response != nil && response.body != nil { + // Drain up to 512 bytes and close the body to let the Transport reuse the connection + io.CopyN(ioutil.Discard, response.body, 512) + response.body.Close() + } +} + +func isTimeout(err error) bool { + type timeout interface { + Timeout() bool + } + e := err + switch urlErr := err.(type) { + case *url.Error: + e = urlErr.Err + } + t, ok := e.(timeout) + return ok && t.Timeout() +} diff --git a/pkg/api/client/target_create.go b/pkg/api/client/target_create.go new file mode 100644 index 0000000..626904f --- /dev/null +++ b/pkg/api/client/target_create.go @@ -0,0 +1,35 @@ +/* +Copyright 2016 The GoStor Authors All rights reserved. + +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 client + +import ( + "encoding/json" + + "github.com/gostor/gotgt/pkg/api" + "golang.org/x/net/context" +) + +// TargetCreate creates a target in the SCSI Target. +func (cli *Client) TargetCreate(ctx context.Context, options api.TargetCreateRequest) (api.SCSITarget, error) { + var target api.SCSITarget + resp, err := cli.post(ctx, "/target/create", nil, options, nil) + if err != nil { + return target, err + } + err = json.NewDecoder(resp.body).Decode(&target) + ensureReaderClosed(resp) + return target, err +} diff --git a/pkg/apiserver/doc.go b/pkg/api/client/target_delete.go similarity index 52% rename from pkg/apiserver/doc.go rename to pkg/api/client/target_delete.go index 4f00fa2..b35d833 100644 --- a/pkg/apiserver/doc.go +++ b/pkg/api/client/target_delete.go @@ -1,5 +1,5 @@ /* -Copyright 2015 The GoStor Authors All rights reserved. +Copyright 2016 The GoStor Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,5 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package apiserver contains the code that provides a rest.ful api service. -package apiserver +package client + +import ( + "net/url" + + "github.com/gostor/gotgt/pkg/api" + "golang.org/x/net/context" +) + +// TargetCreate creates a target in the SCSI Target. +func (cli *Client) TargetRemove(ctx context.Context, options api.TargetRemoveOptions) error { + query := url.Values{} + if options.Force { + query.Set("force", "1") + } + resp, err := cli.delete(ctx, "/targets/"+options.Name, query, nil) + ensureReaderClosed(resp) + return err +} diff --git a/pkg/api/client/target_list.go b/pkg/api/client/target_list.go new file mode 100644 index 0000000..2156624 --- /dev/null +++ b/pkg/api/client/target_list.go @@ -0,0 +1,40 @@ +/* +Copyright 2016 The GoStor Authors All rights reserved. + +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 client + +import ( + "encoding/json" + "net/url" + + "github.com/gostor/gotgt/pkg/api" + "golang.org/x/net/context" +) + +// TargetCreate creates a target in the SCSI Target. +func (cli *Client) TargetList(ctx context.Context, options api.TargetListOptions) ([]api.SCSITarget, error) { + var targets []api.SCSITarget + var query = url.Values{} + if options.Name != "" { + query.Set("name", options.Name) + } + resp, err := cli.get(ctx, "/target/list", query, nil) + if err != nil { + return targets, err + } + err = json.NewDecoder(resp.body).Decode(&targets) + ensureReaderClosed(resp) + return targets, err +} diff --git a/pkg/api/client/transport/LICENSE b/pkg/api/client/transport/LICENSE new file mode 100644 index 0000000..c157bff --- /dev/null +++ b/pkg/api/client/transport/LICENSE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2015-2016 Docker, 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 + + https://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. diff --git a/pkg/api/client/transport/cancellable/LICENSE b/pkg/api/client/transport/cancellable/LICENSE new file mode 100644 index 0000000..6a66aea --- /dev/null +++ b/pkg/api/client/transport/cancellable/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkg/api/client/transport/cancellable/canceler.go b/pkg/api/client/transport/cancellable/canceler.go new file mode 100644 index 0000000..ff8c612 --- /dev/null +++ b/pkg/api/client/transport/cancellable/canceler.go @@ -0,0 +1,23 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.5 + +package cancellable + +import ( + "net/http" + + "github.com/gostor/gotgt/pkg/api/client/transport" +) + +func canceler(client transport.Sender, req *http.Request) func() { + // TODO(djd): Respect any existing value of req.Cancel. + ch := make(chan struct{}) + req.Cancel = ch + + return func() { + close(ch) + } +} diff --git a/pkg/api/client/transport/cancellable/canceler_go14.go b/pkg/api/client/transport/cancellable/canceler_go14.go new file mode 100644 index 0000000..61653e4 --- /dev/null +++ b/pkg/api/client/transport/cancellable/canceler_go14.go @@ -0,0 +1,27 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !go1.5 + +package cancellable + +import ( + "net/http" + + "github.com/gostor/gotgt/pkg/api/client/transport" +) + +type requestCanceler interface { + CancelRequest(*http.Request) +} + +func canceler(client transport.Sender, req *http.Request) func() { + rc, ok := client.(requestCanceler) + if !ok { + return func() {} + } + return func() { + rc.CancelRequest(req) + } +} diff --git a/pkg/api/client/transport/cancellable/cancellable.go b/pkg/api/client/transport/cancellable/cancellable.go new file mode 100644 index 0000000..60f93bc --- /dev/null +++ b/pkg/api/client/transport/cancellable/cancellable.go @@ -0,0 +1,112 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package cancellable provides helper function to cancel http requests. +package cancellable + +import ( + "io" + "net/http" + + "github.com/gostor/gotgt/pkg/api/client/transport" + "golang.org/x/net/context" +) + +func nop() {} + +var ( + testHookContextDoneBeforeHeaders = nop + testHookDoReturned = nop + testHookDidBodyClose = nop +) + +// Do sends an HTTP request with the provided transport.Sender and returns an HTTP response. +// If the client is nil, http.DefaultClient is used. +// If the context is canceled or times out, ctx.Err() will be returned. +// +// FORK INFORMATION: +// +// This function deviates from the upstream version in golang.org/x/net/context/ctxhttp by +// taking a Sender interface rather than a *http.Client directly. That allow us to use +// this funcion with mocked clients and hijacked connections. +func Do(ctx context.Context, client transport.Sender, req *http.Request) (*http.Response, error) { + if client == nil { + client = http.DefaultClient + } + + // Request cancelation changed in Go 1.5, see canceler.go and canceler_go14.go. + cancel := canceler(client, req) + + type responseAndError struct { + resp *http.Response + err error + } + result := make(chan responseAndError, 1) + + go func() { + resp, err := client.Do(req) + testHookDoReturned() + result <- responseAndError{resp, err} + }() + + var resp *http.Response + + select { + case <-ctx.Done(): + testHookContextDoneBeforeHeaders() + cancel() + // Clean up after the goroutine calling client.Do: + go func() { + if r := <-result; r.resp != nil && r.resp.Body != nil { + testHookDidBodyClose() + r.resp.Body.Close() + } + }() + return nil, ctx.Err() + case r := <-result: + var err error + resp, err = r.resp, r.err + if err != nil { + return resp, err + } + } + + c := make(chan struct{}) + go func() { + select { + case <-ctx.Done(): + cancel() + case <-c: + // The response's Body is closed. + } + }() + resp.Body = ¬ifyingReader{resp.Body, c} + + return resp, nil +} + +// notifyingReader is an io.ReadCloser that closes the notify channel after +// Close is called or a Read fails on the underlying ReadCloser. +type notifyingReader struct { + io.ReadCloser + notify chan<- struct{} +} + +func (r *notifyingReader) Read(p []byte) (int, error) { + n, err := r.ReadCloser.Read(p) + if err != nil && r.notify != nil { + close(r.notify) + r.notify = nil + } + return n, err +} + +func (r *notifyingReader) Close() error { + err := r.ReadCloser.Close() + if r.notify != nil { + close(r.notify) + r.notify = nil + } + return err +} diff --git a/pkg/api/client/transport/client.go b/pkg/api/client/transport/client.go new file mode 100644 index 0000000..13d4b3a --- /dev/null +++ b/pkg/api/client/transport/client.go @@ -0,0 +1,47 @@ +package transport + +import ( + "crypto/tls" + "net/http" +) + +// Sender is an interface that clients must implement +// to be able to send requests to a remote connection. +type Sender interface { + // Do sends request to a remote endpoint. + Do(*http.Request) (*http.Response, error) +} + +// Client is an interface that abstracts all remote connections. +type Client interface { + Sender + // Secure tells whether the connection is secure or not. + Secure() bool + // Scheme returns the connection protocol the client uses. + Scheme() string + // TLSConfig returns any TLS configuration the client uses. + TLSConfig() *tls.Config +} + +// tlsInfo returns information about the TLS configuration. +type tlsInfo struct { + tlsConfig *tls.Config +} + +// TLSConfig returns the TLS configuration. +func (t *tlsInfo) TLSConfig() *tls.Config { + return t.tlsConfig +} + +// Scheme returns protocol scheme to use. +func (t *tlsInfo) Scheme() string { + if t.tlsConfig != nil { + return "https" + } + return "http" +} + +// Secure returns true if there is a TLS configuration. +func (t *tlsInfo) Secure() bool { + return t.tlsConfig != nil +} diff --git a/pkg/api/client/transport/transport.go b/pkg/api/client/transport/transport.go new file mode 100644 index 0000000..ff28af1 --- /dev/null +++ b/pkg/api/client/transport/transport.go @@ -0,0 +1,57 @@ +// Package transport provides function to send request to remote endpoints. +package transport + +import ( + "fmt" + "net/http" + + "github.com/docker/go-connections/sockets" +) + +// apiTransport holds information about the http transport to connect with the API. +type apiTransport struct { + *http.Client + *tlsInfo + transport *http.Transport +} + +// NewTransportWithHTTP creates a new transport based on the provided proto, address and http client. +// It uses Docker's default http transport configuration if the client is nil. +// It does not modify the client's transport if it's not nil. +func NewTransportWithHTTP(proto, addr string, client *http.Client) (Client, error) { + var transport *http.Transport + + if client != nil { + tr, ok := client.Transport.(*http.Transport) + if !ok { + return nil, fmt.Errorf("unable to verify TLS configuration, invalid transport %v", client.Transport) + } + transport = tr + } else { + transport = defaultTransport(proto, addr) + client = &http.Client{ + Transport: transport, + } + } + + return &apiTransport{ + Client: client, + tlsInfo: &tlsInfo{transport.TLSClientConfig}, + transport: transport, + }, nil +} + +// CancelRequest stops a request execution. +func (a *apiTransport) CancelRequest(req *http.Request) { + a.transport.CancelRequest(req) +} + +// defaultTransport creates a new http.Transport with Docker's +// default transport configuration. +func defaultTransport(proto, addr string) *http.Transport { + tr := new(http.Transport) + sockets.ConfigureTransport(tr, proto, addr) + return tr +} + +var _ Client = &apiTransport{} diff --git a/pkg/apiserver/api_installer.go b/pkg/api/options.go similarity index 68% rename from pkg/apiserver/api_installer.go rename to pkg/api/options.go index 4f00fa2..bf8d517 100644 --- a/pkg/apiserver/api_installer.go +++ b/pkg/api/options.go @@ -1,5 +1,5 @@ /* -Copyright 2015 The GoStor Authors All rights reserved. +Copyright 2016 The GoStor Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,5 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package apiserver contains the code that provides a rest.ful api service. -package apiserver +package api + +type TargetCreateRequest struct { + Name string +} + +type TargetRemoveOptions struct { + Name string + Force bool +} + +type TargetListOptions struct { + Name string + Verbose bool +} diff --git a/pkg/api/types.go b/pkg/api/types.go index 12430e2..d0b295d 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -3,6 +3,7 @@ package api import ( "bytes" "errors" + "os" ) type SCSICommandType byte @@ -147,7 +148,7 @@ type SCSICommand struct { TL uint32 SCB *bytes.Buffer SCBLength int - Lun []uint8 + Lun [8]uint8 Attribute int Tag uint64 Result byte @@ -156,13 +157,14 @@ type SCSICommand struct { ITNexus *ITNexus ITNexusLuInfo *ITNexusLuInfo } + type ITNexus struct { - ID uint64 - Ctime uint64 - Commands []SCSICommand - Target *SCSITarget - Host int - Info string + ID uint64 `json:"id"` + Ctime uint64 `json:"ctime"` + Commands []SCSICommand `json:"-"` + Target *SCSITarget `json:"-"` + Host int `json:"host"` + Info string `json:"info"` } type ITNexusLuInfo struct { @@ -172,14 +174,14 @@ type ITNexusLuInfo struct { } type SCSITarget struct { - Name string - TID int - LID int - State SCSITargetState - Devices []SCSILu - ITNexus []ITNexus + Name string `json:"name"` + TID int `json:"tid"` + LID int `json:"lid"` + State SCSITargetState `json:"state"` + Devices []*SCSILu `json:"-"` + ITNexus []*ITNexus `json:"itnexus"` - SCSITargetDriver interface{} + SCSITargetDriver interface{} `json:"-"` } type SCSITargetDriverState int @@ -210,7 +212,7 @@ type SCSILuPhyAttribute struct { ProductRev string VersionDesction []uint16 // Peripheral device type - DeviceType uint + DeviceType SCSIDeviceType // Peripheral Qualifier Qualifier bool // Removable media @@ -234,8 +236,8 @@ type SCSILuPhyAttribute struct { } var ( - DefaultBlockShift int = 9 - DefaultSenseBufferSize int = 252 + DefaultBlockShift uint = 9 + DefaultSenseBufferSize int = 252 ) var ( @@ -294,8 +296,32 @@ var ( TYPE_PT SCSIDeviceType = 0xff ) +type CommandFunc func(host int, cmd *SCSICommand) SAMStat + +type BackingStore interface { + Open(dev *SCSILu, path string) (*os.File, error) + Close(dev *SCSILu) error + Init(dev *SCSILu, Opts string) error + Exit(dev *SCSILu) error + CommandSubmit(cmd *SCSICommand) error +} + +type SCSIDeviceProtocol interface { + PerformCommand(opcode int) interface{} + InitLu(lu *SCSILu) error + ConfigLu(lu *SCSILu) error + OnlineLu(lu *SCSILu) error + OfflineLu(lu *SCSILu) error + ExitLu(lu *SCSILu) error +} +type ModePage struct { + Pcode uint8 // Page code + SubPcode uint8 // Sub page code + Data []byte // Rest of mode page info +} + type SCSILu struct { - FD int + File *os.File Address uint64 Size uint64 Lun uint64 @@ -304,4 +330,12 @@ type SCSILu struct { BlockShift uint ReserveID uint64 Attrs SCSILuPhyAttribute + ModePages []ModePage + + Target *SCSITarget + Storage BackingStore + DeviceProtocol SCSIDeviceProtocol + + PerformCommand CommandFunc + FinishCommand func(*SCSITarget, *SCSICommand) } diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 4f00fa2..05adb92 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -1,5 +1,5 @@ /* -Copyright 2015 The GoStor Authors All rights reserved. +Copyright 2016 The GoStor Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,3 +16,306 @@ limitations under the License. // Package apiserver contains the code that provides a rest.ful api service. package apiserver + +import ( + "crypto/tls" + "fmt" + "net" + "net/http" + "strconv" + "strings" + + systemdActivation "github.com/coreos/go-systemd/activation" + "github.com/docker/docker/utils" + "github.com/docker/go-connections/sockets" + "github.com/golang/glog" + "github.com/gorilla/mux" + "github.com/gostor/gotgt/pkg/apiserver/httputils" + "github.com/gostor/gotgt/pkg/apiserver/router" + "github.com/gostor/gotgt/pkg/apiserver/router/discovery" + "github.com/gostor/gotgt/pkg/apiserver/router/lu" + "github.com/gostor/gotgt/pkg/apiserver/router/target" + "golang.org/x/net/context" +) + +// versionMatcher defines a variable matcher to be parsed by the router +// when a request is about to be served. +const versionMatcher = "/v{version:[0-9.]+}" + +// Config provides the configuration for the API server +type Config struct { + Logging bool + EnableCors bool + CorsHeaders string + AuthorizationPluginNames []string + Version string + SocketGroup string + TLSConfig *tls.Config + Addrs []Addr + APIrouter string +} + +// Addr contains string representation of address and its protocol (tcp, unix...). +type Addr struct { + Proto string + Addr string +} + +// Server contains instance details for the server +type Server struct { + cfg *Config + servers []*HTTPServer + routers []router.Router + routerSwapper *routerSwapper +} + +// New returns a new instance of the server based on the specified configuration. +// It allocates resources which will be needed for ServeAPI(ports, unix-sockets). +func New(cfg *Config) (*Server, error) { + s := &Server{ + cfg: cfg, + } + for _, addr := range cfg.Addrs { + srv, err := s.newServer(addr.Proto, addr.Addr) + if err != nil { + return nil, err + } + glog.V(3).Infof("Server created for HTTP on %s (%s)", addr.Proto, addr.Addr) + s.servers = append(s.servers, srv...) + } + return s, nil +} + +// Close closes servers and thus stop receiving requests +func (s *Server) Close() { + for _, srv := range s.servers { + if err := srv.Close(); err != nil { + glog.Error(err) + } + } +} + +// serveAPI loops through all initialized servers and spawns goroutine +// with Server method for each. It sets createMux() as Handler also. +func (s *Server) serveAPI() error { + s.initRouterSwapper() + + var chErrors = make(chan error, len(s.servers)) + for _, srv := range s.servers { + srv.srv.Handler = s.routerSwapper + go func(srv *HTTPServer) { + var err error + glog.V(3).Infof("API listen on %s", srv.l.Addr()) + if err = srv.Serve(); err != nil && strings.Contains(err.Error(), "use of closed network connection") { + err = nil + } + chErrors <- err + }(srv) + } + + for i := 0; i < len(s.servers); i++ { + err := <-chErrors + if err != nil { + return err + } + } + + return nil +} + +// HTTPServer contains an instance of http server and the listener. +// srv *http.Server, contains configuration to create a http server and a mux router with all api end points. +// l net.Listener, is a TCP or Socket listener that dispatches incoming request to the router. +type HTTPServer struct { + srv *http.Server + l net.Listener +} + +// Serve starts listening for inbound requests. +func (s *HTTPServer) Serve() error { + return s.srv.Serve(s.l) +} + +// Close closes the HTTPServer from listening for the inbound requests. +func (s *HTTPServer) Close() error { + return s.l.Close() +} + +func (s *Server) initTCPSocket(addr string) (l net.Listener, err error) { + if s.cfg.TLSConfig == nil || s.cfg.TLSConfig.ClientAuth != tls.RequireAndVerifyClientCert { + glog.Warning("/!\\ DON'T BIND ON ANY IP ADDRESS WITHOUT setting -tlsverify IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\") + } + if l, err = sockets.NewTCPSocket(addr, s.cfg.TLSConfig); err != nil { + return nil, err + } + + return l, nil +} + +func (s *Server) makeHTTPHandler(handler httputils.APIFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // log the handler call + glog.V(3).Infof("Calling %s %s", r.Method, r.URL.Path) + + // Define the context that we'll pass around to share info + // like the docker-request-id. + // + // The 'context' will be used for global data that should + // apply to all requests. Data that is specific to the + // immediate function being called should still be passed + // as 'args' on the function call. + ctx := context.Background() + handlerFunc := s.handleWithGlobalMiddlewares(handler) + + vars := mux.Vars(r) + if vars == nil { + vars = make(map[string]string) + } + + if err := handlerFunc(ctx, w, r, vars); err != nil { + glog.Errorf("Handler for %s %s returned error: %s", r.Method, r.URL.Path, utils.GetErrorMessage(err)) + httputils.WriteError(w, err) + } + } +} + +// InitRouters initializes a list of routers for the server. +func (s *Server) InitRouters() { + s.addRouter(target.NewRouter()) + s.addRouter(lu.NewRouter()) + s.addRouter(discovery.NewRouter()) +} + +// addRouter adds a new router to the server. +func (s *Server) addRouter(r router.Router) { + s.routers = append(s.routers, r) +} + +// createMux initializes the main router the server uses. +// we keep enableCors just for legacy usage, need to be removed in the future +func (s *Server) createMux() *mux.Router { + m := mux.NewRouter() + + glog.V(3).Infof("Registering routers") + for _, apiRouter := range s.routers { + for _, r := range apiRouter.Routes() { + f := s.makeHTTPHandler(r.Handler()) + + glog.V(3).Infof("Registering %s, %s", r.Method(), r.Path()) + m.Path(versionMatcher + r.Path()).Methods(r.Method()).Handler(f) + m.Path(r.Path()).Methods(r.Method()).Handler(f) + } + } + + return m +} + +// Wait blocks the server goroutine until it exits. +// It sends an error message if there is any error during +// the API execution. +func (s *Server) Wait(waitChan chan error) { + if err := s.serveAPI(); err != nil { + glog.Errorf("ServeAPI error: %v", err) + waitChan <- err + return + } + waitChan <- nil +} + +func (s *Server) initRouterSwapper() { + s.routerSwapper = &routerSwapper{ + router: s.createMux(), + } +} + +func (s *Server) handleWithGlobalMiddlewares(handler httputils.APIFunc) httputils.APIFunc { + return handler +} + +// newServer sets up the required HTTPServers and does protocol specific checking. +// newServer does not set any muxers, you should set it later to Handler field +func (s *Server) newServer(proto, addr string) ([]*HTTPServer, error) { + var ( + err error + ls []net.Listener + ) + switch proto { + case "fd": + ls, err = listenFD(addr, s.cfg.TLSConfig) + if err != nil { + return nil, err + } + case "tcp": + l, err := s.initTCPSocket(addr) + if err != nil { + return nil, err + } + ls = append(ls, l) + case "unix": + l, err := sockets.NewUnixSocket(addr, s.cfg.SocketGroup) + if err != nil { + return nil, fmt.Errorf("can't create unix socket %s: %v", addr, err) + } + ls = append(ls, l) + default: + return nil, fmt.Errorf("Invalid protocol format: %q", proto) + } + var res []*HTTPServer + for _, l := range ls { + res = append(res, &HTTPServer{ + &http.Server{ + Addr: addr, + }, + l, + }) + } + return res, nil +} + +// listenFD returns the specified socket activated files as a slice of +// net.Listeners or all of the activated files if "*" is given. +func listenFD(addr string, tlsConfig *tls.Config) ([]net.Listener, error) { + var ( + err error + listeners []net.Listener + ) + // socket activation + if tlsConfig != nil { + listeners, err = systemdActivation.TLSListeners(false, tlsConfig) + } else { + listeners, err = systemdActivation.Listeners(false) + } + if err != nil { + return nil, err + } + + if len(listeners) == 0 { + return nil, fmt.Errorf("No sockets found") + } + + // default to all fds just like unix:// and tcp:// + if addr == "" || addr == "*" { + return listeners, nil + } + + fdNum, err := strconv.Atoi(addr) + if err != nil { + return nil, fmt.Errorf("failed to parse systemd address, should be number: %v", err) + } + fdOffset := fdNum - 3 + if len(listeners) < int(fdOffset)+1 { + return nil, fmt.Errorf("Too few socket activated files passed in") + } + if listeners[fdOffset] == nil { + return nil, fmt.Errorf("failed to listen on systemd activated file at fd %d", fdOffset+3) + } + for i, ls := range listeners { + if i == fdOffset || ls == nil { + continue + } + if err := ls.Close(); err != nil { + glog.Errorf("Failed to close systemd activated file at fd %d: %v", fdOffset+3, err) + } + } + return []net.Listener{listeners[fdOffset]}, nil +} diff --git a/pkg/apiserver/handlers.go b/pkg/apiserver/handlers.go deleted file mode 100644 index 2b4b0fe..0000000 --- a/pkg/apiserver/handlers.go +++ /dev/null @@ -1,17 +0,0 @@ -/* -Copyright 2015 The GoStor Authors All rights reserved. - -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 apiserver diff --git a/pkg/apiserver/httputils/form.go b/pkg/apiserver/httputils/form.go new file mode 100644 index 0000000..e72efed --- /dev/null +++ b/pkg/apiserver/httputils/form.go @@ -0,0 +1,88 @@ +/* +Copyright 2016 The GoStor Authors All rights reserved. + +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 httputils + +import ( + "fmt" + "net/http" + "path/filepath" + "strconv" + "strings" +) + +// BoolValue transforms a form value in different formats into a boolean type. +func BoolValue(r *http.Request, k string) bool { + s := strings.ToLower(strings.TrimSpace(r.FormValue(k))) + return !(s == "" || s == "0" || s == "no" || s == "false" || s == "none") +} + +// BoolValueOrDefault returns the default bool passed if the query param is +// missing, otherwise it's just a proxy to boolValue above +func BoolValueOrDefault(r *http.Request, k string, d bool) bool { + if _, ok := r.Form[k]; !ok { + return d + } + return BoolValue(r, k) +} + +// Int64ValueOrZero parses a form value into an int64 type. +// It returns 0 if the parsing fails. +func Int64ValueOrZero(r *http.Request, k string) int64 { + val, err := Int64ValueOrDefault(r, k, 0) + if err != nil { + return 0 + } + return val +} + +// Int64ValueOrDefault parses a form value into an int64 type. If there is an +// error, returns the error. If there is no value returns the default value. +func Int64ValueOrDefault(r *http.Request, field string, def int64) (int64, error) { + if r.Form.Get(field) != "" { + value, err := strconv.ParseInt(r.Form.Get(field), 10, 64) + if err != nil { + return value, err + } + return value, nil + } + return def, nil +} + +// ArchiveOptions stores archive information for different operations. +type ArchiveOptions struct { + Name string + Path string +} + +// ArchiveFormValues parses form values and turns them into ArchiveOptions. +// It fails if the archive name and path are not in the request. +func ArchiveFormValues(r *http.Request, vars map[string]string) (ArchiveOptions, error) { + if err := ParseForm(r); err != nil { + return ArchiveOptions{}, err + } + + name := vars["name"] + path := filepath.FromSlash(r.Form.Get("path")) + + switch { + case name == "": + return ArchiveOptions{}, fmt.Errorf("bad parameter: 'name' cannot be empty") + case path == "": + return ArchiveOptions{}, fmt.Errorf("bad parameter: 'path' cannot be empty") + } + + return ArchiveOptions{name, path}, nil +} diff --git a/pkg/apiserver/httputils/form_test.go b/pkg/apiserver/httputils/form_test.go new file mode 100644 index 0000000..c56f7c1 --- /dev/null +++ b/pkg/apiserver/httputils/form_test.go @@ -0,0 +1,105 @@ +package httputils + +import ( + "net/http" + "net/url" + "testing" +) + +func TestBoolValue(t *testing.T) { + cases := map[string]bool{ + "": false, + "0": false, + "no": false, + "false": false, + "none": false, + "1": true, + "yes": true, + "true": true, + "one": true, + "100": true, + } + + for c, e := range cases { + v := url.Values{} + v.Set("test", c) + r, _ := http.NewRequest("POST", "", nil) + r.Form = v + + a := BoolValue(r, "test") + if a != e { + t.Fatalf("Value: %s, expected: %v, actual: %v", c, e, a) + } + } +} + +func TestBoolValueOrDefault(t *testing.T) { + r, _ := http.NewRequest("GET", "", nil) + if !BoolValueOrDefault(r, "queryparam", true) { + t.Fatal("Expected to get true default value, got false") + } + + v := url.Values{} + v.Set("param", "") + r, _ = http.NewRequest("GET", "", nil) + r.Form = v + if BoolValueOrDefault(r, "param", true) { + t.Fatal("Expected not to get true") + } +} + +func TestInt64ValueOrZero(t *testing.T) { + cases := map[string]int64{ + "": 0, + "asdf": 0, + "0": 0, + "1": 1, + } + + for c, e := range cases { + v := url.Values{} + v.Set("test", c) + r, _ := http.NewRequest("POST", "", nil) + r.Form = v + + a := Int64ValueOrZero(r, "test") + if a != e { + t.Fatalf("Value: %s, expected: %v, actual: %v", c, e, a) + } + } +} + +func TestInt64ValueOrDefault(t *testing.T) { + cases := map[string]int64{ + "": -1, + "-1": -1, + "42": 42, + } + + for c, e := range cases { + v := url.Values{} + v.Set("test", c) + r, _ := http.NewRequest("POST", "", nil) + r.Form = v + + a, err := Int64ValueOrDefault(r, "test", -1) + if a != e { + t.Fatalf("Value: %s, expected: %v, actual: %v", c, e, a) + } + if err != nil { + t.Fatalf("Error should be nil, but received: %s", err) + } + } +} + +func TestInt64ValueOrDefaultWithError(t *testing.T) { + v := url.Values{} + v.Set("test", "invalid") + r, _ := http.NewRequest("POST", "", nil) + r.Form = v + + _, err := Int64ValueOrDefault(r, "test", -1) + if err == nil { + t.Fatalf("Expected an error.") + } +} diff --git a/pkg/apiserver/httputils/httputils.go b/pkg/apiserver/httputils/httputils.go new file mode 100644 index 0000000..4e5899c --- /dev/null +++ b/pkg/apiserver/httputils/httputils.go @@ -0,0 +1,167 @@ +/* +Copyright 2016 The GoStor Authors All rights reserved. + +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 httputils + +import ( + "encoding/json" + "fmt" + "io" + "mime" + "net/http" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/gostor/gotgt/pkg/version" + "golang.org/x/net/context" +) + +// APIVersionKey is the client's requested API version. +const APIVersionKey = "api-version" + +// APIFunc is an adapter to allow the use of ordinary functions as Docker API endpoints. +// Any function that has the appropriate signature can be register as a API endpoint (e.g. getVersion). +type APIFunc func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error + +// HijackConnection interrupts the http response writer to get the +// underlying connection and operate with it. +func HijackConnection(w http.ResponseWriter) (io.ReadCloser, io.Writer, error) { + conn, _, err := w.(http.Hijacker).Hijack() + if err != nil { + return nil, nil, err + } + // Flush the options to make sure the client sets the raw mode + conn.Write([]byte{}) + return conn, conn, nil +} + +// CloseStreams ensures that a list for http streams are properly closed. +func CloseStreams(streams ...interface{}) { + for _, stream := range streams { + if tcpc, ok := stream.(interface { + CloseWrite() error + }); ok { + tcpc.CloseWrite() + } else if closer, ok := stream.(io.Closer); ok { + closer.Close() + } + } +} + +// MatchesContentType validates the content type against the expected one +func MatchesContentType(contentType, expectedType string) bool { + mimetype, _, err := mime.ParseMediaType(contentType) + if err != nil { + logrus.Errorf("Error parsing media type: %s error: %v", contentType, err) + } + return err == nil && mimetype == expectedType +} + +// CheckForJSON makes sure that the request's Content-Type is application/json. +func CheckForJSON(r *http.Request) error { + ct := r.Header.Get("Content-Type") + + // No Content-Type header is ok as long as there's no Body + if ct == "" { + if r.Body == nil || r.ContentLength == 0 { + return nil + } + } + + // Otherwise it better be json + if MatchesContentType(ct, "application/json") { + return nil + } + return fmt.Errorf("Content-Type specified (%s) must be 'application/json'", ct) +} + +// ParseForm ensures the request form is parsed even with invalid content types. +// If we don't do this, POST method without Content-type (even with empty body) will fail. +func ParseForm(r *http.Request) error { + if r == nil { + return nil + } + if err := r.ParseForm(); err != nil && !strings.HasPrefix(err.Error(), "mime:") { + return err + } + return nil +} + +// ParseMultipartForm ensure the request form is parsed, even with invalid content types. +func ParseMultipartForm(r *http.Request) error { + if err := r.ParseMultipartForm(4096); err != nil && !strings.HasPrefix(err.Error(), "mime:") { + return err + } + return nil +} + +// WriteError decodes a specific docker error and sends it in the response. +func WriteError(w http.ResponseWriter, err error) { + if err == nil || w == nil { + logrus.WithFields(logrus.Fields{"error": err, "writer": w}).Error("unexpected HTTP error handling") + return + } + + statusCode := http.StatusInternalServerError + errMsg := err.Error() + + // This part of will be removed once we've + // converted everything over to use the errcode package + + // FIXME: this is brittle and should not be necessary. + // If we need to differentiate between different possible error types, + // we should create appropriate error types with clearly defined meaning + errStr := strings.ToLower(err.Error()) + for keyword, status := range map[string]int{ + "not found": http.StatusNotFound, + "no such": http.StatusNotFound, + "bad parameter": http.StatusBadRequest, + "conflict": http.StatusConflict, + "impossible": http.StatusNotAcceptable, + "wrong login/password": http.StatusUnauthorized, + "hasn't been activated": http.StatusForbidden, + } { + if strings.Contains(errStr, keyword) { + statusCode = status + break + } + } + + if statusCode == 0 { + statusCode = http.StatusInternalServerError + } + + http.Error(w, errMsg, statusCode) +} + +// WriteJSON writes the value v to the http response stream as json with standard json encoding. +func WriteJSON(w http.ResponseWriter, code int, v interface{}) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + return json.NewEncoder(w).Encode(v) +} + +// VersionFromContext returns an API version from the context using APIVersionKey. +// It panics if the context value does not have version.Version type. +func VersionFromContext(ctx context.Context) string { + if ctx == nil { + return version.VERSION + } + val := ctx.Value(APIVersionKey) + if val == nil { + return version.VERSION + } + return val.(string) +} diff --git a/pkg/apiserver/router/discovery/discovery.go b/pkg/apiserver/router/discovery/discovery.go new file mode 100644 index 0000000..7aa5c69 --- /dev/null +++ b/pkg/apiserver/router/discovery/discovery.go @@ -0,0 +1,60 @@ +/* +Copyright 2016 The GoStor Authors All rights reserved. + +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 discovery + +import ( + "net/http" + + "github.com/gostor/gotgt/pkg/apiserver/router" + "golang.org/x/net/context" +) + +// containerRouter is a router to talk with the container controller +type discoveryRouter struct { + routes []router.Route +} + +// NewRouter initializes a new container router +func NewRouter() router.Router { + r := &discoveryRouter{} + r.initRoutes() + return r +} + +// Routes returns the available routers to the container controller +func (r *discoveryRouter) Routes() []router.Route { + return r.routes +} + +// initRoutes initializes the routes in discovery router +func (r *discoveryRouter) initRoutes() { + r.routes = []router.Route{ + // GET + router.NewGetRoute("/discovery/{name:.*}", r.getDiscovery), + // POST + router.NewPostRoute("/discovery/create", r.postDiscoveryCreate), + // PUT + // DELETE + } +} + +func (s *discoveryRouter) getDiscovery(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + return nil +} + +func (s *discoveryRouter) postDiscoveryCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + return nil +} diff --git a/pkg/apiserver/router/lu/lu.go b/pkg/apiserver/router/lu/lu.go new file mode 100644 index 0000000..f4bb166 --- /dev/null +++ b/pkg/apiserver/router/lu/lu.go @@ -0,0 +1,65 @@ +/* +Copyright 2016 The GoStor Authors All rights reserved. + +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 lu + +import ( + "net/http" + + "github.com/gostor/gotgt/pkg/apiserver/router" + "golang.org/x/net/context" +) + +// containerRouter is a router to talk with the container controller +type luRouter struct { + routes []router.Route +} + +// NewRouter initializes a new container router +func NewRouter() router.Router { + r := &luRouter{} + r.initRoutes() + return r +} + +// Routes returns the available routers to the container controller +func (r *luRouter) Routes() []router.Route { + return r.routes +} + +// initRoutes initializes the routes in lu router +func (r *luRouter) initRoutes() { + r.routes = []router.Route{ + // GET + router.NewGetRoute("/lu/{id:.*}", r.getLu), + // POST + router.NewPostRoute("/lu/create", r.postLuCreate), + // PUT + // DELETE + router.NewDeleteRoute("/lu/{id:.*}", r.deleteLu), + } +} + +func (s *luRouter) getLu(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + return nil +} + +func (s *luRouter) postLuCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + return nil +} + +func (s *luRouter) deleteLu(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + return nil +} diff --git a/pkg/apiserver/router/router.go b/pkg/apiserver/router/router.go new file mode 100644 index 0000000..a7915c8 --- /dev/null +++ b/pkg/apiserver/router/router.go @@ -0,0 +1,76 @@ +/* +Copyright 2016 The GoStor Authors All rights reserved. + +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 router + +import "github.com/gostor/gotgt/pkg/apiserver/httputils" + +// Router defines an interface to specify a group of routes to add the the docker server. +type Router interface { + Routes() []Route +} + +// Route defines an individual API route in the docker server. +type Route interface { + // Handler returns the raw function to create the http handler. + Handler() httputils.APIFunc + // Method returns the http method that the route responds to. + Method() string + // Path returns the subpath where the route responds to. + Path() string +} + +// localRoute defines an individual API route to connect with the docker daemon. +// It implements router.Route. +type localRoute struct { + method string + path string + handler httputils.APIFunc +} + +// Handler returns the APIFunc to let the server wrap it in middlewares +func (l localRoute) Handler() httputils.APIFunc { + return l.handler +} + +// Method returns the http method that the route responds to. +func (l localRoute) Method() string { + return l.method +} + +// Path returns the subpath where the route responds to. +func (l localRoute) Path() string { + return l.path +} + +// NewRoute initializes a new local router for the reouter +func NewRoute(method, path string, handler httputils.APIFunc) Route { + return localRoute{method, path, handler} +} + +// NewGetRoute initializes a new route with the http method GET. +func NewGetRoute(path string, handler httputils.APIFunc) Route { + return NewRoute("GET", path, handler) +} + +// NewPostRoute initializes a new route with the http method POST. +func NewPostRoute(path string, handler httputils.APIFunc) Route { + return NewRoute("POST", path, handler) +} + +// NewPostRoute initializes a new route with the http method POST. +func NewDeleteRoute(path string, handler httputils.APIFunc) Route { + return NewRoute("DELETE", path, handler) +} diff --git a/pkg/apiserver/router/target/target.go b/pkg/apiserver/router/target/target.go new file mode 100644 index 0000000..87cbc9a --- /dev/null +++ b/pkg/apiserver/router/target/target.go @@ -0,0 +1,77 @@ +/* +Copyright 2016 The GoStor Authors All rights reserved. + +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 target + +import ( + "net/http" + + "github.com/gostor/gotgt/pkg/apiserver/httputils" + "github.com/gostor/gotgt/pkg/apiserver/router" + "github.com/gostor/gotgt/pkg/scsi" + "golang.org/x/net/context" +) + +// containerRouter is a router to talk with the container controller +type targetRouter struct { + routes []router.Route +} + +// NewRouter initializes a new container router +func NewRouter() router.Router { + r := &targetRouter{} + r.initRoutes() + return r +} + +// Routes returns the available routers to the container controller +func (r *targetRouter) Routes() []router.Route { + return r.routes +} + +// initRoutes initializes the routes in target router +func (r *targetRouter) initRoutes() { + r.routes = []router.Route{ + // GET + router.NewGetRoute("/target/list", r.getTargetList), + // POST + router.NewPostRoute("/target/create", r.postTargetCreate), + router.NewPostRoute("/target/up", r.postTargetUp), + // PUT + // DELETE + router.NewDeleteRoute("/target/{name:.*}", r.deleteTarget), + } +} + +func (r *targetRouter) getTargetList(ctx context.Context, w http.ResponseWriter, req *http.Request, vars map[string]string) error { + service := scsi.NewSCSITargetService() + tgts, err := service.GetTargetList() + if err != nil { + return err + } + return httputils.WriteJSON(w, http.StatusOK, tgts) +} + +func (r *targetRouter) postTargetCreate(ctx context.Context, w http.ResponseWriter, req *http.Request, vars map[string]string) error { + return nil +} + +func (r *targetRouter) postTargetUp(ctx context.Context, w http.ResponseWriter, req *http.Request, vars map[string]string) error { + return nil +} + +func (r *targetRouter) deleteTarget(ctx context.Context, w http.ResponseWriter, req *http.Request, vars map[string]string) error { + return nil +} diff --git a/pkg/apiserver/router_swapper.go b/pkg/apiserver/router_swapper.go new file mode 100644 index 0000000..fdec9a7 --- /dev/null +++ b/pkg/apiserver/router_swapper.go @@ -0,0 +1,47 @@ +/* +Copyright 2016 The GoStor Authors All rights reserved. + +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 apiserver contains the code that provides a rest.ful api service. +package apiserver + +import ( + "net/http" + "sync" + + "github.com/gorilla/mux" +) + +// routerSwapper is an http.Handler that allow you to swap +// mux routers. +type routerSwapper struct { + mu sync.Mutex + router *mux.Router +} + +// Swap changes the old router with the new one. +func (rs *routerSwapper) Swap(newRouter *mux.Router) { + rs.mu.Lock() + rs.router = newRouter + rs.mu.Unlock() +} + +// ServeHTTP makes the routerSwapper to implement the http.Handler interface. +func (rs *routerSwapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { + rs.mu.Lock() + router := rs.router + rs.mu.Unlock() + router.ServeHTTP(w, r) +}