Add SessionManager implementation 35/162035/15
authorAdam Malinowski <a.malinowsk2@partner.samsung.com>
Thu, 23 Nov 2017 09:18:23 +0000 (10:18 +0100)
committerPawel Wieczorek <p.wieczorek2@samsung.com>
Thu, 14 Jun 2018 10:13:38 +0000 (12:13 +0200)
MuxPi is needed to perform tests. SSH key needs to be generated and
its private part has to be saved to a local file. To run tests:

ginkgo manager/dryad -- \
    -keyFile=file \
    -address=address \
    -port=port \
    -userName=login

Where:
  file    - path to a file containing private part of SSH key generated
            on Dryad
  address - IP address of Dryad
  port    - SSH port to use for connection with Dryad (default: 22)
  login   - user name for connection with Dryad

Change-Id: I1b4cf55f8082b5b0d9ec1d0311f37f87f35fb753
Signed-off-by: Adam Malinowski <a.malinowsk2@partner.samsung.com>
Signed-off-by: Aleksander Mistewicz <a.mistewicz@samsung.com>
manager/dryad/dryad_session_provider.go
manager/dryad/dryad_session_provider_test.go [new file with mode: 0644]
manager/dryad/dryad_suite_test.go [new file with mode: 0644]
manager/dryad/error.go [new file with mode: 0644]

index 55707fb..19e2657 100644 (file)
 package dryad
 
 import (
+       "bytes"
+       "time"
+
+       "strings"
+
+       "crypto/rsa"
+
+       "fmt"
+
        . "git.tizen.org/tools/weles"
+       "golang.org/x/crypto/ssh"
+)
+
+const (
+       stmCommand = "/usr/local/stm"
 )
 
-// NewSessionProvider is a stub.
+type sshClient struct {
+       config *ssh.ClientConfig
+       client *ssh.Client
+}
+
+// sessionProvider implements SessionProvider interface.
+// FIXME: When the connection is broken after it is established, all client functions stall. This provider has to be rewritten.
+type sessionProvider struct {
+       SessionProvider
+       dryad      Dryad
+       connection *sshClient
+}
+
+func prepareSSHConfig(userName string, key rsa.PrivateKey) *ssh.ClientConfig {
+       signer, _ := ssh.NewSignerFromKey(&key)
+
+       return &ssh.ClientConfig{
+               User: userName,
+               Auth: []ssh.AuthMethod{
+                       ssh.PublicKeys(signer),
+               },
+               HostKeyCallback: ssh.InsecureIgnoreHostKey(),
+               Timeout:         30 * time.Second, // TODO: Use value from config when such appears.
+       }
+}
+
+func (d *sessionProvider) connect() (err error) {
+       d.connection.client, err = ssh.Dial("tcp", d.dryad.Addr.String(), d.connection.config)
+       return
+}
+
+func (d *sessionProvider) newSession() (*ssh.Session, error) {
+       if d.connection.client == nil {
+               err := d.connect()
+               if err != nil {
+                       return nil, err
+               }
+       }
+
+       session, err := d.connection.client.NewSession()
+       if err != nil {
+               return nil, err
+       }
+
+       return session, nil
+}
+
+func (d *sessionProvider) executeRemoteCommand(cmd string) ([]byte, []byte, error) {
+       session, err := d.newSession()
+       if err != nil {
+               return nil, nil, err
+       }
+       defer session.Close()
+
+       var stdout, stderr bytes.Buffer
+       session.Stdout = &stdout
+       session.Stderr = &stderr
+
+       err = session.Run(cmd)
+       return stdout.Bytes(), stderr.Bytes(), err
+}
+
+// NewSessionProvider returns new instance of SessionProvider.
 func NewSessionProvider(dryad Dryad) SessionProvider {
+       cfg := prepareSSHConfig(dryad.Username, dryad.Key)
+
+       return &sessionProvider{
+               dryad: dryad,
+               connection: &sshClient{
+                       config: cfg,
+               },
+       }
+}
+
+// Exec is a part of SessionProvider interface.
+// FIXME: Exec function checks every argument and if contains space (except surrounding ones) surrounds the argument
+// with double quotes. Caller must be aware of such functionality because it may break some special arguments.
+func (d *sessionProvider) Exec(cmd []string) (stdout, stderr []byte, err error) {
+       joinedCommand := cmd[0] + " "
+       for i := 1; i < len(cmd); i++ {
+               if strings.Contains(strings.Trim(cmd[i], " "), " ") {
+                       joinedCommand += `"` + cmd[i] + `" `
+               } else {
+                       joinedCommand += cmd[i] + " "
+               }
+       }
+       return d.executeRemoteCommand(joinedCommand)
+}
+
+// DUT is a part of SessionProvider interface.
+// This function requires 'stm' binary on MuxPi's NanoPi.
+func (d *sessionProvider) DUT() error {
+       _, stderr, err := d.executeRemoteCommand(stmCommand + " -dut")
+       if err != nil {
+               return fmt.Errorf("DUT command failed: %s : %s", err, stderr)
+       }
        return nil
 }
+
+// TS is a part of SessionProvider interface.
+// This function requires 'stm' binary on MuxPi's NanoPi.
+func (d *sessionProvider) TS() error {
+       _, stderr, err := d.executeRemoteCommand(stmCommand + " -ts")
+       if err != nil {
+               return fmt.Errorf("TS command failed: %s : %s", err, stderr)
+       }
+       return nil
+}
+
+// PowerTick is a part of SessionProvider interface.
+// This function requires 'stm' binary on MuxPi's NanoPi.
+func (d *sessionProvider) PowerTick() error {
+       _, stderr, err := d.executeRemoteCommand(stmCommand + " -tick")
+       if err != nil {
+               return fmt.Errorf("PowerTick command failed: %s : %s", err, stderr)
+       }
+       return nil
+}
+
+// Close is a part of SessionProvider interface.
+func (d *sessionProvider) Close() error {
+       if d.connection.client == nil {
+               return nil
+       }
+
+       err := d.connection.client.Close()
+       d.connection.client = nil
+       return err
+}
diff --git a/manager/dryad/dryad_session_provider_test.go b/manager/dryad/dryad_session_provider_test.go
new file mode 100644 (file)
index 0000000..48c30c6
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ *  Copyright (c) 2017 Samsung Electronics Co., Ltd 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 dryad
+
+import (
+       "strings"
+
+       . "github.com/onsi/ginkgo"
+       . "github.com/onsi/gomega"
+)
+
+var _ = Describe("SessionProvider", func() {
+       var sp SessionProvider
+
+       BeforeEach(func() {
+               if !accessInfoGiven {
+                       Skip("No valid access info to Dryad")
+               }
+               sp = NewSessionProvider(dryadInfo)
+       })
+
+       AfterEach(func() {
+               sp.Close()
+       })
+
+       It("should write poem to a file", func() {
+               stdout, stderr, err := sp.Exec([]string{"echo", flyingCows, " > ", flyingCowsPath})
+               Expect(err).ToNot(HaveOccurred())
+               Expect(stdout).To(BeEmpty())
+               Expect(stderr).To(BeEmpty())
+       })
+
+       It("should read poem from a file", func() {
+               stdout, stderr, err := sp.Exec([]string{"cat", flyingCowsPath})
+               Expect(err).ToNot(HaveOccurred())
+               Expect(strings.TrimRight(string(stdout), "\n")).To(BeIdenticalTo(flyingCows))
+               Expect(stderr).To(BeEmpty())
+       })
+
+       It("should not read poem from nonexistent file", func() {
+               stdout, stderr, err := sp.Exec([]string{"cat", "/Ihopethispathdoesnotexist/" + flyingCowsPath + ".txt"})
+               Expect(err).To(HaveOccurred())
+               Expect(stdout).To(BeEmpty())
+               Expect(stderr).ToNot(BeEmpty())
+       })
+
+       It("should switch to DUT", func() {
+               Expect(sp.DUT()).ToNot(HaveOccurred())
+       })
+
+       It("should tick DUT's power supply", func() {
+               Expect(sp.PowerTick()).ToNot(HaveOccurred())
+       })
+
+       It("should switch to TS", func() {
+               Expect(sp.TS()).ToNot(HaveOccurred())
+       })
+})
diff --git a/manager/dryad/dryad_suite_test.go b/manager/dryad/dryad_suite_test.go
new file mode 100644 (file)
index 0000000..45d2608
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ *  Copyright (c) 2017 Samsung Electronics Co., Ltd 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 dryad
+
+import (
+       . "github.com/onsi/ginkgo"
+       . "github.com/onsi/gomega"
+
+       "flag"
+       "io/ioutil"
+       "net"
+       "testing"
+
+       "crypto/x509"
+
+       "encoding/pem"
+
+       . "git.tizen.org/tools/weles"
+)
+
+func TestManager(t *testing.T) {
+       RegisterFailHandler(Fail)
+       RunSpecs(t, "Dryad Suite")
+}
+
+var (
+       dryadAddress string
+       port         int
+       userName     string
+       keyFile      string
+
+       flyingCows = `Cows called Daisy
+Are often lazy.
+But cows called Brian
+They be flyin'
+Up in the air
+And out into space
+Because of the grass
+And the gasses it makes!`
+
+       flyingCowsPath = "/tmp/flyingCow.txt"
+
+       dryadInfo       Dryad
+       accessInfoGiven bool
+)
+
+func init() {
+       flag.StringVar(&dryadAddress, "address", "", "IP address to dryad")
+       flag.IntVar(&port, "port", 22, "SSH port to use for connection")
+       flag.StringVar(&userName, "userName", "", "user name")
+       flag.StringVar(&keyFile, "keyFile", "", "path to file containing private part of ssh key")
+}
+
+var _ = BeforeSuite(func() {
+       if dryadAddress != "" && userName != "" && keyFile != "" {
+               accessInfoGiven = true
+               strkey, err := ioutil.ReadFile(keyFile)
+               if err != nil {
+                       Skip("Error reading key file: " + err.Error())
+               }
+
+               block, _ := pem.Decode(strkey)
+               if block == nil {
+                       Skip("Error decoding PEM block from key file contents")
+               }
+
+               key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
+               if err != nil {
+                       Skip("Error parsing key file: " + err.Error())
+               }
+
+               dryadInfo = Dryad{
+                       Addr:     &net.TCPAddr{net.ParseIP(dryadAddress), port, ""},
+                       Username: userName,
+                       Key:      *key,
+               }
+       }
+})
diff --git a/manager/dryad/error.go b/manager/dryad/error.go
new file mode 100644 (file)
index 0000000..20de442
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ *  Copyright (c) 2017 Samsung Electronics Co., Ltd 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
+ */
+
+// File manager/dryad/error.go contains definitions of errors.
+
+package dryad
+
+import "errors"
+
+var (
+       // ErrConnectionClosed is returned when caller tries to close already closed connection to Dryad.
+       ErrConnectionClosed = errors.New("attempt to close already closed connection")
+)