Add SessionManager implementation 35/162035/9
authorAdam Malinowski <a.malinowsk2@partner.samsung.com>
Thu, 23 Nov 2017 09:18:23 +0000 (10:18 +0100)
committerAdam Malinowski <a.malinowsk2@partner.samsung.com>
Fri, 8 Dec 2017 09:08:16 +0000 (10:08 +0100)
MuxPi is needed to perform tests. SSH key needs to be generates and
its private part has to be saved to a local file. To run tests:
dryad.test --keyFile=file --address=address --userName=login
Where:
  file    - path to a file containing private part of ssh key generated
            on dryad
  address - IP address of dryad. Do not specify port, it is hardcoded
            as 22
  login   - user name

Change-Id: I1b4cf55f8082b5b0d9ec1d0311f37f87f35fb753
Signed-off-by: Adam Malinowski <a.malinowsk2@partner.samsung.com>
dryadjobmanager.go
manager/dryad/dryad.go
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]
manager/dryad_job.go

index 1a1b79a..0702ec0 100644 (file)
 
 package weles
 
+import (
+       "crypto/rsa"
+       "net"
+)
+
 // DryadJobStatus is a representation of current state of DryadJob.
 type DryadJobStatus string
 
@@ -48,7 +53,14 @@ type DryadJobStatusChange DryadJobInfo
 
 // Dryad contains information about device allocated for Job
 // and credentials required to use it.
-type Dryad struct{}
+type Dryad struct {
+       // Address to Dryad.
+       Address net.Addr
+       // Username used to initialize ssh connection to Dryad
+       Username string
+       // Key is a private RSA key
+       Key rsa.PrivateKey
+}
 
 // DryadJobFilter is used by List to access only jobs of interest.
 //
index 3af47aa..65851c4 100644 (file)
@@ -85,6 +85,8 @@ type DeviceCommunicationProvider interface {
        // command may be terminated if the stdout and stderr is too large, err will be set.
        //
        // Large outputs should be redirected to files.
+       // 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 brake some special arguments.
        Exec(cmd []string, timeout time.Duration) (stdout, stderr []byte, err error)
 
        // Close terminates session to Device.
index 55707fb..7fea8a0 100644 (file)
 
 package dryad
 
+// FIXME: When the connection is broken after it is established, all client functions stalls. This provider has to be rewritten.
+
 import (
+       "bytes"
+       "time"
+
+       "strings"
+
+       "crypto/rsa"
+
        . "git.tizen.org/tools/weles"
+       "golang.org/x/crypto/ssh"
 )
 
-// NewSessionProvider is a stub.
-func NewSessionProvider(dryad Dryad) SessionProvider {
-       return nil
+type sshClient struct {
+       config *ssh.ClientConfig
+       client *ssh.Client
+}
+
+// sessionProvider implements SessionProvider interface.
+type sessionProvider struct {
+       SessionProvider
+       dryad      Dryad
+       connection *sshClient
+}
+
+func prepareSSHConfig(userName string, key rsa.PrivateKey) (*ssh.ClientConfig, error) {
+       signer, err := ssh.NewSignerFromKey(&key)
+       if err != nil {
+               return nil, err
+       }
+
+       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.
+       }, nil
+}
+
+func (d *sessionProvider) connect() (err error) {
+       d.connection.client, err = ssh.Dial("tcp", d.dryad.Address.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 or nil if error occurs.
+func NewSessionProvider(dryad Dryad) (SessionProvider, error) {
+       cfg, err := prepareSSHConfig(dryad.Username, dryad.Key)
+       if err != nil {
+               return nil, err
+       }
+
+       return &sessionProvider{
+               dryad: dryad,
+               connection: &sshClient{
+                       config: cfg,
+                       client: nil,
+               },
+       }, nil
+}
+
+// Exec is a part of SessionProvider interface.
+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.
+func (d *sessionProvider) DUT() error {
+       _, _, err := d.executeRemoteCommand("stm -dut")
+       return err
+}
+
+// TS is a part of SessionProvider interface.
+func (d *sessionProvider) TS() error {
+       _, _, err := d.executeRemoteCommand("stm -ts")
+       return err
+}
+
+// Close is a part of SessionProvider interface.
+func (d *sessionProvider) PowerTick() error {
+       _, _, err := d.executeRemoteCommand("stm -tick")
+       return err
+}
+
+// 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..68604ea
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ *  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_test
+
+import (
+       "strings"
+
+       "git.tizen.org/tools/weles/manager/dryad"
+       . "github.com/onsi/ginkgo"
+       . "github.com/onsi/gomega"
+)
+
+var _ = Describe("SessionProvider", func() {
+       var sp dryad.SessionProvider
+
+       BeforeEach(func() {
+               if !accessInfoGiven {
+                       Skip("No valid access info to Dryad")
+               }
+               sp, _ = dryad.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", "/Ihopethispathdoesnotexit/" + 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..9a44522
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ *  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_test
+
+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
+       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.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 reading key file: " + err.Error())
+               }
+
+               key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
+               if err != nil {
+                       Skip("Error parsing key file: " + err.Error())
+               }
+
+               dryadInfo = Dryad{
+                       Address:  &net.TCPAddr{net.ParseIP(dryadAddress), 22, ""},
+                       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")
+)
index 4c4a32f..c56134c 100644 (file)
@@ -55,7 +55,7 @@ func newDryadJobWithCancel(job JobID, changes chan<- DryadJobStatusChange,
 // newDryadJob creates an instance of dryadJob and starts a goroutine
 // executing phases of given job implemented by provider of DryadJobRunner interface.
 func newDryadJob(job JobID, rusalka Dryad, changes chan<- DryadJobStatusChange) *dryadJob {
-       session := dryad.NewSessionProvider(rusalka)
+       session, _ := dryad.NewSessionProvider(rusalka) // FIXME: Make use of returned error.
        device := dryad.NewDeviceCommunicationProvider(session)
 
        ctx, cancel := context.WithCancel(context.Background())