From 536c88eca46aae8a1fd1a0ec926a299b4d92b22c Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Tue, 9 Jun 2015 16:35:45 +0200 Subject: [PATCH 01/32] wip #56 --- cp.go | 100 ++++++++++++++++++++++++++++++++++------------------------ 1 file changed, 59 insertions(+), 41 deletions(-) diff --git a/cp.go b/cp.go index bff1eac53f..11a7759915 100644 --- a/cp.go +++ b/cp.go @@ -13,9 +13,14 @@ import ( var cmdCp = &Command{ Exec: runCp, - UsageLine: "cp [OPTIONS] SERVER:PATH HOSTDIR|-", + UsageLine: "cp [OPTIONS] SERVER:PATH|HOSTPATH|- SERVER:PATH|HOSTPATH|-", Description: "Copy files/folders from a PATH on the server to a HOSTDIR on the host", Help: "Copy files/folders from a PATH on the server to a HOSTDIR on the host\nrunning the command. Use '-' to write the data as a tar file to STDOUT.", + Examples: ` + $ scw cp path/to/my/local/file myserver:path + $ scw cp myserver:path path/to/my/local/file + $ scw cp myserver:path myserver2:path +`, } func init() { @@ -33,62 +38,75 @@ func runCp(cmd *Command, args []string) { cmd.PrintShortUsage() } - hostPath := args[1] + var tarOutputStream io.ReadCloser - serverParts := strings.Split(args[0], ":") - if len(serverParts) != 2 { - log.Fatalf("usage: scw %s", cmd.UsageLine) - } + // source + source := args[0] + if strings.Index(source, ":") > -1 { // source server address + serverParts := strings.Split(args[0], ":") + if len(serverParts) != 2 { + log.Fatalf("usage: scw %s", cmd.UsageLine) + } - serverID := cmd.API.GetServerID(serverParts[0]) + serverID := cmd.API.GetServerID(serverParts[0]) - server, err := cmd.API.GetServer(serverID) - if err != nil { - log.Fatalf("Failed to get server information for %s: %v", serverID, err) - } + server, err := cmd.API.GetServer(serverID) + if err != nil { + log.Fatalf("Failed to get server information for %s: %v", serverID, err) + } - // remoteCommand is executed on the remote server - // it streams a tarball raw content - remoteCommand := []string{"tar"} - remoteCommand = append(remoteCommand, "-C", path.Dir(serverParts[1])) - if os.Getenv("DEBUG") == "1" { - remoteCommand = append(remoteCommand, "-v") - } - remoteCommand = append(remoteCommand, "-cf", "-") - remoteCommand = append(remoteCommand, path.Base(serverParts[1])) + // remoteCommand is executed on the remote server + // it streams a tarball raw content + remoteCommand := []string{"tar"} + remoteCommand = append(remoteCommand, "-C", path.Dir(serverParts[1])) + if os.Getenv("DEBUG") == "1" { + remoteCommand = append(remoteCommand, "-v") + } + remoteCommand = append(remoteCommand, "-cf", "-") + remoteCommand = append(remoteCommand, path.Base(serverParts[1])) - // execCmd contains the ssh connection + the remoteCommand - execCmd := append(NewSSHExecCmd(server.PublicAddress.IP, false, remoteCommand)) - log.Debugf("Executing: ssh %s", strings.Join(execCmd, " ")) - spawn := exec.Command("ssh", execCmd...) + // execCmd contains the ssh connection + the remoteCommand + execCmd := append(NewSSHExecCmd(server.PublicAddress.IP, false, remoteCommand)) + log.Debugf("Executing: ssh %s", strings.Join(execCmd, " ")) + spawn := exec.Command("ssh", execCmd...) - tarOutputStream, err := spawn.StdoutPipe() - if err != nil { - log.Fatal(err) - } - tarErrorStream, err := spawn.StderrPipe() - if err != nil { - log.Fatal(err) - } + tarOutputStream, err = spawn.StdoutPipe() + if err != nil { + log.Fatal(err) + } + tarErrorStream, err := spawn.StderrPipe() + if err != nil { + log.Fatal(err) + } - err = spawn.Start() - if err != nil { - log.Fatalf("Failed to start ssh command: %v", err) - } + err = spawn.Start() + if err != nil { + log.Fatalf("Failed to start ssh command: %v", err) + } - defer spawn.Wait() + defer spawn.Wait() - io.Copy(os.Stderr, tarErrorStream) + io.Copy(os.Stderr, tarErrorStream) + } else if source == "-" { // stdin + log.Fatalf("'scw cp - ...' is not yet implemented") + } else { // source host path + log.Fatalf("'scw cp HOSTPATH ...' is not yet implemented") + } - if hostPath == "-" { + // destination + destination := args[1] + if strings.Index(destination, ":") > -1 { // destination server address + log.Fatalf("'scw cp ... SERVER' is not yet implemented") + } else if destination == "-" { // stdout log.Debugf("Writing tarOutputStream(%v) to os.Stdout(%v)", tarOutputStream, os.Stdout) written, err := io.Copy(os.Stdout, tarOutputStream) log.Debugf("%d bytes written", written) if err != nil { log.Fatal(err) } - } else { - err = archive.Untar(tarOutputStream, hostPath, &archive.TarOptions{NoLchown: true}) + + } else { // destination host path + err := archive.Untar(tarOutputStream, destination, &archive.TarOptions{NoLchown: true}) if err != nil { log.Fatalf("Failed to untar the remote archive: %v", err) } From 4560ecf2e253deab810a228a2da2eb9b88af9bbe Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Tue, 9 Jun 2015 20:19:15 +0200 Subject: [PATCH 02/32] Improved Makefile dependencies --- Makefile | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 515fbd4833..41bd31a601 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,9 @@ # Go parameters GOCMD ?= go -GOBUILD ?= $(GOCMD) build +GOBUILD ?= godep $(GOCMD) build GOCLEAN ?= $(GOCMD) clean GOINSTALL ?= $(GOCMD) install -GOTEST ?= $(GOCMD) test +GOTEST ?= godep $(GOCMD) test GODEP ?= $(GOTEST) -i GOFMT ?= gofmt -w @@ -20,10 +20,10 @@ CLEAN_LIST = $(foreach int, $(SRC), $(int)_clean) INSTALL_LIST = $(foreach int, $(SRC), $(int)_install) IREF_LIST = $(foreach int, $(SRC), $(int)_iref) TEST_LIST = $(foreach int, $(SRC), $(int)_test) -FMT_TEST = $(foreach int, $(SRC), $(int)_fmt) +FMT_LIST = $(foreach int, $(SRC), $(int)_fmt) -.PHONY: $(CLEAN_LIST) $(TEST_LIST) $(FMT_LIST) $(INSTALL_LIST) $(BUILD_LIST) $(IREF_LIST) scwversion/version.go +.PHONY: $(CLEAN_LIST) $(TEST_LIST) $(FMT_LIST) $(INSTALL_LIST) $(BUILD_LIST) $(IREF_LIST) all: build @@ -32,10 +32,14 @@ clean: $(CLEAN_LIST) install: $(INSTALL_LIST) test: $(TEST_LIST) iref: $(IREF_LIST) -fmt: $(FMT_TEST) +fmt: $(FMT_LIST) -scwversion/version.go: +.git: + touch $@ + + +scwversion/version.go: .git @sed 's/\(.*GITCOMMIT.* = \).*/\1"$(REV)"/;s/\(.*VERSION.* = \).*/\1"$(TAG)"/' scwversion/version.tpl > $@.tmp @if [ "$$(diff $@.tmp $@ 2>&1)" != "" ]; then mv $@.tmp $@; fi @rm -f $@.tmp @@ -58,7 +62,7 @@ $(IREF_LIST): %_iref: Godeps $(GODEP) ./$* $(TEST_LIST): %_test: $(GOTEST) ./$* -$(FMT_TEST): %_fmt: +$(FMT_LIST): %_fmt: $(GOFMT) ./$* From 2c40d4f204f7f668311834da9140421b2ef32328 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Tue, 9 Jun 2015 23:55:22 +0200 Subject: [PATCH 03/32] Added PathToSCPPathParts helper --- utils.go | 7 +++++++ utils_test.go | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/utils.go b/utils.go index e0afbc2a37..7eb88d10c3 100644 --- a/utils.go +++ b/utils.go @@ -6,6 +6,7 @@ import ( "net" "os" "os/exec" + "path" "regexp" "strings" "time" @@ -100,3 +101,9 @@ func wordify(str string) string { str = strings.Trim(str, "_") return str } + +// PathToSCPPathparts returns the two parts of a unix path +func PathToSCPPathparts(fullPath string) (string, string) { + fullPath = strings.TrimRight(fullPath, "/") + return path.Dir(fullPath), path.Base(fullPath) +} diff --git a/utils_test.go b/utils_test.go index a0deebd52d..d24ed84324 100644 --- a/utils_test.go +++ b/utils_test.go @@ -35,3 +35,26 @@ func TestTruncIf(t *testing.T) { t.Errorf("returned value is invalid [actual: %s][expected: %s]", actual, expected) } } + +func TestPathToSCPPathparts(t *testing.T) { + dir, base := PathToSCPPathparts("/etc/passwd") + expected := []string{"/etc", "passwd"} + actual := []string{dir, base} + if actual[0] != expected[0] || actual[1] != expected[1] { + t.Errorf("returned value is invalid [actual: %s][expected: %s]", actual, expected) + } + + dir, base = PathToSCPPathparts("/etc") + expected = []string{"/", "etc"} + actual = []string{dir, base} + if actual[0] != expected[0] || actual[1] != expected[1] { + t.Errorf("returned value is invalid [actual: %s][expected: %s]", actual, expected) + } + + dir, base = PathToSCPPathparts("/etc/") + expected = []string{"/", "etc"} + actual = []string{dir, base} + if actual[0] != expected[0] || actual[1] != expected[1] { + t.Errorf("returned value is invalid [actual: %s][expected: %s]", actual, expected) + } +} From 619172ea0922953135a9fd54c9e5b16bf800a584 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Tue, 9 Jun 2015 23:55:37 +0200 Subject: [PATCH 04/32] Refactored `scw cp` to accept any kind of source and destination (#56) --- cp.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/cp.go b/cp.go index 11a7759915..3298aa83c9 100644 --- a/cp.go +++ b/cp.go @@ -4,7 +4,6 @@ import ( "io" "os" "os/exec" - "path" "strings" log "github.com/Sirupsen/logrus" @@ -18,8 +17,15 @@ var cmdCp = &Command{ Help: "Copy files/folders from a PATH on the server to a HOSTDIR on the host\nrunning the command. Use '-' to write the data as a tar file to STDOUT.", Examples: ` $ scw cp path/to/my/local/file myserver:path - $ scw cp myserver:path path/to/my/local/file - $ scw cp myserver:path myserver2:path + $ scw cp myserver:path/to/file path/to/my/local/file + $ scw cp myserver:path/to/file myserver2:path/to/file + $ scw cp myserver:path/to/file - > myserver-pathtofile-backup.tar + $ scw cp myserver:path/to/file - | tar -tvf - + $ scw cp path/to/my/local/dir myserver:path + $ scw cp myserver:path/to/dir path/to/my/local/dir + $ scw cp myserver:path/to/dir myserver2:path/to/dir + $ scw cp myserver:path/to/dir - > myserver-pathtodir-backup.tar + $ scw cp myserver:path/to/dir - | tar -tvf - `, } @@ -39,6 +45,7 @@ func runCp(cmd *Command, args []string) { } var tarOutputStream io.ReadCloser + var tarErrorStream io.ReadCloser // source source := args[0] @@ -55,15 +62,17 @@ func runCp(cmd *Command, args []string) { log.Fatalf("Failed to get server information for %s: %v", serverID, err) } + dir, base := PathToSCPPathparts(serverParts[1]) + // remoteCommand is executed on the remote server // it streams a tarball raw content remoteCommand := []string{"tar"} - remoteCommand = append(remoteCommand, "-C", path.Dir(serverParts[1])) + remoteCommand = append(remoteCommand, "-C", dir) if os.Getenv("DEBUG") == "1" { remoteCommand = append(remoteCommand, "-v") } remoteCommand = append(remoteCommand, "-cf", "-") - remoteCommand = append(remoteCommand, path.Base(serverParts[1])) + remoteCommand = append(remoteCommand, base) // execCmd contains the ssh connection + the remoteCommand execCmd := append(NewSSHExecCmd(server.PublicAddress.IP, false, remoteCommand)) @@ -74,7 +83,7 @@ func runCp(cmd *Command, args []string) { if err != nil { log.Fatal(err) } - tarErrorStream, err := spawn.StderrPipe() + tarErrorStream, err = spawn.StderrPipe() if err != nil { log.Fatal(err) } @@ -97,6 +106,7 @@ func runCp(cmd *Command, args []string) { destination := args[1] if strings.Index(destination, ":") > -1 { // destination server address log.Fatalf("'scw cp ... SERVER' is not yet implemented") + } else if destination == "-" { // stdout log.Debugf("Writing tarOutputStream(%v) to os.Stdout(%v)", tarOutputStream, os.Stdout) written, err := io.Copy(os.Stdout, tarOutputStream) From 8f1e97a6fc89e77a1951ef8845dcb930281a3c21 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Tue, 9 Jun 2015 17:37:32 +0200 Subject: [PATCH 05/32] Added .travis.yml --- .travis.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..80dac01f46 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: go + +go: + - 1.4.2 + +install: + - export PATH=$HOME/gopath/bin:$PATH + - go get golang.org/x/tools/cmd/cover + +script: + - make Godeps + - make build + - go test -v -covermode=count -coverprofile=profile.cov From 38450e147809dfbbb69ad5ddb7cee3e781f6e25c Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Tue, 9 Jun 2015 17:48:15 +0200 Subject: [PATCH 06/32] Updated .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f101238b2c..108610c3ec 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ dist/ *~ *# .#* +profile.cov From f8326fc71fc29d59b632d1917a550b222ea919bf Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Tue, 9 Jun 2015 17:53:23 +0200 Subject: [PATCH 07/32] Refactored travis checks --- .travis.yml | 18 +++++++++++++----- Makefile | 8 ++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 80dac01f46..51029fbbad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,21 @@ language: go + +sudo: false + + +env: + global: + - PATH=$HOME/gopath/bin:$PATH + + go: - 1.4.2 + install: - - export PATH=$HOME/gopath/bin:$PATH - - go get golang.org/x/tools/cmd/cover + - make travis_install + script: - - make Godeps - - make build - - go test -v -covermode=count -coverprofile=profile.cov + - make travis_run diff --git a/Makefile b/Makefile index 41bd31a601..e55056cf8a 100644 --- a/Makefile +++ b/Makefile @@ -76,3 +76,11 @@ cross: scwversion/version.go touch tmp/bin/* mv tmp/bin/* dist/ rm -rf tmp dist/godep + + +travis_install: + go get golang.org/x/tools/cmd/cover + + +travis_run: build + go test -v -covermode=count -coverprofile=profile.cov From 468959813b25c3e821e29a33c4c12d5755dcb9f1 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Tue, 9 Jun 2015 17:55:13 +0200 Subject: [PATCH 08/32] Added travis-ci badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1738b9bdde..bed0e546ab 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ Interact with Scaleway API from the command line. +[![Build Status (Travis)](https://img.shields.io/travis/scaleway/scaleway-cli.svg)](https://travis-ci.org/scaleway/scaleway-cli) [![GoDoc](https://godoc.org/github.com/scaleway/scaleway-cli?status.svg)](https://godoc.org/github.com/scaleway/scaleway-cli) For node version, check out [scaleway-cli-node](https://github.com/moul/scaleway-cli-node). From edd52318ab2d14f667e751e56215f1371ddf5349 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Wed, 10 Jun 2015 10:12:26 +0200 Subject: [PATCH 09/32] Handling 'scw cp HOSTPATH ...' (#56) --- cp.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/cp.go b/cp.go index 3298aa83c9..a01ccec3fd 100644 --- a/cp.go +++ b/cp.go @@ -4,6 +4,7 @@ import ( "io" "os" "os/exec" + "path/filepath" "strings" log "github.com/Sirupsen/logrus" @@ -99,7 +100,26 @@ func runCp(cmd *Command, args []string) { } else if source == "-" { // stdin log.Fatalf("'scw cp - ...' is not yet implemented") } else { // source host path - log.Fatalf("'scw cp HOSTPATH ...' is not yet implemented") + log.Debugf("Creating tarball of local path %s", source) + path, err := filepath.Abs(source) + if err != nil { + log.Fatalf("Cannot tar local path: %v", err) + } + path, err = filepath.EvalSymlinks(path) + if err != nil { + log.Fatalf("Cannot tar local path: %v", err) + } + log.Debugf("Real local path is %s", path) + + dir, base := PathToSCPPathparts(path) + + tarOutputStream, err = archive.TarWithOptions(dir, &archive.TarOptions{ + Compression: archive.Uncompressed, + IncludeFiles: []string{base}, + }) + if err != nil { + log.Fatalf("Cannot tar local path: %v", err) + } } // destination From 6f4a46ce91870a1bf56308da9546f705dcda433c Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Wed, 10 Jun 2015 10:19:44 +0200 Subject: [PATCH 10/32] Handling 'scw cp - ...' (#56) --- cp.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/cp.go b/cp.go index a01ccec3fd..796d639678 100644 --- a/cp.go +++ b/cp.go @@ -18,15 +18,17 @@ var cmdCp = &Command{ Help: "Copy files/folders from a PATH on the server to a HOSTDIR on the host\nrunning the command. Use '-' to write the data as a tar file to STDOUT.", Examples: ` $ scw cp path/to/my/local/file myserver:path - $ scw cp myserver:path/to/file path/to/my/local/file - $ scw cp myserver:path/to/file myserver2:path/to/file + $ scw cp myserver:path/to/file path/to/my/local/dir + $ scw cp myserver:path/to/file myserver2:path/to/dir $ scw cp myserver:path/to/file - > myserver-pathtofile-backup.tar $ scw cp myserver:path/to/file - | tar -tvf - - $ scw cp path/to/my/local/dir myserver:path - $ scw cp myserver:path/to/dir path/to/my/local/dir - $ scw cp myserver:path/to/dir myserver2:path/to/dir - $ scw cp myserver:path/to/dir - > myserver-pathtodir-backup.tar - $ scw cp myserver:path/to/dir - | tar -tvf - + $ scw cp path/to/my/local/dir myserver:path + $ scw cp myserver:path/to/dir path/to/my/local/dir + $ scw cp myserver:path/to/dir myserver2:path/to/dir + $ scw cp myserver:path/to/dir - > myserver-pathtodir-backup.tar + $ scw cp myserver:path/to/dir - | tar -tvf - + $ cat archive.tar | scw cp - myserver:/path + $ tar -cvf - . | scw cp - myserver:path `, } @@ -98,7 +100,7 @@ func runCp(cmd *Command, args []string) { io.Copy(os.Stderr, tarErrorStream) } else if source == "-" { // stdin - log.Fatalf("'scw cp - ...' is not yet implemented") + tarOutputStream = os.Stdin } else { // source host path log.Debugf("Creating tarball of local path %s", source) path, err := filepath.Abs(source) From 7257d67d7317f00ec9c1cdadc2c5c7dd8ed26984 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Wed, 10 Jun 2015 10:26:53 +0200 Subject: [PATCH 11/32] Renamed misnamed function --- cp.go | 4 ++-- utils.go | 4 ++-- utils_test.go | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cp.go b/cp.go index 796d639678..fc98f526fd 100644 --- a/cp.go +++ b/cp.go @@ -65,7 +65,7 @@ func runCp(cmd *Command, args []string) { log.Fatalf("Failed to get server information for %s: %v", serverID, err) } - dir, base := PathToSCPPathparts(serverParts[1]) + dir, base := PathToTARPathparts(serverParts[1]) // remoteCommand is executed on the remote server // it streams a tarball raw content @@ -113,7 +113,7 @@ func runCp(cmd *Command, args []string) { } log.Debugf("Real local path is %s", path) - dir, base := PathToSCPPathparts(path) + dir, base := PathToTARPathparts(path) tarOutputStream, err = archive.TarWithOptions(dir, &archive.TarOptions{ Compression: archive.Uncompressed, diff --git a/utils.go b/utils.go index 7eb88d10c3..9122682299 100644 --- a/utils.go +++ b/utils.go @@ -102,8 +102,8 @@ func wordify(str string) string { return str } -// PathToSCPPathparts returns the two parts of a unix path -func PathToSCPPathparts(fullPath string) (string, string) { +// PathToTARPathparts returns the two parts of a unix path +func PathToTARPathparts(fullPath string) (string, string) { fullPath = strings.TrimRight(fullPath, "/") return path.Dir(fullPath), path.Base(fullPath) } diff --git a/utils_test.go b/utils_test.go index d24ed84324..87a209b5e1 100644 --- a/utils_test.go +++ b/utils_test.go @@ -36,22 +36,22 @@ func TestTruncIf(t *testing.T) { } } -func TestPathToSCPPathparts(t *testing.T) { - dir, base := PathToSCPPathparts("/etc/passwd") +func TestPathToTARPathparts(t *testing.T) { + dir, base := PathToTARPathparts("/etc/passwd") expected := []string{"/etc", "passwd"} actual := []string{dir, base} if actual[0] != expected[0] || actual[1] != expected[1] { t.Errorf("returned value is invalid [actual: %s][expected: %s]", actual, expected) } - dir, base = PathToSCPPathparts("/etc") + dir, base = PathToTARPathparts("/etc") expected = []string{"/", "etc"} actual = []string{dir, base} if actual[0] != expected[0] || actual[1] != expected[1] { t.Errorf("returned value is invalid [actual: %s][expected: %s]", actual, expected) } - dir, base = PathToSCPPathparts("/etc/") + dir, base = PathToTARPathparts("/etc/") expected = []string{"/", "etc"} actual = []string{dir, base} if actual[0] != expected[0] || actual[1] != expected[1] { From fa73064c4f8eadf06b25de1b0bb8029782c903e0 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Wed, 10 Jun 2015 10:49:56 +0200 Subject: [PATCH 12/32] wip: Handling 'scw cp ... SERVER:PATH' (#56) --- cp.go | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/cp.go b/cp.go index fc98f526fd..939d818c78 100644 --- a/cp.go +++ b/cp.go @@ -127,8 +127,55 @@ func runCp(cmd *Command, args []string) { // destination destination := args[1] if strings.Index(destination, ":") > -1 { // destination server address - log.Fatalf("'scw cp ... SERVER' is not yet implemented") + serverParts := strings.Split(destination, ":") + if len(serverParts) != 2 { + log.Fatalf("usage: scw %s", cmd.UsageLine) + } + + serverID := cmd.API.GetServerID(serverParts[0]) + + server, err := cmd.API.GetServer(serverID) + if err != nil { + log.Fatalf("Failed to get server information for %s: %v", serverID, err) + } + + // remoteCommand is executed on the remote server + // it streams a tarball raw content + remoteCommand := []string{"tar"} + remoteCommand = append(remoteCommand, "-C", serverParts[1]) + if os.Getenv("DEBUG") == "1" { + remoteCommand = append(remoteCommand, "-v") + } + remoteCommand = append(remoteCommand, "-tf", "-") + + // execCmd contains the ssh connection + the remoteCommand + execCmd := append(NewSSHExecCmd(server.PublicAddress.IP, false, remoteCommand)) + log.Debugf("Executing: ssh %s", strings.Join(execCmd, " ")) + spawn := exec.Command("ssh", execCmd...) + + untarInputStream, err := spawn.StdinPipe() + if err != nil { + log.Fatal(err) + } + untarErrorStream, err := spawn.StderrPipe() + if err != nil { + log.Fatal(err) + } + untarOutputStream, err := spawn.StdoutPipe() + if err != nil { + log.Fatal(err) + } + + err = spawn.Start() + if err != nil { + log.Fatalf("Failed to start ssh command: %v", err) + } + + defer spawn.Wait() + io.Copy(untarInputStream, tarOutputStream) + io.Copy(os.Stderr, untarErrorStream) + io.Copy(os.Stdout, untarOutputStream) } else if destination == "-" { // stdout log.Debugf("Writing tarOutputStream(%v) to os.Stdout(%v)", tarOutputStream, os.Stdout) written, err := io.Copy(os.Stdout, tarOutputStream) From 631b62d666c88c53bf22f6bae44d9b3c7ac86f96 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Thu, 11 Jun 2015 15:59:58 +0200 Subject: [PATCH 13/32] Added resolveIdentifier and getIdentifier api helpers --- api_helpers.go | 86 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 63 insertions(+), 23 deletions(-) diff --git a/api_helpers.go b/api_helpers.go index a5b99855c4..74b8dedadb 100644 --- a/api_helpers.go +++ b/api_helpers.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "os" "strings" "sync" "time" @@ -40,6 +41,66 @@ type ScalewayResolvedIdentifier struct { Needle string } +// fillIdentifierCache fills the cache by fetching fro the API +func fillIdentifierCache(api *ScalewayAPI) { + log.Debugf("Filling the cache") + var wg sync.WaitGroup + wg.Add(5) + go func() { + api.GetServers(true, 0) + wg.Done() + }() + go func() { + api.GetImages() + wg.Done() + }() + go func() { + api.GetSnapshots() + wg.Done() + }() + go func() { + api.GetVolumes() + wg.Done() + }() + go func() { + api.GetBootscripts() + wg.Done() + }() + wg.Wait() +} + +// getIdentifier returns a an identifier if the resolved needles only match one element, else, it exists the program +func getIdentifier(api *ScalewayAPI, needle string) *ScalewayIdentifier { + idents := resolveIdentifier(api, needle) + + if len(idents) == 1 { + return &idents[0] + } + if len(idents) == 0 { + log.Fatalf("No such identifier: %s", needle) + } + log.Errorf("Too many candidates for %s (%d)", needle, len(idents)) + for _, identifier := range idents { + // FIXME: also print the name + log.Infof("- %s", identifier.Identifier) + } + os.Exit(1) + return nil +} + +// resolveIdentifier resolves needle provided by the user +func resolveIdentifier(api *ScalewayAPI, needle string) []ScalewayIdentifier { + idents := api.Cache.LookUpIdentifiers(needle) + if len(idents) > 0 { + return idents + } + + fillIdentifierCache(api) + + idents = api.Cache.LookUpIdentifiers(needle) + return idents +} + // resolveIdentifiers resolves needles provided by the user func resolveIdentifiers(api *ScalewayAPI, needles []string, out chan ScalewayResolvedIdentifier) { // first attempt, only lookup from the cache @@ -57,29 +118,8 @@ func resolveIdentifiers(api *ScalewayAPI, needles []string, out chan ScalewayRes } // fill the cache by fetching from the API and resolve missing identifiers if len(unresolved) > 0 { - var wg sync.WaitGroup - wg.Add(5) - go func() { - api.GetServers(true, 0) - wg.Done() - }() - go func() { - api.GetImages() - wg.Done() - }() - go func() { - api.GetSnapshots() - wg.Done() - }() - go func() { - api.GetVolumes() - wg.Done() - }() - go func() { - api.GetBootscripts() - wg.Done() - }() - wg.Wait() + fillIdentifierCache(api) + for _, needle := range unresolved { idents := api.Cache.LookUpIdentifiers(needle) out <- ScalewayResolvedIdentifier{ From 36e25b963334465e1c649cab0aab9f79385fc5eb Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Thu, 11 Jun 2015 16:11:49 +0200 Subject: [PATCH 14/32] typo --- api.go | 6 +++--- rename.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api.go b/api.go index fd49ae5416..88f3ed77a6 100644 --- a/api.go +++ b/api.go @@ -380,8 +380,8 @@ type ScalewayServer struct { Organization string `json:"organization,omitempty"` } -// ScalewayServerPathNameDefinition represents a Scaleway C1 server with only its name as field -type ScalewayServerPathNameDefinition struct { +// ScalewayServerPatchNameDefinition represents a Scaleway C1 server with only its name as field +type ScalewayServerPatchNameDefinition struct { // Name is the user-defined name of the server Name string `json:"name"` } @@ -683,7 +683,7 @@ func (s *ScalewayAPI) PostServer(definition ScalewayServerDefinition) (string, e } // PatchServerName changes the name of the server -func (s *ScalewayAPI) PatchServerName(serverID string, definition ScalewayServerPathNameDefinition) error { +func (s *ScalewayAPI) PatchServerName(serverID string, definition ScalewayServerPatchNameDefinition) error { resp, err := s.PatchResponse(fmt.Sprintf("servers/%s", serverID), definition) if err != nil { return err diff --git a/rename.go b/rename.go index 6d3cdec749..32d5c5f27f 100644 --- a/rename.go +++ b/rename.go @@ -26,7 +26,7 @@ func runRename(cmd *Command, args []string) { serverID := cmd.API.GetServerID(args[0]) - var server ScalewayServerPathNameDefinition + var server ScalewayServerPatchNameDefinition server.Name = args[1] err := cmd.API.PatchServerName(serverID, server) From 1aa16fa8d48643d2f5b9ee90e713b742e1b6eb14 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Thu, 11 Jun 2015 16:23:53 +0200 Subject: [PATCH 15/32] Added generic api.PatchServer method --- api.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/api.go b/api.go index 88f3ed77a6..2fb5832b40 100644 --- a/api.go +++ b/api.go @@ -380,6 +380,25 @@ type ScalewayServer struct { Organization string `json:"organization,omitempty"` } +// ScalewayServerPatchDefinition represents a Scaleway C1 server with nullable fields (for PATCH) +type ScalewayServerPatchDefinition struct { + Name *string `json:"name,omitempty"` + CreationDate *string `json:"creation_date,omitempty"` + ModificationDate *string `json:"modification_date,omitempty"` + Image *ScalewayImage `json:"image,omitempty"` + DynamicIPRequired *bool `json:"dynamic_ip_required,omitempty"` + PublicAddress *ScalewayIPAddress `json:"public_ip,omitempty"` + State *string `json:"state,omitempty"` + StateDetail *string `json:"state_detail,omitempty"` + PrivateIP *string `json:"private_ip,omitempty"` + Bootscript *ScalewayBootscript `json:"bootscript,omitempty"` + Hostname *string `json:"hostname,omitempty"` + Volumes *map[string]ScalewayVolume `json:"volumes,omitempty"` + SecurityGroup *ScalewaySecurityGroup `json:"security_group,omitempty"` + Organization *string `json:"organization,omitempty"` + //Tags *[]string `json:"tags",omitempty` +} + // ScalewayServerPatchNameDefinition represents a Scaleway C1 server with only its name as field type ScalewayServerPatchNameDefinition struct { // Name is the user-defined name of the server @@ -708,6 +727,32 @@ func (s *ScalewayAPI) PatchServerName(serverID string, definition ScalewayServer return error } +// PatchServer updates a server +func (s *ScalewayAPI) PatchServer(serverID string, definition ScalewayServerPatchDefinition) error { + resp, err := s.PatchResponse(fmt.Sprintf("servers/%s", serverID), definition) + if err != nil { + return err + } + + defer resp.Body.Close() + decoder := json.NewDecoder(resp.Body) + + // Succeed PATCH code + if resp.StatusCode == 200 { + return nil + } + + var error ScalewayAPIError + err = decoder.Decode(&error) + if err != nil { + return err + } + + error.StatusCode = resp.StatusCode + error.Debug() + return error +} + // PostSnapshot creates a new snapshot func (s *ScalewayAPI) PostSnapshot(volumeID string, name string) (string, error) { definition := ScalewaySnapshotDefinition{ From c6bf7323000c6ddb90e3c50d3f126e093b8367d3 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Thu, 11 Jun 2015 16:24:20 +0200 Subject: [PATCH 16/32] Added _patch method (#57) --- cli.go | 1 + patch.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 patch.go diff --git a/cli.go b/cli.go index 7a98890e28..c570721127 100644 --- a/cli.go +++ b/cli.go @@ -165,6 +165,7 @@ var commands = []*Command{ cmdKill, cmdLogin, cmdLogs, + cmdPatch, cmdPort, cmdPs, cmdRename, diff --git a/patch.go b/patch.go new file mode 100644 index 0000000000..3c0bb29693 --- /dev/null +++ b/patch.go @@ -0,0 +1,67 @@ +package main + +import ( + "fmt" + "strings" + + log "github.com/Sirupsen/logrus" +) + +var cmdPatch = &Command{ + Exec: runPatch, + UsageLine: "_patch [OPTIONS] IDENTIFIER FIELD=VALUE", + Description: "", + Hidden: true, + Help: "PATCH an object on the API", + Examples: ` + $ scw _patch myserver state_detail=booted +`, +} + +func init() { + cmdPatch.Flag.BoolVar(&patchHelp, []string{"h", "-help"}, false, "Print usage") +} + +// Flags +var patchHelp bool // -h, --help flag + +func runPatch(cmd *Command, args []string) { + if patchHelp { + cmd.PrintUsage() + } + if len(args) != 2 { + cmd.PrintShortUsage() + } + + // Parsing FIELD=VALUE + updateParts := strings.Split(args[1], "=") + if len(updateParts) != 2 { + cmd.PrintShortUsage() + } + fieldName := updateParts[0] + newValue := updateParts[1] + + ident := getIdentifier(cmd.API, args[0]) + switch ident.Type { + case IdentifierServer: + var payload ScalewayServerPatchDefinition + + switch fieldName { + case "state_detail": + payload.StateDetail = &newValue + case "name": + payload.Name = &newValue + log.Warnf("Use 'scw rename instead'") + default: + log.Fatalf("'_patch server %s=' not implemented", fieldName) + } + + err := cmd.API.PatchServer(ident.Identifier, payload) + if err != nil { + log.Fatalf("Cannot rename server: %v", err) + } + default: + log.Fatalf("_patch not implemented for this kind of object") + } + fmt.Println(ident.Identifier) +} From 03912411b066afba5e07101733c12223116939cd Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Thu, 11 Jun 2015 16:26:17 +0200 Subject: [PATCH 17/32] Rename now uses generic PatchServer --- api.go | 32 -------------------------------- rename.go | 8 ++++---- 2 files changed, 4 insertions(+), 36 deletions(-) diff --git a/api.go b/api.go index 2fb5832b40..fa35f42752 100644 --- a/api.go +++ b/api.go @@ -399,12 +399,6 @@ type ScalewayServerPatchDefinition struct { //Tags *[]string `json:"tags",omitempty` } -// ScalewayServerPatchNameDefinition represents a Scaleway C1 server with only its name as field -type ScalewayServerPatchNameDefinition struct { - // Name is the user-defined name of the server - Name string `json:"name"` -} - // ScalewayServerDefinition represents a Scaleway C1 server with image definition type ScalewayServerDefinition struct { // Name is the user-defined name of the server @@ -701,32 +695,6 @@ func (s *ScalewayAPI) PostServer(definition ScalewayServerDefinition) (string, e return "", error } -// PatchServerName changes the name of the server -func (s *ScalewayAPI) PatchServerName(serverID string, definition ScalewayServerPatchNameDefinition) error { - resp, err := s.PatchResponse(fmt.Sprintf("servers/%s", serverID), definition) - if err != nil { - return err - } - - defer resp.Body.Close() - decoder := json.NewDecoder(resp.Body) - - // Succeed PATCH code - if resp.StatusCode == 200 { - return nil - } - - var error ScalewayAPIError - err = decoder.Decode(&error) - if err != nil { - return err - } - - error.StatusCode = resp.StatusCode - error.Debug() - return error -} - // PatchServer updates a server func (s *ScalewayAPI) PatchServer(serverID string, definition ScalewayServerPatchDefinition) error { resp, err := s.PatchResponse(fmt.Sprintf("servers/%s", serverID), definition) diff --git a/rename.go b/rename.go index 32d5c5f27f..5d3ad7a415 100644 --- a/rename.go +++ b/rename.go @@ -26,13 +26,13 @@ func runRename(cmd *Command, args []string) { serverID := cmd.API.GetServerID(args[0]) - var server ScalewayServerPatchNameDefinition - server.Name = args[1] + var server ScalewayServerPatchDefinition + server.Name = &args[1] - err := cmd.API.PatchServerName(serverID, server) + err := cmd.API.PatchServer(serverID, server) if err != nil { log.Fatalf("Cannot rename server: %v", err) } else { - cmd.API.Cache.InsertServer(serverID, server.Name) + cmd.API.Cache.InsertServer(serverID, *server.Name) } } From 61548b55c89f1e581d8f06cc75cf8dc908ee91e1 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Thu, 11 Jun 2015 16:28:16 +0200 Subject: [PATCH 18/32] Updated changelog --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bed0e546ab..63cb8cc5b2 100644 --- a/README.md +++ b/README.md @@ -918,6 +918,7 @@ Development in progress #### Features +* Support of `_patch` experimental command ([#57](https://github.com/scaleway/scaleway-cli/issues/57)) * Support of `_completion` command (shell completion helper) ([#45](https://github.com/scaleway/scaleway-cli/issues/45)) * Returing more resource fields on `scw inspect` ([#50](https://github.com/scaleway/scaleway-cli/issues/50)) * Show public ip address in PORTS field in `scw ps` ([#54](https://github.com/scaleway/scaleway-cli/issues/54)) From 07676a9a52d0d17a9c9e32840fe09a1f9486dea1 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Thu, 11 Jun 2015 16:36:54 +0200 Subject: [PATCH 19/32] Using i386 version for x86_64 release until we fix the static build --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a8d53bce9a..6c7cb40136 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,8 +22,9 @@ ADD . /go/src/github.com/scaleway/scaleway-cli # Compile the binary and statically link RUN cd $APP_DIR && GOOS=darwin GOARCH=amd64 godep go build -a -v -ldflags '-d -w -s' -o /go/bin/scw-Darwin-x86_64 RUN cd $APP_DIR && GOOS=darwin GOARCH=386 godep go build -a -v -ldflags '-d -w -s' -o /go/bin/scw-Darwin-i386 -RUN cd $APP_DIR && GOOS=linux GOARCH=amd64 godep go build -a -v -ldflags '-w -s' -o /go/bin/scw-Linux-x86_64 RUN cd $APP_DIR && GOOS=linux GOARCH=386 godep go build -a -v -ldflags '-d -w -s' -o /go/bin/scw-Linux-i386 +#RUN cd $APP_DIR && GOOS=linux GOARCH=amd64 godep go build -a -v -ldflags '-w -s' -o /go/bin/scw-Linux-x86_64 +RUN cp /go/bin/scw-Linux-i386 /go/bin/scw-Linux-x86_64 RUN cd $APP_DIR && GOOS=linux GOARCH=arm GOARM=5 godep go build -a -v -ldflags '-d -w -s' -o /go/bin/scw-Linux-arm RUN cd $APP_DIR && GOOS=linux GOARCH=arm GOARM=6 godep go build -a -v -ldflags '-d -w -s' -o /go/bin/scw-Linux-armv6 RUN cd $APP_DIR && GOOS=linux GOARCH=arm GOARM=7 godep go build -a -v -ldflags '-d -w -s' -o /go/bin/scw-Linux-armv7 From 391e187a00f7b73e818e391100d565770aa30e68 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Thu, 11 Jun 2015 16:41:43 +0200 Subject: [PATCH 20/32] Added .exe extension to windows binary --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6c7cb40136..72bdfba360 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,6 +34,6 @@ RUN cd $APP_DIR && GOOS=freebsd GOARCH=arm GOARM=5 godep go build -a -v -ld #RUN cd $APP_DIR && GOOS=openbsd GOARCH=amd64 godep go build -a -v -ldflags '-w -s' -o /go/bin/scw-Openbsd-x86_64 #RUN cd $APP_DIR && GOOS=openbsd GOARCH=386 godep go build -a -v -ldflags '-d -w -s' -o /go/bin/scw-Openbsd-i386 #RUN cd $APP_DIR && GOOS=openbsd GOARCH=arm GOARM=5 godep go build -a -v -ldflags '-d -w -s' -o /go/bin/scw-Openbsd-arm -RUN cd $APP_DIR && GOOS=windows GOARCH=amd64 godep go build -a -v -ldflags '-w -s' -o /go/bin/scw-Windows-x86_64 +RUN cd $APP_DIR && GOOS=windows GOARCH=amd64 godep go build -a -v -ldflags '-w -s' -o /go/bin/scw-Windows-x86_64.exe #RUN cd $APP_DIR && GOOS=windows GOARCH=386 godep go build -a -v -ldflags '-d -w -s' -o /go/bin/scw-Windows-i386 #RUN cd $APP_DIR && GOOS=windows GOARCH=arm GOARM=5 godep go build -a -v -ldflags '-d -w -s' -o /go/bin/scw-Windows-arm From 11d5bddf8688038310e3166e7db7051717f8e90d Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Thu, 11 Jun 2015 16:43:31 +0200 Subject: [PATCH 21/32] Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 63cb8cc5b2..47af5a2c53 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,12 @@ curl -L https://github.com/scaleway/scaleway-cli/releases/download/v1.0.0/scw-`u chmod +x /usr/local/bin/scw ``` +To install Scaleway CLI master git, run the following command: + +```bash +go get github.com/scaleway/scaleway-cli +``` + ## Quick start Login From 84952271aecd72a48bf8e741a0c77043df227ff7 Mon Sep 17 00:00:00 2001 From: Julien Castets Date: Thu, 11 Jun 2015 16:47:30 +0200 Subject: [PATCH 22/32] Fix typo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 47af5a2c53..29a0d9471c 100644 --- a/README.md +++ b/README.md @@ -926,7 +926,7 @@ Development in progress * Support of `_patch` experimental command ([#57](https://github.com/scaleway/scaleway-cli/issues/57)) * Support of `_completion` command (shell completion helper) ([#45](https://github.com/scaleway/scaleway-cli/issues/45)) -* Returing more resource fields on `scw inspect` ([#50](https://github.com/scaleway/scaleway-cli/issues/50)) +* Returning more resource fields on `scw inspect` ([#50](https://github.com/scaleway/scaleway-cli/issues/50)) * Show public ip address in PORTS field in `scw ps` ([#54](https://github.com/scaleway/scaleway-cli/issues/54)) * Support of `inspect --format` option * Support of `exec --timeout` option ([#31](https://github.com/scaleway/scaleway-cli/issues/31)) From 2bcc368dbbd715fce493d7dd33241f01deff6c72 Mon Sep 17 00:00:00 2001 From: "s. rannou" Date: Fri, 12 Jun 2015 11:42:06 +0200 Subject: [PATCH 23/32] Do not mix src/dst spawned process when having two remote endpoints in scw cp. --- cp.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cp.go b/cp.go index 939d818c78..de7992a76b 100644 --- a/cp.go +++ b/cp.go @@ -80,23 +80,23 @@ func runCp(cmd *Command, args []string) { // execCmd contains the ssh connection + the remoteCommand execCmd := append(NewSSHExecCmd(server.PublicAddress.IP, false, remoteCommand)) log.Debugf("Executing: ssh %s", strings.Join(execCmd, " ")) - spawn := exec.Command("ssh", execCmd...) + spawnSrc := exec.Command("ssh", execCmd...) - tarOutputStream, err = spawn.StdoutPipe() + tarOutputStream, err = spawnSrc.StdoutPipe() if err != nil { log.Fatal(err) } - tarErrorStream, err = spawn.StderrPipe() + tarErrorStream, err = spawnSrc.StderrPipe() if err != nil { log.Fatal(err) } - err = spawn.Start() + err = spawnSrc.Start() if err != nil { log.Fatalf("Failed to start ssh command: %v", err) } - defer spawn.Wait() + defer spawnSrc.Wait() io.Copy(os.Stderr, tarErrorStream) } else if source == "-" { // stdin @@ -151,27 +151,27 @@ func runCp(cmd *Command, args []string) { // execCmd contains the ssh connection + the remoteCommand execCmd := append(NewSSHExecCmd(server.PublicAddress.IP, false, remoteCommand)) log.Debugf("Executing: ssh %s", strings.Join(execCmd, " ")) - spawn := exec.Command("ssh", execCmd...) + spawnDst := exec.Command("ssh", execCmd...) - untarInputStream, err := spawn.StdinPipe() + untarInputStream, err := spawnDst.StdinPipe() if err != nil { log.Fatal(err) } - untarErrorStream, err := spawn.StderrPipe() + untarErrorStream, err := spawnDst.StderrPipe() if err != nil { log.Fatal(err) } - untarOutputStream, err := spawn.StdoutPipe() + untarOutputStream, err := spawnDst.StdoutPipe() if err != nil { log.Fatal(err) } - err = spawn.Start() + err = spawnDst.Start() if err != nil { log.Fatalf("Failed to start ssh command: %v", err) } - defer spawn.Wait() + defer spawnDst.Wait() io.Copy(untarInputStream, tarOutputStream) io.Copy(os.Stderr, untarErrorStream) From 42f23425086cfa0a209bc1344b61f0cd1fe7b448 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Fri, 12 Jun 2015 12:46:00 +0200 Subject: [PATCH 24/32] Updated debug --- cp.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cp.go b/cp.go index de7992a76b..64a5a108d2 100644 --- a/cp.go +++ b/cp.go @@ -53,6 +53,7 @@ func runCp(cmd *Command, args []string) { // source source := args[0] if strings.Index(source, ":") > -1 { // source server address + log.Debugf("Creating a tarball remotely and streaming it using SSH") serverParts := strings.Split(args[0], ":") if len(serverParts) != 2 { log.Fatalf("usage: scw %s", cmd.UsageLine) @@ -66,6 +67,7 @@ func runCp(cmd *Command, args []string) { } dir, base := PathToTARPathparts(serverParts[1]) + log.Debugf("Kind of equivalent of 'scp root@%s:%s/%s ...'", server.PublicAddress.IP, dir, base) // remoteCommand is executed on the remote server // it streams a tarball raw content @@ -100,9 +102,10 @@ func runCp(cmd *Command, args []string) { io.Copy(os.Stderr, tarErrorStream) } else if source == "-" { // stdin + log.Debugf("Streaming tarball from stdin") tarOutputStream = os.Stdin } else { // source host path - log.Debugf("Creating tarball of local path %s", source) + log.Debugf("Taring local path %s", source) path, err := filepath.Abs(source) if err != nil { log.Fatalf("Cannot tar local path: %v", err) @@ -127,6 +130,7 @@ func runCp(cmd *Command, args []string) { // destination destination := args[1] if strings.Index(destination, ":") > -1 { // destination server address + log.Debugf("Streaming using ssh and untaring remotely") serverParts := strings.Split(destination, ":") if len(serverParts) != 2 { log.Fatalf("usage: scw %s", cmd.UsageLine) @@ -185,6 +189,7 @@ func runCp(cmd *Command, args []string) { } } else { // destination host path + log.Debugf("Untaring to local path: %s", destination) err := archive.Untar(tarOutputStream, destination, &archive.TarOptions{NoLchown: true}) if err != nil { log.Fatalf("Failed to untar the remote archive: %v", err) From fd4a0f5a23e2b8a18c0f7568089af501191fbb1d Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Fri, 12 Jun 2015 14:01:41 +0200 Subject: [PATCH 25/32] Refactored cp (#56) --- cp.go | 174 ++++++++++++++++++++++++++++++++++------------------------ 1 file changed, 102 insertions(+), 72 deletions(-) diff --git a/cp.go b/cp.go index 64a5a108d2..00dd289344 100644 --- a/cp.go +++ b/cp.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "io" "os" "os/exec" @@ -39,35 +40,26 @@ func init() { // Flags var cpHelp bool // -h, --help flag -func runCp(cmd *Command, args []string) { - if cpHelp { - cmd.PrintUsage() - } - if len(args) != 2 { - cmd.PrintShortUsage() - } - +func TarFromSource(api *ScalewayAPI, source string) (*io.ReadCloser, error) { var tarOutputStream io.ReadCloser - var tarErrorStream io.ReadCloser - // source - source := args[0] - if strings.Index(source, ":") > -1 { // source server address + // source is a server address + path (scp-like uri) + if strings.Index(source, ":") > -1 { log.Debugf("Creating a tarball remotely and streaming it using SSH") - serverParts := strings.Split(args[0], ":") + serverParts := strings.Split(source, ":") if len(serverParts) != 2 { - log.Fatalf("usage: scw %s", cmd.UsageLine) + return nil, fmt.Errorf("invalid source uri, see 'scw cp -h' for usage") } - serverID := cmd.API.GetServerID(serverParts[0]) + serverID := api.GetServerID(serverParts[0]) - server, err := cmd.API.GetServer(serverID) + server, err := api.GetServer(serverID) if err != nil { - log.Fatalf("Failed to get server information for %s: %v", serverID, err) + return nil, err } dir, base := PathToTARPathparts(serverParts[1]) - log.Debugf("Kind of equivalent of 'scp root@%s:%s/%s ...'", server.PublicAddress.IP, dir, base) + log.Debugf("Equivalent to 'scp root@%s:%s/%s ...'", server.PublicAddress.IP, dir, base) // remoteCommand is executed on the remote server // it streams a tarball raw content @@ -86,61 +78,73 @@ func runCp(cmd *Command, args []string) { tarOutputStream, err = spawnSrc.StdoutPipe() if err != nil { - log.Fatal(err) + return nil, err } - tarErrorStream, err = spawnSrc.StderrPipe() + defer tarOutputStream.Close() + + tarErrorStream, err := spawnSrc.StderrPipe() if err != nil { - log.Fatal(err) + return nil, err } + defer tarErrorStream.Close() + io.Copy(os.Stderr, tarErrorStream) err = spawnSrc.Start() if err != nil { - log.Fatalf("Failed to start ssh command: %v", err) + return nil, err } - defer spawnSrc.Wait() - io.Copy(os.Stderr, tarErrorStream) - } else if source == "-" { // stdin + return &tarOutputStream, nil + } + + // source is stdin + if source == "-" { log.Debugf("Streaming tarball from stdin") tarOutputStream = os.Stdin - } else { // source host path - log.Debugf("Taring local path %s", source) - path, err := filepath.Abs(source) - if err != nil { - log.Fatalf("Cannot tar local path: %v", err) - } - path, err = filepath.EvalSymlinks(path) - if err != nil { - log.Fatalf("Cannot tar local path: %v", err) - } - log.Debugf("Real local path is %s", path) + defer tarOutputStream.Close() + return &tarOutputStream, nil + } - dir, base := PathToTARPathparts(path) + // source is a path on localhost + log.Debugf("Taring local path %s", source) + path, err := filepath.Abs(source) + if err != nil { + return nil, err + } + path, err = filepath.EvalSymlinks(path) + if err != nil { + return nil, err + } + log.Debugf("Real local path is %s", path) - tarOutputStream, err = archive.TarWithOptions(dir, &archive.TarOptions{ - Compression: archive.Uncompressed, - IncludeFiles: []string{base}, - }) - if err != nil { - log.Fatalf("Cannot tar local path: %v", err) - } + dir, base := PathToTARPathparts(path) + + tarOutputStream, err = archive.TarWithOptions(dir, &archive.TarOptions{ + Compression: archive.Uncompressed, + IncludeFiles: []string{base}, + }) + if err != nil { + return nil, err } + defer tarOutputStream.Close() + return &tarOutputStream, nil +} - // destination - destination := args[1] - if strings.Index(destination, ":") > -1 { // destination server address +func UntarToDest(api *ScalewayAPI, sourceStream *io.ReadCloser, destination string) error { + // destination is a server address + path (scp-like uri) + if strings.Index(destination, ":") > -1 { log.Debugf("Streaming using ssh and untaring remotely") serverParts := strings.Split(destination, ":") if len(serverParts) != 2 { - log.Fatalf("usage: scw %s", cmd.UsageLine) + return fmt.Errorf("invalid destination uri, see 'scw cp -h' for usage") } - serverID := cmd.API.GetServerID(serverParts[0]) + serverID := api.GetServerID(serverParts[0]) - server, err := cmd.API.GetServer(serverID) + server, err := api.GetServer(serverID) if err != nil { - log.Fatalf("Failed to get server information for %s: %v", serverID, err) + return err } // remoteCommand is executed on the remote server @@ -150,7 +154,7 @@ func runCp(cmd *Command, args []string) { if os.Getenv("DEBUG") == "1" { remoteCommand = append(remoteCommand, "-v") } - remoteCommand = append(remoteCommand, "-tf", "-") + remoteCommand = append(remoteCommand, "-xf", "-") // execCmd contains the ssh connection + the remoteCommand execCmd := append(NewSSHExecCmd(server.PublicAddress.IP, false, remoteCommand)) @@ -159,40 +163,66 @@ func runCp(cmd *Command, args []string) { untarInputStream, err := spawnDst.StdinPipe() if err != nil { - log.Fatal(err) + return err } + defer untarInputStream.Close() + untarErrorStream, err := spawnDst.StderrPipe() if err != nil { - log.Fatal(err) + return err } + defer untarErrorStream.Close() + untarOutputStream, err := spawnDst.StdoutPipe() if err != nil { - log.Fatal(err) + return err } + defer untarOutputStream.Close() err = spawnDst.Start() if err != nil { - log.Fatalf("Failed to start ssh command: %v", err) + return err } - defer spawnDst.Wait() - io.Copy(untarInputStream, tarOutputStream) + io.Copy(untarInputStream, *sourceStream) io.Copy(os.Stderr, untarErrorStream) - io.Copy(os.Stdout, untarOutputStream) - } else if destination == "-" { // stdout - log.Debugf("Writing tarOutputStream(%v) to os.Stdout(%v)", tarOutputStream, os.Stdout) - written, err := io.Copy(os.Stdout, tarOutputStream) - log.Debugf("%d bytes written", written) - if err != nil { - log.Fatal(err) - } + _, err = io.Copy(os.Stdout, untarOutputStream) + return err + } - } else { // destination host path - log.Debugf("Untaring to local path: %s", destination) - err := archive.Untar(tarOutputStream, destination, &archive.TarOptions{NoLchown: true}) - if err != nil { - log.Fatalf("Failed to untar the remote archive: %v", err) - } + // destination is stdout + if destination == "-" { // stdout + log.Debugf("Writing sourceStream(%v) to os.Stdout(%v)", sourceStream, os.Stdout) + _, err := io.Copy(os.Stdout, *sourceStream) + return err + } + + // destination is a path on localhost + log.Debugf("Untaring to local path: %s", destination) + err := archive.Untar(*sourceStream, destination, &archive.TarOptions{NoLchown: true}) + return err +} + +func runCp(cmd *Command, args []string) { + if cpHelp { + cmd.PrintUsage() + } + if len(args) != 2 { + cmd.PrintShortUsage() + } + + if strings.Count(args[0], ":") > 1 || strings.Count(args[1], ":") > 1 { + log.Fatalf("usage: scw %s", cmd.UsageLine) + } + + sourceStream, err := TarFromSource(cmd.API, args[0]) + if err != nil { + log.Fatalf("Cannot tar from source '%s': %v", args[0], err) + } + + err = UntarToDest(cmd.API, sourceStream, args[1]) + if err != nil { + log.Fatalf("Cannot untar to destionation '%s': %v", args[1], err) } } From 0f910d5524990db45e67919c9ab397a545c9a8c4 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Fri, 12 Jun 2015 14:21:43 +0200 Subject: [PATCH 26/32] Removed spinlocks --- cp.go | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/cp.go b/cp.go index 00dd289344..90ff592551 100644 --- a/cp.go +++ b/cp.go @@ -80,7 +80,6 @@ func TarFromSource(api *ScalewayAPI, source string) (*io.ReadCloser, error) { if err != nil { return nil, err } - defer tarOutputStream.Close() tarErrorStream, err := spawnSrc.StderrPipe() if err != nil { @@ -102,7 +101,6 @@ func TarFromSource(api *ScalewayAPI, source string) (*io.ReadCloser, error) { if source == "-" { log.Debugf("Streaming tarball from stdin") tarOutputStream = os.Stdin - defer tarOutputStream.Close() return &tarOutputStream, nil } @@ -127,7 +125,6 @@ func TarFromSource(api *ScalewayAPI, source string) (*io.ReadCloser, error) { if err != nil { return nil, err } - defer tarOutputStream.Close() return &tarOutputStream, nil } @@ -167,27 +164,15 @@ func UntarToDest(api *ScalewayAPI, sourceStream *io.ReadCloser, destination stri } defer untarInputStream.Close() - untarErrorStream, err := spawnDst.StderrPipe() - if err != nil { - return err - } - defer untarErrorStream.Close() - - untarOutputStream, err := spawnDst.StdoutPipe() - if err != nil { - return err - } - defer untarOutputStream.Close() + // spawnDst.Stderr = os.Stderr + // spawnDst.Stdout = os.Stdout err = spawnDst.Start() if err != nil { return err } - defer spawnDst.Wait() - io.Copy(untarInputStream, *sourceStream) - io.Copy(os.Stderr, untarErrorStream) - _, err = io.Copy(os.Stdout, untarOutputStream) + _, err = io.Copy(untarInputStream, *sourceStream) return err } @@ -223,6 +208,6 @@ func runCp(cmd *Command, args []string) { err = UntarToDest(cmd.API, sourceStream, args[1]) if err != nil { - log.Fatalf("Cannot untar to destionation '%s': %v", args[1], err) + log.Fatalf("Cannot untar to destination '%s': %v", args[1], err) } } From 2608b9d7f51cd10d801f489f72411317d87063f7 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Thu, 11 Jun 2015 17:26:35 +0200 Subject: [PATCH 27/32] golint fixes --- api.go | 2 +- api_helpers.go | 4 ++-- cli.go | 2 +- utils.go | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api.go b/api.go index fa35f42752..f337030cbf 100644 --- a/api.go +++ b/api.go @@ -1094,7 +1094,7 @@ func (s *ScalewayAPI) CheckCredentials() error { return err } if resp.StatusCode != 200 { - return fmt.Errorf("Invalid credentials") + return fmt.Errorf("invalid credentials") } return nil } diff --git a/api_helpers.go b/api_helpers.go index 74b8dedadb..41685e7d52 100644 --- a/api_helpers.go +++ b/api_helpers.go @@ -300,14 +300,14 @@ func startServer(api *ScalewayAPI, needle string, wait bool) error { err := api.PostServerAction(server, "poweron") if err != nil { if err.Error() != "server should be stopped" { - return fmt.Errorf("Server %s is already started: %v", server, err) + return fmt.Errorf("server %s is already started: %v", server, err) } } if wait { _, err = WaitForServerReady(api, server) if err != nil { - return fmt.Errorf("Failed to wait for server %s to be ready, %v", needle, err) + return fmt.Errorf("failed to wait for server %s to be ready, %v", needle, err) } } return nil diff --git a/cli.go b/cli.go index c570721127..b781826179 100644 --- a/cli.go +++ b/cli.go @@ -274,7 +274,7 @@ func GetConfigFilePath() (string, error) { homeDir = os.Getenv("USERPROFILE") } if homeDir == "" { - return "", errors.New("User home directory not found.") + return "", errors.New("user home directory not found") } return filepath.Join(homeDir, ".scwrc"), nil diff --git a/utils.go b/utils.go index 9122682299..5dafcbe798 100644 --- a/utils.go +++ b/utils.go @@ -16,12 +16,12 @@ import ( func sshExec(ipAddress string, command []string, checkConnection bool) error { if ipAddress == "" { - return errors.New("Server does not have public IP") + return errors.New("server does not have public IP") } if checkConnection { if !IsTCPPortOpen(fmt.Sprintf("%s:22", ipAddress)) { - return errors.New("Server is not ready, try again later.") + return errors.New("server is not ready, try again later") } } From 9ecfe878597d8711883b26871b87ee13424accef Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Thu, 11 Jun 2015 17:29:12 +0200 Subject: [PATCH 28/32] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 29a0d9471c..278fd9a5d9 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ Interact with Scaleway API from the command line. [![Build Status (Travis)](https://img.shields.io/travis/scaleway/scaleway-cli.svg)](https://travis-ci.org/scaleway/scaleway-cli) [![GoDoc](https://godoc.org/github.com/scaleway/scaleway-cli?status.svg)](https://godoc.org/github.com/scaleway/scaleway-cli) +![License](https://img.shields.io/github/license/scaleway/scaleway-cli.svg) For node version, check out [scaleway-cli-node](https://github.com/moul/scaleway-cli-node). From ec8c96b0cbf7316eba04b8ffafd9387e653f1d26 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Fri, 12 Jun 2015 10:19:47 +0200 Subject: [PATCH 29/32] Improved help messages for login (#59) --- login.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/login.go b/login.go index d0f2a1e4d2..165b5be5a8 100644 --- a/login.go +++ b/login.go @@ -18,7 +18,10 @@ var cmdLogin = &Command{ Description: "Log in to Scaleway API", Help: `Generates a configuration file in '/home/$USER/.scwrc' containing credentials used to interact with the Scaleway API. This -configuration file is automatically used by the 'scw' commands.`, +configuration file is automatically used by the 'scw' commands. + +You can get your credentials on https://cloud.scaleway.com/#/credentials +`, } func promptUser(prompt string, output *string, echo bool) { @@ -58,7 +61,8 @@ func runLogin(cmd *Command, args []string) { } if len(organization) == 0 { - promptUser("Organization: ", &organization, true) + fmt.Println("You can get your credentials on https://cloud.scaleway.com/#/credentials") + promptUser("Organization (access key): ", &organization, true) } if len(token) == 0 { promptUser("Token: ", &token, false) From a43079186cf3a2a3d9c5739a69ad17013d1359b4 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Fri, 12 Jun 2015 10:38:27 +0200 Subject: [PATCH 30/32] Added 'scw logout' command --- cli.go | 2 ++ logout.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 logout.go diff --git a/cli.go b/cli.go index b781826179..aca5aeb57b 100644 --- a/cli.go +++ b/cli.go @@ -164,6 +164,7 @@ var commands = []*Command{ cmdInspect, cmdKill, cmdLogin, + cmdLogout, cmdLogs, cmdPatch, cmdPort, @@ -220,6 +221,7 @@ func main() { usage() } name := args[0] + args = args[1:] for _, cmd := range commands { diff --git a/logout.go b/logout.go new file mode 100644 index 0000000000..00ee7d9ba3 --- /dev/null +++ b/logout.go @@ -0,0 +1,42 @@ +package main + +import ( + "os" + + log "github.com/Sirupsen/logrus" +) + +var cmdLogout = &Command{ + Exec: runLogout, + UsageLine: "logout [OPTIONS]", + Description: "Log out from the Scaleway API", + Help: "Log out from the Scaleway API.", +} + +func init() { + cmdLogout.Flag.BoolVar(&logoutHelp, []string{"h", "-help"}, false, "Print usage") +} + +// FLags +var logoutHelp bool // -h, --help flag + +func runLogout(cmd *Command, args []string) { + if logoutHelp { + cmd.PrintUsage() + } + if len(args) != 0 { + cmd.PrintShortUsage() + } + + scwrcPath, err := GetConfigFilePath() + if err != nil { + log.Fatalf("Unable to get scwrc config file path: %v", err) + } + + if _, err = os.Stat(scwrcPath); err == nil { + err = os.Remove(scwrcPath) + if err != nil { + log.Fatalf("Unable to remove scwrc config file: %v", err) + } + } +} From 48d2266429ad451258e8791867d2fa8e1a13c7bf Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Fri, 12 Jun 2015 10:41:25 +0200 Subject: [PATCH 31/32] Priting login error early (#59) --- cli.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cli.go b/cli.go index aca5aeb57b..ac622e71a5 100644 --- a/cli.go +++ b/cli.go @@ -231,9 +231,12 @@ func main() { if err != nil { log.Fatalf("usage: scw %s", cmd.UsageLine) } - if cmd.Name() != "login" { + if cmd.Name() != "login" && cmd.Name() != "help" { if cfgErr != nil { - log.Fatalf("Unable to open .scwrc config file: %v", cfgErr) + if name != "login" && config == nil { + fmt.Fprintf(os.Stderr, "You need to login first: 'scw login'\n") + os.Exit(1) + } } api, err := getScalewayAPI() if err != nil { From c8535d30606c093465c5ce5b59b47d6a5cf3d5fc Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Fri, 12 Jun 2015 14:28:59 +0200 Subject: [PATCH 32/32] Updated README.md --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 278fd9a5d9..3eb3933da0 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,21 @@ running the command. Use '-' to write the data as a tar file to STDOUT. Options: -h, --help=false Print usage + +Examples: + + $ scw cp path/to/my/local/file myserver:path + $ scw cp myserver:path/to/file path/to/my/local/dir + $ scw cp myserver:path/to/file myserver2:path/to/dir + $ scw cp myserver:path/to/file - > myserver-pathtofile-backup.tar + $ scw cp myserver:path/to/file - | tar -tvf - + $ scw cp path/to/my/local/dir myserver:path + $ scw cp myserver:path/to/dir path/to/my/local/dir + $ scw cp myserver:path/to/dir myserver2:path/to/dir + $ scw cp myserver:path/to/dir - > myserver-pathtodir-backup.tar + $ scw cp myserver:path/to/dir - | tar -tvf - + $ cat archive.tar | scw cp - myserver:/path + $ tar -cvf - . | scw cp - myserver:path ``` @@ -925,6 +940,7 @@ Development in progress #### Features +* Support of `scw cp` from {server-path,local-path,stdin} to {server-path,local-path,stdout} ([#56](https://github.com/scaleway/scaleway-cli/issues/56)) * Support of `_patch` experimental command ([#57](https://github.com/scaleway/scaleway-cli/issues/57)) * Support of `_completion` command (shell completion helper) ([#45](https://github.com/scaleway/scaleway-cli/issues/45)) * Returning more resource fields on `scw inspect` ([#50](https://github.com/scaleway/scaleway-cli/issues/50))