63 changes: 18 additions & 45 deletions helper/sshutil/sshutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import (
"strings"
"sync"

"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
Expand Down Expand Up @@ -163,41 +162,6 @@ func (sw *SSHSessionWrapper) RunCommand(ctx context.Context, cmd string) error {
return sw.session.Run(cmd)
}

// ReadPrivateKey returns an authentication method relying on private/public key pairs
// The argument is :
// - either a path to the private key file,
// - or the content or this private key file
func ReadPrivateKey(pk string) (ssh.AuthMethod, error) {
raw, err := ToPrivateKeyContent(pk)
if err != nil {
return nil, err
}
signer, err := ssh.ParsePrivateKey(raw)
if err != nil {
return nil, errors.Wrapf(err, "Failed to parse key file %q", pk)
}
return ssh.PublicKeys(signer), nil
}

// ToPrivateKeyContent allows to convert private key content or file to byte array
func ToPrivateKeyContent(pk string) ([]byte, error) {
var p []byte
// check if pk is a path
keyPath, err := homedir.Expand(pk)
if err != nil {
return nil, errors.Wrap(err, "failed to expand key path")
}
if _, err := os.Stat(keyPath); err == nil {
p, err = ioutil.ReadFile(keyPath)
if err != nil {
p = []byte(pk)
}
} else {
p = []byte(pk)
}
return p, nil
}

// CopyFile allows to copy a reader over SSH with defined remote path and specific permissions
// CopyFile allows to copy a reader over SSH with defined remote path and specific permissions
func (client *SSHClient) CopyFile(source io.Reader, remotePath string, permissions string) error {
Expand Down Expand Up @@ -324,15 +288,8 @@ func NewSSHAgent(ctx context.Context) (*SSHAgent, error) {
}, nil
}

// AddKey allows to add a key into ssh-agent keys list
func (sa *SSHAgent) AddKey(privateKey string, lifeTime uint32) error {
log.Debugf("Add key for SSH-AGENT")
keyContent, err := ToPrivateKeyContent(privateKey)
if err != nil {
return errors.Wrapf(err, "failed to retrieve private key content")
}

rawKey, err := ssh.ParseRawPrivateKey(keyContent)
func (sa *SSHAgent) addKey(privateKey []byte, lifeTime uint32) error {
rawKey, err := ssh.ParseRawPrivateKey(privateKey)
if err != nil {
return errors.Wrapf(err, "failed to parse raw private key")
}
Expand All @@ -344,6 +301,22 @@ func (sa *SSHAgent) AddKey(privateKey string, lifeTime uint32) error {
return sa.agent.Add(*addedKey)
}

// AddKey allows to add a key into ssh-agent keys list
func (sa *SSHAgent) AddKey(privateKey string, lifeTime uint32) error {
log.Debugf("Add key for SSH-AGENT")
keyContent, err := ToPrivateKeyContent(privateKey)
if err != nil {
return errors.Wrapf(err, "failed to retrieve private key content")
}
return sa.addKey(keyContent, lifeTime)
}

// AddPrivateKey allows to add a key into ssh-agent keys list
func (sa *SSHAgent) AddPrivateKey(privateKey *PrivateKey, lifeTime uint32) error {
log.Debugf("Add key for SSH-AGENT")
return sa.addKey(privateKey.Content, lifeTime)
}

// RemoveKey allows to remove a key into ssh-agent keys list
func (sa *SSHAgent) RemoveKey(privateKey string) error {
keyContent, err := ToPrivateKeyContent(privateKey)
Expand Down
2 changes: 1 addition & 1 deletion helper/sshutil/sshutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,6 @@ func TestSSHAgent(t *testing.T) {
// BER SSH key is not handled by crypto/ssh
// https://github.com/golang/go/issues/14145
func TestReadPrivateKey(t *testing.T) {
_, err := ReadPrivateKey("./testdata/test.pem")
_, err := ReadPrivateKey("./testdata/ber_test.pem")
require.NotNil(t, err)
}
92 changes: 92 additions & 0 deletions helper/sshutil/testdata/ComputeWithCredentials.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
tosca_definitions_version: alien_dsl_2_0_0

metadata:
template_name: PersistentDiskTest
template_version: 1.0
template_author: tester

description: ""

imports:
- <normative-types.yml>
- <yorc-google-types.yml>
- <yorc-types.yml>

topology_template:
node_templates:
Compute:
metadata:
type: yorc.nodes.google.Compute
properties:
image_project: "centos-cloud"
image_family: "centos-7"
machine_type: "n1-standard-1"
zone: "europe-west1-b"
capabilities:
endpoint:
properties:
secure: true
protocol: tcp
network_name: PRIVATE
initiator: source
credentials:
user: centos
keys:
0: "./testdata/validkey.pem"
scalable:
properties:
min_instances: 1
max_instances: 1
default_instances: 1
ComputeMultiKeys:
metadata:
type: yorc.nodes.google.Compute
properties:
image_project: "centos-cloud"
image_family: "centos-7"
machine_type: "n1-standard-1"
zone: "europe-west1-b"
capabilities:
endpoint:
properties:
secure: true
protocol: tcp
network_name: PRIVATE
initiator: source
credentials:
user: centos
keys:
0: "./testdata/validkey.pem"
anotherValidKey: |
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAuEl5Wjgdvlqbz0x2vcllSQrDiRd+bWdA2MgpOl726ovxw9uE
QJSlXYBJbSCQg+q++OEtXmvfahN5Y9aemuPey/o/S9RWyQ/X+uVeXdNV4Xkgar6b
uYr1n1Ju7ltmdVJME7fr+Ti+2d+EMBs7V+jGXyZzBTdr6wCJuBHHXV/ZKDzw1cHd
bRF8obBmMcxyzNbXnhSUvBgXT+GQ0/CgkNdrTwGOgtckqNYTuw1Rd6wAsF5xgN23
uss5WJOg3/eMW2JMjyxNqaJhBUtA2CKcdnLjwyDxWdmC1NMHKL1umPOjuCyNczpl
axMKW//UZT3WyfVt/gcHGGNIuI0izwFJ6QjlrQIDAQABAoIBAAet8COlUP/8sJ97
1TrlaJYZn7pXw0n10or2FFm9WVa+zC1YOXOjfhyeWvD0OXF1181xPL3BiwbVlupl
KCjWNBOV8wtK5u7r/RkUc9E/HEYQERzBoqWht8iS29KM9oEPE+KCeI/jIHjdypli
mR95sMKITKS8AYBCfnqwKvmmI9t8VIXsrZWsg1dUD9TCa8QxoA66raSpXegDgjox
T8IjZW90BwD6oG/5+HfbuwtjKR1Lca5tMzqxDMvqBf3KdCuee1x2Uuzla9/MsK/4
Nuqv88gpoI7bDJOJnF/KrJqEH1ihF5zNVOs5c7XKmnAdry05tA7CjbiILOeFq3yn
elkdR5UCgYEA3RC0bAR/TjSKGBEzvzfJFNp2ipdlZ1svobHl5sqDJzQp7P9aIogU
qUhw2vr/nHg4dfmLnMHJYh6GCIjN1H7NZzaBiQSUcT+s2GRxYJqRV4geFHvKNzt3
a50Hi5rSsbKm0LvlUA3vGkMABICyzkETPDl2WSFtKWUYrTMZSKixCtsCgYEA1Wjj
fn+3HOmAv3lX4PzhHiBBfBj60BKPCnBbWG6TTF4ya7UEU+e5aAbLD10QdQyx77rL
V3G3Xda1BWA2wGKRDfC9ksFUuxH2egNPGadOVZH2U/a/87YGOFUmbf03jJ6mbeRV
BBBVcB8oGSD+NemiDPqYUi/G1lT+oRLFIkkYhBcCgYEApjKj4j2zVCFt3NA5/j27
gGEKFAHka8MDWWY8uLlxxuyRxKrpoeJ63hYnOorP11wO3qshClYqyAi4rfvj+yjl
1f4FfvShgU7k7L7++ijaslsUekPi8IlVq+MfxBY+5vewMGfC69+97hmHDtuPEj+c
bX+p+TKHNkLaPYSYMqcYi1cCgYEAxf6JSfyt48oT5BFtYdTb+zpL5xm54T/GrBWv
+eylBm5Cc0E/YaUUlBnxXTCnqyD7GQKB04AycoJX8kPgqD8KexeGmlh6BxFUTsEx
KwjZGXTRR/cfAbo4LR17CQKr/e/XUw9LfPi2e868QgwlLdmzujzpAx9GZ+X1U3V5
piSQ9UMCgYBdegnYh2fqU/oGH+d9WahuO1LW9vz8sFEIhRgJyLfA/ypAg6WCgJF2
GtepEYBXL+QZnhudVxi0YPTmNN3+gtHdr+B4dKZ8z7m9NO2nk5AKdf0sYGWHEzhy
PAgZzG5OTZiu+YohUPnC66eFiyS6anLBj0DGNa9VA8j352ecgeNO4A==
-----END RSA PRIVATE KEY-----
scalable:
properties:
min_instances: 1
max_instances: 1
default_instances: 1
File renamed without changes.
27 changes: 27 additions & 0 deletions helper/sshutil/testdata/validkey.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAuEl5Wjgdvlqbz0x2vcllSQrDiRd+bWdA2MgpOl726ovxw9uE
QJSlXYBJbSCQg+q++OEtXmvfahN5Y9aemuPey/o/S9RWyQ/X+uVeXdNV4Xkgar6b
uYr1n1Ju7ltmdVJME7fr+Ti+2d+EMBs7V+jGXyZzBTdr6wCJuBHHXV/ZKDzw1cHd
bRF8obBmMcxyzNbXnhSUvBgXT+GQ0/CgkNdrTwGOgtckqNYTuw1Rd6wAsF5xgN23
uss5WJOg3/eMW2JMjyxNqaJhBUtA2CKcdnLjwyDxWdmC1NMHKL1umPOjuCyNczpl
axMKW//UZT3WyfVt/gcHGGNIuI0izwFJ6QjlrQIDAQABAoIBAAet8COlUP/8sJ97
1TrlaJYZn7pXw0n10or2FFm9WVa+zC1YOXOjfhyeWvD0OXF1181xPL3BiwbVlupl
KCjWNBOV8wtK5u7r/RkUc9E/HEYQERzBoqWht8iS29KM9oEPE+KCeI/jIHjdypli
mR95sMKITKS8AYBCfnqwKvmmI9t8VIXsrZWsg1dUD9TCa8QxoA66raSpXegDgjox
T8IjZW90BwD6oG/5+HfbuwtjKR1Lca5tMzqxDMvqBf3KdCuee1x2Uuzla9/MsK/4
Nuqv88gpoI7bDJOJnF/KrJqEH1ihF5zNVOs5c7XKmnAdry05tA7CjbiILOeFq3yn
elkdR5UCgYEA3RC0bAR/TjSKGBEzvzfJFNp2ipdlZ1svobHl5sqDJzQp7P9aIogU
qUhw2vr/nHg4dfmLnMHJYh6GCIjN1H7NZzaBiQSUcT+s2GRxYJqRV4geFHvKNzt3
a50Hi5rSsbKm0LvlUA3vGkMABICyzkETPDl2WSFtKWUYrTMZSKixCtsCgYEA1Wjj
fn+3HOmAv3lX4PzhHiBBfBj60BKPCnBbWG6TTF4ya7UEU+e5aAbLD10QdQyx77rL
V3G3Xda1BWA2wGKRDfC9ksFUuxH2egNPGadOVZH2U/a/87YGOFUmbf03jJ6mbeRV
BBBVcB8oGSD+NemiDPqYUi/G1lT+oRLFIkkYhBcCgYEApjKj4j2zVCFt3NA5/j27
gGEKFAHka8MDWWY8uLlxxuyRxKrpoeJ63hYnOorP11wO3qshClYqyAi4rfvj+yjl
1f4FfvShgU7k7L7++ijaslsUekPi8IlVq+MfxBY+5vewMGfC69+97hmHDtuPEj+c
bX+p+TKHNkLaPYSYMqcYi1cCgYEAxf6JSfyt48oT5BFtYdTb+zpL5xm54T/GrBWv
+eylBm5Cc0E/YaUUlBnxXTCnqyD7GQKB04AycoJX8kPgqD8KexeGmlh6BxFUTsEx
KwjZGXTRR/cfAbo4LR17CQKr/e/XUw9LfPi2e868QgwlLdmzujzpAx9GZ+X1U3V5
piSQ9UMCgYBdegnYh2fqU/oGH+d9WahuO1LW9vz8sFEIhRgJyLfA/ypAg6WCgJF2
GtepEYBXL+QZnhudVxi0YPTmNN3+gtHdr+B4dKZ8z7m9NO2nk5AKdf0sYGWHEzhy
PAgZzG5OTZiu+YohUPnC66eFiyS6anLBj0DGNa9VA8j352ecgeNO4A==
-----END RSA PRIVATE KEY-----
100 changes: 54 additions & 46 deletions prov/ansible/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,17 @@ import (
"syscall"

"github.com/hashicorp/consul/api"
"github.com/mitchellh/mapstructure"
"github.com/moby/moby/client"
"github.com/pkg/errors"
uuid "github.com/satori/go.uuid"
"golang.org/x/crypto/ssh"
"gopkg.in/yaml.v2"

"github.com/ystia/yorc/v3/config"
"github.com/ystia/yorc/v3/deployments"
"github.com/ystia/yorc/v3/events"
"github.com/ystia/yorc/v3/helper/consulutil"
"github.com/ystia/yorc/v3/helper/executil"
"github.com/ystia/yorc/v3/helper/pathutil"
"github.com/ystia/yorc/v3/helper/provutil"
"github.com/ystia/yorc/v3/helper/sshutil"
"github.com/ystia/yorc/v3/helper/stringutil"
Expand All @@ -52,6 +51,7 @@ import (
"github.com/ystia/yorc/v3/prov/operations"
"github.com/ystia/yorc/v3/tasks"
"github.com/ystia/yorc/v3/tosca"
"github.com/ystia/yorc/v3/tosca/datatypes"
)

const taskContextOutput = "task_context"
Expand Down Expand Up @@ -120,18 +120,18 @@ func (oni operationNotImplemented) Error() string {
}

type hostConnection struct {
host string
port int
user string
instanceID string
privateKey string
password string
host string
port int
user string
instanceID string
privateKeys map[string]*sshutil.PrivateKey
password string
}

type sshCredentials struct {
user string
privateKey string
password string
user string
privateKeys map[string]*sshutil.PrivateKey
password string
}

type execution interface {
Expand Down Expand Up @@ -364,31 +364,32 @@ func (e *executionCommon) setHostConnection(kv *api.KV, host, instanceID, capTyp
return err
}
if hasEndpoint {
user, err := deployments.GetInstanceCapabilityAttributeValue(e.kv, e.deploymentID, host, instanceID, "endpoint", "credentials", "user")
credentialValue, err := deployments.GetInstanceCapabilityAttributeValue(e.kv, e.deploymentID, host, instanceID, "endpoint", "credentials")
if err != nil {
return err
}
if user != nil {
conn.user = config.DefaultConfigTemplateResolver.ResolveValueWithTemplates("host.user", user.RawString()).(string)
credentials := new(datatypes.Credential)
if credentialValue != nil && credentialValue.RawString() != "" {
err = mapstructure.Decode(credentialValue.Value, credentials)
if err != nil {
return errors.Wrapf(err, "failed to decode credentials for node %q", host)
}
}
if credentials.User != "" {
conn.user = config.DefaultConfigTemplateResolver.ResolveValueWithTemplates("host.user", credentials.User).(string)
} else {
mess := fmt.Sprintf("[Warning] No user set for connection:%+v", conn)
log.Printf(mess)
events.WithContextOptionalFields(e.ctx).NewLogEntry(events.LogLevelWARN, e.deploymentID).RegisterAsString(mess)
}
password, err := deployments.GetInstanceCapabilityAttributeValue(e.kv, e.deploymentID, host, instanceID, "endpoint", "credentials", "token")
if err != nil {
return err
}
if password != nil && password.RawString() != "" {
conn.password = config.DefaultConfigTemplateResolver.ResolveValueWithTemplates("host.password", password.RawString()).(string)
if credentials.Token != "" {
conn.password = config.DefaultConfigTemplateResolver.ResolveValueWithTemplates("host.password", credentials.Token).(string)
}
privateKey, err := deployments.GetInstanceCapabilityAttributeValue(e.kv, e.deploymentID, host, instanceID, "endpoint", "credentials", "keys", "0")

conn.privateKeys, err = sshutil.GetKeysFromCredentialsDataType(credentials)
if err != nil {
return err
}
if privateKey != nil && privateKey.RawString() != "" {
conn.privateKey = config.DefaultConfigTemplateResolver.ResolveValueWithTemplates("host.privateKey", privateKey.RawString()).(string)
}

port, err := deployments.GetInstanceCapabilityAttributeValue(e.kv, e.deploymentID, host, instanceID, "endpoint", "port")
if err != nil {
Expand Down Expand Up @@ -800,21 +801,30 @@ func (e *executionCommon) generateHostConnectionForOrchestratorOperation(ctx con
return nil
}

func (e *executionCommon) getSSHCredentials(ctx context.Context, host *hostConnection, warnOfMissingValues bool) sshCredentials {
func (e *executionCommon) getSSHCredentials(ctx context.Context, host *hostConnection) (sshCredentials, error) {

creds := sshCredentials{}
sshUser := host.user
if sshUser == "" {
// Use root as default user
sshUser = "root"
events.WithContextOptionalFields(ctx).NewLogEntry(events.LogLevelWARN, e.deploymentID).RegisterAsString("Ansible provisioning: Missing ssh user information, trying to use root user.")
}
creds.user = sshUser
sshPassword := host.password
sshPrivateKey := host.privateKey
if sshPrivateKey == "" && sshPassword == "" {
sshPrivateKey = "~/.ssh/yorc.pem"
host.privateKey = sshPrivateKey
sshPrivateKeys := host.privateKeys
if len(sshPrivateKeys) == 0 && sshPassword == "" {
defaultKey, err := sshutil.GetDefaultKey()
if err != nil {
return creds, err
}
sshPrivateKeys = map[string]*sshutil.PrivateKey{"0": defaultKey}
host.privateKeys = sshPrivateKeys
events.WithContextOptionalFields(ctx).NewLogEntry(events.LogLevelWARN, e.deploymentID).RegisterAsString("Ansible provisioning: Missing ssh password or private key information, trying to use default private key ~/.ssh/yorc.pem.")
}
return sshCredentials{user: sshUser, password: sshPassword, privateKey: sshPrivateKey}
creds.privateKeys = sshPrivateKeys
creds.password = sshPassword
return creds, nil
}

func (e *executionCommon) generateHostConnection(ctx context.Context, buffer *bytes.Buffer, host *hostConnection) error {
Expand All @@ -825,22 +835,20 @@ func (e *executionCommon) generateHostConnection(ctx context.Context, buffer *by
return err
}
} else {
sshCredentials := e.getSSHCredentials(ctx, host, true)
sshCredentials, err := e.getSSHCredentials(ctx, host)
if err != nil {
return err
}
buffer.WriteString(fmt.Sprintf(" ansible_ssh_user=%s", sshCredentials.user))
// Set with priority private key against password
if e.cfg.DisableSSHAgent && sshCredentials.privateKey != "" {
// check privateKey's a valid path
if is, err := pathutil.IsValidPath(sshCredentials.privateKey); err != nil || !is {
// Truncate it if it's a private key
ufo := sshCredentials.privateKey
if _, err = ssh.ParsePrivateKey([]byte(sshCredentials.privateKey)); err == nil {
ufo = stringutil.Truncate(sshCredentials.privateKey, 20)
}
return errors.Errorf("%q is not a valid path", ufo)
if e.cfg.DisableSSHAgent && len(sshCredentials.privateKeys) > 0 {
key := sshutil.SelectPrivateKeyOnName(sshCredentials.privateKeys, true)
if key == nil {
return errors.Errorf("%d private keys provided (may include the default key %q) but none are stored on disk. As ssh-agent is disabled by configuration we can't use direct key content.", len(sshCredentials.privateKeys), sshutil.DefaultSSHPrivateKeyFilePath)
}
buffer.WriteString(fmt.Sprintf(" ansible_ssh_private_key_file=%s", sshCredentials.privateKey))
buffer.WriteString(fmt.Sprintf(" ansible_ssh_private_key_file=%s", key.Path))
} else if sshCredentials.password != "" {
// TODO use vault
// TODO use ansible vault
buffer.WriteString(fmt.Sprintf(" ansible_ssh_pass=%s", sshCredentials.password))
}
// Specify SSH port when different than default 22
Expand Down Expand Up @@ -1001,7 +1009,7 @@ func (e *executionCommon) executeWithCurrentInstance(ctx context.Context, retry
}
if perInstanceInputsBuffer.Len() > 0 {
if err = ioutil.WriteFile(filepath.Join(ansibleHostVarsPath, host.host+".yml"), perInstanceInputsBuffer.Bytes(), 0664); err != nil {
return errors.Wrapf(err, "Failed to write vars for host %q file: %v", host, err)
return errors.Wrapf(err, "Failed to write vars for host %q file: %v", host.host, err)
}
}
}
Expand Down Expand Up @@ -1235,7 +1243,7 @@ func (e *executionCommon) executePlaybook(ctx context.Context, retry bool,
func (e *executionCommon) configureSSHAgent(ctx context.Context) (*sshutil.SSHAgent, error) {
var addSSHAgent bool
for _, host := range e.hosts {
if host.privateKey != "" {
if len(host.privateKeys) > 0 {
addSSHAgent = true
break
}
Expand All @@ -1249,8 +1257,8 @@ func (e *executionCommon) configureSSHAgent(ctx context.Context) (*sshutil.SSHAg
return nil, err
}
for _, host := range e.hosts {
if host.privateKey != "" {
if err = agent.AddKey(host.privateKey, 3600); err != nil {
for _, key := range host.privateKeys {
if err = agent.AddPrivateKey(key, 3600); err != nil {
return nil, err
}
}
Expand Down
23 changes: 17 additions & 6 deletions prov/hostspool/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/dustin/go-humanize"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/go-multierror"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"

"github.com/ystia/yorc/v3/config"
Expand All @@ -31,6 +32,7 @@ import (
"github.com/ystia/yorc/v3/helper/labelsutil"
"github.com/ystia/yorc/v3/tasks"
"github.com/ystia/yorc/v3/tosca"
"github.com/ystia/yorc/v3/tosca/datatypes"
)

type defaultExecutor struct {
Expand Down Expand Up @@ -143,14 +145,23 @@ func (e *defaultExecutor) hostsPoolCreate(originalCtx context.Context, cc *api.C
if err != nil {
return err
}
credentials := map[string]interface{}{"user": host.Connection.User}
if host.Connection.Password != "" {
credentials["token"] = host.Connection.Password
credentials := datatypes.Credential{
User: host.Connection.User,
Token: host.Connection.Password,
Keys: map[string]string{
// 0 is the default key name we are trying to remove this by allowing multiple keys
"0": host.Connection.PrivateKey,
},
}
if host.Connection.PrivateKey != "" {
credentials["keys"] = []string{host.Connection.PrivateKey}

var credentialsMap map[string]interface{}
err = mapstructure.Decode(credentials, &credentialsMap)
if err != nil {
return err
}
err = deployments.SetInstanceCapabilityAttributeComplex(deploymentID, nodeName, instance, "endpoint", "credentials", credentials)

err = deployments.SetInstanceCapabilityAttributeComplex(deploymentID, nodeName, instance, "endpoint", "credentials", credentialsMap)

if err != nil {
return err
}
Expand Down
12 changes: 4 additions & 8 deletions prov/hostspool/hostspool_mgr_connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,27 +163,31 @@ func (cm *consulManager) GetHostConnection(hostname string) (Connection, error)
}
if kvp != nil {
conn.Host = string(kvp.Value)
conn.Host = config.DefaultConfigTemplateResolver.ResolveValueWithTemplates("Connection.Host", conn.Host).(string)
}
kvp, _, err = kv.Get(path.Join(connKVPrefix, "user"), nil)
if err != nil {
return conn, errors.Wrap(err, consulutil.ConsulGenericErrMsg)
}
if kvp != nil {
conn.User = string(kvp.Value)
conn.User = config.DefaultConfigTemplateResolver.ResolveValueWithTemplates("Connection.User", conn.User).(string)
}
kvp, _, err = kv.Get(path.Join(connKVPrefix, "password"), nil)
if err != nil {
return conn, errors.Wrap(err, consulutil.ConsulGenericErrMsg)
}
if kvp != nil {
conn.Password = string(kvp.Value)
conn.Password = config.DefaultConfigTemplateResolver.ResolveValueWithTemplates("Connection.Password", conn.Password).(string)
}
kvp, _, err = kv.Get(path.Join(connKVPrefix, "private_key"), nil)
if err != nil {
return conn, errors.Wrap(err, consulutil.ConsulGenericErrMsg)
}
if kvp != nil {
conn.PrivateKey = string(kvp.Value)
conn.PrivateKey = config.DefaultConfigTemplateResolver.ResolveValueWithTemplates("Connection.PrivateKey", conn.PrivateKey).(string)
}
kvp, _, err = kv.Get(path.Join(connKVPrefix, "port"), nil)
if err != nil {
Expand All @@ -199,21 +203,13 @@ func (cm *consulManager) GetHostConnection(hostname string) (Connection, error)
return conn, nil
}

func resolveTemplatesInConnection(conn *Connection) {
conn.User = config.DefaultConfigTemplateResolver.ResolveValueWithTemplates("Connection.User", conn.User).(string)
conn.Password = config.DefaultConfigTemplateResolver.ResolveValueWithTemplates("Connection.Password", conn.Password).(string)
conn.PrivateKey = config.DefaultConfigTemplateResolver.ResolveValueWithTemplates("Connection.PrivateKey", conn.PrivateKey).(string)
conn.Host = config.DefaultConfigTemplateResolver.ResolveValueWithTemplates("Connection.Host", conn.Host).(string)
}

// Check if we can log into an host given a connection
func (cm *consulManager) checkConnection(hostname string) error {

conn, err := cm.GetHostConnection(hostname)
if err != nil {
return errors.Wrapf(err, "failed to connect to host %q", hostname)
}
resolveTemplatesInConnection(&conn)
conf, err := getSSHConfig(conn)
if err != nil {
return errors.Wrapf(err, "failed to connect to host %q", hostname)
Expand Down
16 changes: 3 additions & 13 deletions prov/slurm/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,12 @@ func newExecution(kv *api.KV, cfg config.Configuration, taskID, deploymentID, no
}
// Get user credentials from credentials node property
// Its not a capability, so capabilityName set to empty string
creds, err := getUserCredentials(kv, deploymentID, nodeName, "", "credentials")
creds, err := getUserCredentials(kv, cfg, deploymentID, nodeName, "")
if err != nil {
return nil, err
}
// Create sshClient using user credentials from credentials property if the are provided, or from yorc config otherwise
execCommon.client, err = getSSHClient(creds.UserName, creds.PrivateKey, creds.Password, cfg)
execCommon.client, err = getSSHClient(creds, cfg)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -231,7 +231,7 @@ func (e *executionCommon) getJobInfoFromTaskContext() (*jobInfo, error) {
if err != nil {
return nil, errors.Wrap(err, "Failed to unmarshal stored Slurm job information")
}
log.Debugf("Unmarshal Job info for task %s. Got user name info : %s", e.taskID, jobInfo.Credentials.UserName)
log.Debugf("Unmarshal Job info for task %s, Job ID %q.", e.taskID, jobInfo.ID)
return jobInfo, nil
}

Expand All @@ -243,9 +243,6 @@ func (e *executionCommon) buildJobMonitoringAction() *prov.Action {
data["stepName"] = e.stepName
data["nodeName"] = e.NodeName
data["workingDir"] = e.jobInfo.WorkingDir
data["userName"] = e.jobInfo.Credentials.UserName
data["password"] = e.jobInfo.Credentials.Password
data["privateKey"] = e.jobInfo.Credentials.PrivateKey
data["artifacts"] = strings.Join(e.jobInfo.Artifacts, ",")

return &prov.Action{ActionType: "job-monitoring", Data: data}
Expand Down Expand Up @@ -346,13 +343,6 @@ func (e *executionCommon) buildJobInfo(ctx context.Context) error {
e.jobInfo.ID = id.String()
}

// Get user credentials from credentials node property, if values are provided
// Its not a capability property so capability name is empty
e.jobInfo.Credentials, err = getUserCredentials(e.kv, e.deploymentID, e.NodeName, "", "credentials")
if err != nil {
return err
}

// Job account
if acc, err := deployments.GetNodePropertyValue(e.kv, e.deploymentID, e.NodeName, "slurm_options", "account"); err != nil {
return err
Expand Down
4 changes: 2 additions & 2 deletions prov/slurm/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func (e *defaultExecutor) createNodeAllocation(ctx context.Context, cfg config.C

// Return an sshClient configured using the user credentials provided in the yorc.nodes.slurm.Compute node definition,
// or if not provided, the user credentials specified in the Yorc configuration
sshClient, err := getSSHClient(nodeAlloc.credentials.UserName, nodeAlloc.credentials.PrivateKey, nodeAlloc.credentials.Password, cfg)
sshClient, err := getSSHClient(nodeAlloc.credentials, cfg)
if err != nil {
events.WithContextOptionalFields(ctx).NewLogEntry(events.LogLevelERROR, deploymentID).RegisterAsString(err.Error())
return err
Expand Down Expand Up @@ -342,7 +342,7 @@ func (e *defaultExecutor) destroyNodeAllocation(ctx context.Context, cfg config.

// Return an sshClient configured using the user credentials provided in the yorc.nodes.slurm.Compute node definition,
// or if not provided, the user credentials specified in the Yorc configuration
sshClient, err := getSSHClient(nodeAlloc.credentials.UserName, nodeAlloc.credentials.PrivateKey, nodeAlloc.credentials.Password, cfg)
sshClient, err := getSSHClient(nodeAlloc.credentials, cfg)
if err != nil {
events.WithContextOptionalFields(ctx).NewLogEntry(events.LogLevelERROR, deploymentID).RegisterAsString(err.Error())
return err
Expand Down
137 changes: 52 additions & 85 deletions prov/slurm/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@ package slurm
import (
"bufio"
"fmt"
"github.com/dustin/go-humanize"
"io"
"regexp"
"strconv"
"strings"

"github.com/dustin/go-humanize"
"github.com/hashicorp/consul/api"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"golang.org/x/crypto/ssh"

"github.com/ystia/yorc/v3/config"
"github.com/ystia/yorc/v3/deployments"
"github.com/ystia/yorc/v3/helper/sshutil"
"github.com/ystia/yorc/v3/log"
"github.com/ystia/yorc/v3/tosca/datatypes"
)

const reSbatch = `^Submitted batch job (\d+)`
Expand All @@ -39,47 +41,40 @@ const invalidJob = "Invalid job id specified"

// getSSHClient returns a SSH client with slurm credentials from node or job configuration provided by the deployment,
// or by the yorc slurm configuration
func getSSHClient(userName string, privateKey string, password string, cfg config.Configuration) (*sshutil.SSHClient, error) {
func getSSHClient(credentials *datatypes.Credential, cfg config.Configuration) (*sshutil.SSHClient, error) {
// Check manadatory slurm configuration
if err := checkInfraConfig(cfg); err != nil {
log.Printf("Unable to provide SSH client due to:%+v", err)
return nil, err
}

// Get user credentials provided by the deployment, if any
if userName != "" {
if password == "" && privateKey == "" {
return nil, errors.New("Slurm missing authentication details in deployment properties, password or private_key should be set")
}
} else {
// Get user credentials from the yorc configuration
if err := checkInfraUserConfig(cfg); err != nil {
log.Printf("Unable to provide SSH client due to:%+v", err)
return nil, err
}
userName = strings.Trim(cfg.Infrastructures[infrastructureName].GetString("user_name"), "")
privateKey = strings.Trim(cfg.Infrastructures[infrastructureName].GetString("private_key"), "")
password = strings.Trim(cfg.Infrastructures[infrastructureName].GetString("password"), "")
if credentials.Token == "" && len(credentials.Keys) == 0 {
return nil, errors.New("Slurm missing authentication details in deployment properties, password or private_key should be set")
}
keys, err := sshutil.GetKeysFromCredentialsDataType(credentials)
if err != nil {
return nil, err
}

// Get SSH client
SSHConfig := &ssh.ClientConfig{
User: userName,
User: credentials.User,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}

// Set an authentication method. At least one authentication method
// has to be set, private/public key or password.
if privateKey != "" {
keyAuth, err := sshutil.ReadPrivateKey(privateKey)
if err != nil {
return nil, err
if len(keys) > 0 {
for keyName, pk := range keys {
keyAuth, err := sshutil.ReadSSHPrivateKey(pk)
if err != nil {
return nil, errors.Wrapf(err, "failed to read key %q", keyName)
}
SSHConfig.Auth = append(SSHConfig.Auth, keyAuth)
}
SSHConfig.Auth = append(SSHConfig.Auth, keyAuth)
}

if password != "" {
SSHConfig.Auth = append(SSHConfig.Auth, ssh.Password(password))
if credentials.Token != "" {
SSHConfig.Auth = append(SSHConfig.Auth, ssh.Password(credentials.Token))
}

port, err := strconv.Atoi(cfg.Infrastructures[infrastructureName].GetString("port"))
Expand All @@ -98,80 +93,52 @@ func getSSHClient(userName string, privateKey string, password string, cfg confi

// getUserCredentials returns user credentials from a node property, or a capability property.
// the property name is provided by propertyName parameter, and its type is supposed to be tosca.datatypes.Credential
func getUserCredentials(kv *api.KV, deploymentID string, nodeName string, capabilityName, propertyName string) (*UserCredentials, error) {
// Check if user credentials provided in node definition
userName, err := getPropertyValue(kv, deploymentID, nodeName, capabilityName, propertyName, "user")
if err != nil {
return nil, err
}
if userName != "" {
log.Debugf("Got user name %s from property %s", userName, propertyName)
func getUserCredentials(kv *api.KV, cfg config.Configuration, deploymentID, nodeName, capabilityName string) (*datatypes.Credential, error) {
var err error
var credentialsValue *deployments.TOSCAValue
if capabilityName != "" {
credentialsValue, err = deployments.GetCapabilityPropertyValue(kv, deploymentID, nodeName, capabilityName, "credentials")
} else {
credentialsValue, err = deployments.GetNodePropertyValue(kv, deploymentID, nodeName, "credentials")
}

// Check for token-type
tokenType, err := getPropertyValue(kv, deploymentID, nodeName, capabilityName, propertyName, "token_type")
if err != nil {
return nil, err
}

log.Debugf("Got token_type %s from property %s", tokenType, propertyName)

var password, privateKey string
switch tokenType {
case "password":
password, err = getPropertyValue(kv, deploymentID, nodeName, capabilityName, propertyName, "token")
creds := new(datatypes.Credential)
if credentialsValue != nil && credentialsValue.RawString() != "" {
err = mapstructure.Decode(credentialsValue.Value, creds)
if err != nil {
return nil, err
return nil, errors.Wrapf(err, "failed to decode credentials for node %q", nodeName)
}
if password != "" {
log.Debugf("Got password from property")
}

// Get user credentials provided by the deployment, if any
if creds.User != "" {
if creds.Token == "" && len(creds.Keys) == 0 {
return nil, errors.New("Slurm missing authentication details in deployment properties, password or private_key should be set")
}
case "private_key":
privateKey, err = getPropertyValue(kv, deploymentID, nodeName, capabilityName, propertyName, "keys", "0")
if err != nil {
} else {
// Get user credentials from the yorc configuration
if err := checkInfraUserConfig(cfg); err != nil {
log.Printf("Unable to provide SSH client due to:%+v", err)
return nil, err
}
creds.User = strings.Trim(cfg.Infrastructures[infrastructureName].GetString("user_name"), "")
creds.User = config.DefaultConfigTemplateResolver.ResolveValueWithTemplates("slurm.user_name", creds.User).(string)
privateKey := strings.Trim(cfg.Infrastructures[infrastructureName].GetString("private_key"), "")
if privateKey != "" {
log.Debugf("Got private key from property")
}
default:
// password or private_key expected as token_type
if capabilityName != "" {
return nil, errors.Errorf("Unsupported token_type %s in capability %s property %s. One of password or private_key expected", tokenType, capabilityName, propertyName)
} else {
return nil, errors.Errorf("Unsupported token_type %s in property %s. One of password or private_key expected", tokenType, propertyName)
privateKey = config.DefaultConfigTemplateResolver.ResolveValueWithTemplates("slurm.private_key", privateKey).(string)
if creds.Keys == nil {
creds.Keys = make(map[string]string)
}
creds.Keys["default"] = privateKey
}

creds.Token = strings.Trim(cfg.Infrastructures[infrastructureName].GetString("password"), "")
creds.Token = config.DefaultConfigTemplateResolver.ResolveValueWithTemplates("slurm.password", creds.Token).(string)
}

return &UserCredentials{UserName: userName, PrivateKey: privateKey, Password: password}, nil

}
return creds, nil

func getPropertyValue(kv *api.KV, deploymentID string, nodeName string, capabilityName string, propertyName string, nestedKeys ...string) (string, error) {
var value string
var propValue *deployments.TOSCAValue
var err error
if capabilityName != "" {
if len(nestedKeys) == 1 {
propValue, err = deployments.GetCapabilityPropertyValue(kv, deploymentID, nodeName, capabilityName, propertyName, nestedKeys[0])
} else {
propValue, err = deployments.GetCapabilityPropertyValue(kv, deploymentID, nodeName, capabilityName, propertyName, nestedKeys[0], nestedKeys[1])
}
} else {
if len(nestedKeys) == 1 {
propValue, err = deployments.GetNodePropertyValue(kv, deploymentID, nodeName, propertyName, nestedKeys[0])
} else {
propValue, err = deployments.GetNodePropertyValue(kv, deploymentID, nodeName, propertyName, nestedKeys[0], nestedKeys[1])
}
}
if err != nil {
return "", err
}
if propValue != nil {
value = propValue.RawString()
}
return value, nil
}

// checkInfraConfig checks slurm infrastructure mandatory configuration parameters :
Expand Down
25 changes: 12 additions & 13 deletions prov/slurm/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ import (
"crypto/x509"
"encoding/pem"
"errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ystia/yorc/v3/config"
"io/ioutil"
"os"
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ystia/yorc/v3/config"
"github.com/ystia/yorc/v3/tosca/datatypes"
)

// MockSSHSession allows to mock an SSH session
Expand Down Expand Up @@ -257,29 +260,27 @@ func TestPrivateKey(t *testing.T) {

err = checkInfraUserConfig(cfg)
assert.NoError(t, err, "Unexpected error parsing a configuration with private key")
_, err = getSSHClient("", "", "", cfg)
assert.NoError(t, err, "Unexpected error getting a ssh client using a configuration with private key")
_, err = getSSHClient("jdoe", privateKeyContent, "", cfg)
_, err = getSSHClient(&datatypes.Credential{User: "jdoe", Keys: map[string]string{"0": privateKeyContent}}, cfg)
assert.NoError(t, err, "Unexpected error getting a ssh client using provided properties with private key")

// Remove the private key.
// As there is no password defined either, check an error is returned
cfg.Infrastructures["slurm"].Set("private_key", "")
err = checkInfraUserConfig(cfg)
assert.Error(t, err, "Expected an error parsing a wrong configuration with no private key and no password defined")
_, err = getSSHClient("", "", "", cfg)
_, err = getSSHClient(&datatypes.Credential{}, cfg)
assert.Error(t, err, "Expected an error getting a ssh client using a configuration with no private key and no password defined")
_, err = getSSHClient("jdoe", "", "", cfg)
_, err = getSSHClient(&datatypes.Credential{User: "jdoe"}, cfg)
assert.Error(t, err, "Expected an error getting a ssh client using a provided user name property but no private key and no password provided")

// Setting a wrong private key path
// Check the attempt to use this key for the authentication method is failing
cfg.Infrastructures["slurm"].Set("private_key", "invalid_path_to_key.pem")
err = checkInfraUserConfig(cfg)
assert.NoError(t, err, "Unexpected error parsing a configuration with private key")
_, err = getSSHClient("", "", "", cfg)
_, err = getSSHClient(&datatypes.Credential{}, cfg)
assert.Error(t, err, "Expected an error getting a ssh client using a configuration with bad private key and no password defined")
_, err = getSSHClient("jdo", "invalid_path_to_key.pem", "", cfg)
_, err = getSSHClient(&datatypes.Credential{User: "jdoe", Keys: map[string]string{"0": "invalid_path_to_key.pem"}}, cfg)
assert.Error(t, err, "Expected an error getting a ssh client using provided credentials with bad private key and no password defined")

// Slurm Configuration with no private key but a password, the config should be valid
Expand All @@ -292,9 +293,7 @@ func TestPrivateKey(t *testing.T) {

err = checkInfraUserConfig(cfg)
assert.NoError(t, err, "Unexpected error parsing a configuration with password")
_, err = getSSHClient("", "", "", cfg)
assert.NoError(t, err, "Unexpected error getting a ssh client using a configuration with password")
_, err = getSSHClient("jdoe", "", "test", cfg)
_, err = getSSHClient(&datatypes.Credential{User: "jdoe", Token: "test"}, cfg)
assert.NoError(t, err, "Unexpected error getting a ssh client using provided credentials with password")
}

Expand Down
13 changes: 9 additions & 4 deletions prov/slurm/monitoring_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,13 @@ func (o *actionOperator) monitorJob(ctx context.Context, cfg config.Configuratio
actionData.artifacts = strings.Split(artifactsStr, ",")
}

nodeName := action.Data["nodeName"]
credentials, err := getUserCredentials(consulutil.GetKV(), cfg, deploymentID, nodeName, "")
if err != nil {
return true, err
}
// Get a sshClient to connect to slurm client node, and execute slurm commands such as squeue, or system commands such as cp, mv, mkdir, etc.
sshClient, err := getSSHClient(action.Data["userName"], action.Data["privateKey"], action.Data["password"], cfg)
sshClient, err := getSSHClient(credentials, cfg)
if err != nil {
return true, err
}
Expand All @@ -110,7 +115,7 @@ func (o *actionOperator) monitorJob(ctx context.Context, cfg config.Configuratio
if err != nil {
if isNoJobFoundError(err) {
// the job is not found in slurm database (should have been purged) : pass its status to "UNKNOWN"
deployments.SetInstanceStateStringWithContextualLogs(ctx, consulutil.GetKV(), deploymentID, action.Data["nodeName"], "0", "UNKNOWN")
deployments.SetInstanceStateStringWithContextualLogs(ctx, consulutil.GetKV(), deploymentID, nodeName, "0", "UNKNOWN")
}
return true, errors.Wrapf(err, "failed to get job info with jobID:%q", actionData.jobID)
}
Expand Down Expand Up @@ -141,12 +146,12 @@ func (o *actionOperator) monitorJob(ctx context.Context, cfg config.Configuratio
o.logFile(ctx, cfg, action, deploymentID, fmt.Sprintf("slurm-%s.out", actionData.jobID), "StdOut/Stderr", sshClient)
}

previousJobState, err := deployments.GetInstanceStateString(consulutil.GetKV(), deploymentID, action.Data["nodeName"], "0")
previousJobState, err := deployments.GetInstanceStateString(consulutil.GetKV(), deploymentID, nodeName, "0")
if err != nil {
return true, errors.Wrapf(err, "failed to get instance state for job %q", actionData.jobID)
}
if previousJobState != info["JobState"] {
deployments.SetInstanceStateStringWithContextualLogs(ctx, consulutil.GetKV(), deploymentID, action.Data["nodeName"], "0", info["JobState"])
deployments.SetInstanceStateStringWithContextualLogs(ctx, consulutil.GetKV(), deploymentID, nodeName, "0", info["JobState"])
}

// See if monitoring must be continued and set job state if terminated
Expand Down
2 changes: 1 addition & 1 deletion prov/slurm/slurm_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func generateNodeAllocation(ctx context.Context, kv *api.KV, cfg config.Configur
}

// Get user credentials from capability endpoint credentials property, if values are provided
node.credentials, err = getUserCredentials(kv, deploymentID, nodeName, "endpoint", "credentials")
node.credentials, err = getUserCredentials(kv, cfg, deploymentID, nodeName, "endpoint")
if err != nil {
return err
}
Expand Down
8 changes: 4 additions & 4 deletions prov/slurm/slurm_node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ func testSimpleSlurmNodeAllocation(t *testing.T, kv *api.KV, cfg config.Configur
require.Equal(t, "2G", infrastructure.nodes[0].memory)
require.Equal(t, "4", infrastructure.nodes[0].cpu)
require.Equal(t, "xyz", infrastructure.nodes[0].jobName)
require.Equal(t, "johndoe", infrastructure.nodes[0].credentials.UserName)
require.Equal(t, "passpass", infrastructure.nodes[0].credentials.Password)
require.Equal(t, "johndoe", infrastructure.nodes[0].credentials.User)
require.Equal(t, "passpass", infrastructure.nodes[0].credentials.Token)
require.Equal(t, "resa_123", infrastructure.nodes[0].reservation)
require.Equal(t, "account_test", infrastructure.nodes[0].account)
}
Expand All @@ -72,8 +72,8 @@ func testSimpleSlurmNodeAllocationWithoutProps(t *testing.T, kv *api.KV, cfg con
require.Equal(t, "", infrastructure.nodes[0].partition)
require.Equal(t, "", infrastructure.nodes[0].memory)
require.Equal(t, "", infrastructure.nodes[0].cpu)
require.Equal(t, "", infrastructure.nodes[0].credentials.UserName)
require.Equal(t, "", infrastructure.nodes[0].credentials.Password)
require.Equal(t, "root", infrastructure.nodes[0].credentials.User)
require.Equal(t, "pwd", infrastructure.nodes[0].credentials.Token)
require.Equal(t, "simpleSlurmNodeAllocationWithoutProps", infrastructure.nodes[0].jobName)
}

Expand Down
16 changes: 6 additions & 10 deletions prov/slurm/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,24 @@

package slurm

import "time"
import (
"time"

"github.com/ystia/yorc/v3/tosca/datatypes"
)

type infrastructure struct {
nodes []*nodeAllocation
}

// UserCredentials represents the Slurm user credentials
type UserCredentials struct {
UserName string `json:"user_name,omitempty"`
Password string `json:"password,omitempty"`
PrivateKey string `json:"private_key,omitempty"`
}

type nodeAllocation struct {
cpu string
memory string
gres string
constraint string
partition string
jobName string
credentials *UserCredentials
credentials *datatypes.Credential
instanceName string
account string
reservation string
Expand All @@ -53,7 +50,6 @@ type jobInfo struct {
EnvVars []string `json:"env_vars,omitempty"`
Inputs map[string]string `json:"inputs,omitempty"`
MonitoringTimeInterval time.Duration `json:"monitoring_time_interval,omitempty"`
Credentials *UserCredentials `json:"credentials,omitempty"`
Account string `json:"account,omitempty"`
Reservation string `json:"reservation,omitempty"`
Command string `json:"command,omitempty"`
Expand Down
2 changes: 1 addition & 1 deletion prov/terraform/aws/aws_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func (g *awsGenerator) generateAWSInstance(ctx context.Context, kv *api.KV, cfg
}

// Get connection info (user, private key)
user, privateKey, err := commons.GetConnInfoFromEndpointCredentials(kv, deploymentID, nodeName)
user, privateKey, err := commons.GetConnInfoFromEndpointCredentials(ctx, kv, deploymentID, nodeName)
if err != nil {
return err
}
Expand Down
36 changes: 31 additions & 5 deletions prov/terraform/aws/aws_instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,38 @@ import (
"github.com/ystia/yorc/v3/config"
"github.com/ystia/yorc/v3/deployments"
"github.com/ystia/yorc/v3/helper/consulutil"
"github.com/ystia/yorc/v3/helper/sshutil"
"github.com/ystia/yorc/v3/prov/terraform/commons"
)

var expectedPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAuEl5Wjgdvlqbz0x2vcllSQrDiRd+bWdA2MgpOl726ovxw9uE
QJSlXYBJbSCQg+q++OEtXmvfahN5Y9aemuPey/o/S9RWyQ/X+uVeXdNV4Xkgar6b
uYr1n1Ju7ltmdVJME7fr+Ti+2d+EMBs7V+jGXyZzBTdr6wCJuBHHXV/ZKDzw1cHd
bRF8obBmMcxyzNbXnhSUvBgXT+GQ0/CgkNdrTwGOgtckqNYTuw1Rd6wAsF5xgN23
uss5WJOg3/eMW2JMjyxNqaJhBUtA2CKcdnLjwyDxWdmC1NMHKL1umPOjuCyNczpl
axMKW//UZT3WyfVt/gcHGGNIuI0izwFJ6QjlrQIDAQABAoIBAAet8COlUP/8sJ97
1TrlaJYZn7pXw0n10or2FFm9WVa+zC1YOXOjfhyeWvD0OXF1181xPL3BiwbVlupl
KCjWNBOV8wtK5u7r/RkUc9E/HEYQERzBoqWht8iS29KM9oEPE+KCeI/jIHjdypli
mR95sMKITKS8AYBCfnqwKvmmI9t8VIXsrZWsg1dUD9TCa8QxoA66raSpXegDgjox
T8IjZW90BwD6oG/5+HfbuwtjKR1Lca5tMzqxDMvqBf3KdCuee1x2Uuzla9/MsK/4
Nuqv88gpoI7bDJOJnF/KrJqEH1ihF5zNVOs5c7XKmnAdry05tA7CjbiILOeFq3yn
elkdR5UCgYEA3RC0bAR/TjSKGBEzvzfJFNp2ipdlZ1svobHl5sqDJzQp7P9aIogU
qUhw2vr/nHg4dfmLnMHJYh6GCIjN1H7NZzaBiQSUcT+s2GRxYJqRV4geFHvKNzt3
a50Hi5rSsbKm0LvlUA3vGkMABICyzkETPDl2WSFtKWUYrTMZSKixCtsCgYEA1Wjj
fn+3HOmAv3lX4PzhHiBBfBj60BKPCnBbWG6TTF4ya7UEU+e5aAbLD10QdQyx77rL
V3G3Xda1BWA2wGKRDfC9ksFUuxH2egNPGadOVZH2U/a/87YGOFUmbf03jJ6mbeRV
BBBVcB8oGSD+NemiDPqYUi/G1lT+oRLFIkkYhBcCgYEApjKj4j2zVCFt3NA5/j27
gGEKFAHka8MDWWY8uLlxxuyRxKrpoeJ63hYnOorP11wO3qshClYqyAi4rfvj+yjl
1f4FfvShgU7k7L7++ijaslsUekPi8IlVq+MfxBY+5vewMGfC69+97hmHDtuPEj+c
bX+p+TKHNkLaPYSYMqcYi1cCgYEAxf6JSfyt48oT5BFtYdTb+zpL5xm54T/GrBWv
+eylBm5Cc0E/YaUUlBnxXTCnqyD7GQKB04AycoJX8kPgqD8KexeGmlh6BxFUTsEx
KwjZGXTRR/cfAbo4LR17CQKr/e/XUw9LfPi2e868QgwlLdmzujzpAx9GZ+X1U3V5
piSQ9UMCgYBdegnYh2fqU/oGH+d9WahuO1LW9vz8sFEIhRgJyLfA/ypAg6WCgJF2
GtepEYBXL+QZnhudVxi0YPTmNN3+gtHdr+B4dKZ8z7m9NO2nk5AKdf0sYGWHEzhy
PAgZzG5OTZiu+YohUPnC66eFiyS6anLBj0DGNa9VA8j352ecgeNO4A==
-----END RSA PRIVATE KEY-----
`

func loadTestYaml(t *testing.T, kv *api.KV) string {
deploymentID := path.Base(t.Name())
yamlName := "testdata/" + deploymentID + ".yaml"
Expand Down Expand Up @@ -92,11 +120,10 @@ func testSimpleAWSInstance(t *testing.T, kv *api.KV, cfg config.Configuration) {
require.True(t, ok)
require.Equal(t, "centos", rex.Connection.User)

yorcPem, err := sshutil.ToPrivateKeyContent("~/.ssh/yorc.pem")
require.Nil(t, err)
assert.Equal(t, "${var.private_key}", rex.Connection.PrivateKey)
require.Len(t, env, 1)
assert.Equal(t, "TF_VAR_private_key="+string(yorcPem), env[0], "env var for private key expected")
assert.Equal(t, "TF_VAR_private_key="+expectedPrivateKey, env[0], "env var for private key expected")

require.NotContains(t, infrastructure.Resource, "aws_eip_association")

Expand All @@ -110,7 +137,6 @@ func testSimpleAWSInstance(t *testing.T, kv *api.KV, cfg config.Configuration) {
}

func testSimpleAWSInstanceWithPrivateKey(t *testing.T, kv *api.KV, cfg config.Configuration) {
privateKey := []byte(`-----BEGIN RSA PRIVATE KEY----- my secure private key -----END RSA PRIVATE KEY-----`)
t.Parallel()
deploymentID := loadTestYaml(t, kv)
g := awsGenerator{}
Expand Down Expand Up @@ -139,7 +165,7 @@ func testSimpleAWSInstanceWithPrivateKey(t *testing.T, kv *api.KV, cfg config.Co
require.Equal(t, "centos", rex.Connection.User)
assert.Equal(t, "${var.private_key}", rex.Connection.PrivateKey)
require.Len(t, env, 1)
assert.Equal(t, "TF_VAR_private_key="+string(privateKey), env[0], "env var for private key expected")
assert.Equal(t, "TF_VAR_private_key="+expectedPrivateKey, env[0], "env var for private key expected")

require.NotContains(t, infrastructure.Resource, "aws_eip_association")
}
Expand Down
33 changes: 32 additions & 1 deletion prov/terraform/aws/testdata/simpleAWSInstance.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,36 @@ topology_template:
protocol: tcp
network_name: PRIVATE
initiator: source
credentials: {user: centos}
credentials:
user: centos
keys:
0: |
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAuEl5Wjgdvlqbz0x2vcllSQrDiRd+bWdA2MgpOl726ovxw9uE
QJSlXYBJbSCQg+q++OEtXmvfahN5Y9aemuPey/o/S9RWyQ/X+uVeXdNV4Xkgar6b
uYr1n1Ju7ltmdVJME7fr+Ti+2d+EMBs7V+jGXyZzBTdr6wCJuBHHXV/ZKDzw1cHd
bRF8obBmMcxyzNbXnhSUvBgXT+GQ0/CgkNdrTwGOgtckqNYTuw1Rd6wAsF5xgN23
uss5WJOg3/eMW2JMjyxNqaJhBUtA2CKcdnLjwyDxWdmC1NMHKL1umPOjuCyNczpl
axMKW//UZT3WyfVt/gcHGGNIuI0izwFJ6QjlrQIDAQABAoIBAAet8COlUP/8sJ97
1TrlaJYZn7pXw0n10or2FFm9WVa+zC1YOXOjfhyeWvD0OXF1181xPL3BiwbVlupl
KCjWNBOV8wtK5u7r/RkUc9E/HEYQERzBoqWht8iS29KM9oEPE+KCeI/jIHjdypli
mR95sMKITKS8AYBCfnqwKvmmI9t8VIXsrZWsg1dUD9TCa8QxoA66raSpXegDgjox
T8IjZW90BwD6oG/5+HfbuwtjKR1Lca5tMzqxDMvqBf3KdCuee1x2Uuzla9/MsK/4
Nuqv88gpoI7bDJOJnF/KrJqEH1ihF5zNVOs5c7XKmnAdry05tA7CjbiILOeFq3yn
elkdR5UCgYEA3RC0bAR/TjSKGBEzvzfJFNp2ipdlZ1svobHl5sqDJzQp7P9aIogU
qUhw2vr/nHg4dfmLnMHJYh6GCIjN1H7NZzaBiQSUcT+s2GRxYJqRV4geFHvKNzt3
a50Hi5rSsbKm0LvlUA3vGkMABICyzkETPDl2WSFtKWUYrTMZSKixCtsCgYEA1Wjj
fn+3HOmAv3lX4PzhHiBBfBj60BKPCnBbWG6TTF4ya7UEU+e5aAbLD10QdQyx77rL
V3G3Xda1BWA2wGKRDfC9ksFUuxH2egNPGadOVZH2U/a/87YGOFUmbf03jJ6mbeRV
BBBVcB8oGSD+NemiDPqYUi/G1lT+oRLFIkkYhBcCgYEApjKj4j2zVCFt3NA5/j27
gGEKFAHka8MDWWY8uLlxxuyRxKrpoeJ63hYnOorP11wO3qshClYqyAi4rfvj+yjl
1f4FfvShgU7k7L7++ijaslsUekPi8IlVq+MfxBY+5vewMGfC69+97hmHDtuPEj+c
bX+p+TKHNkLaPYSYMqcYi1cCgYEAxf6JSfyt48oT5BFtYdTb+zpL5xm54T/GrBWv
+eylBm5Cc0E/YaUUlBnxXTCnqyD7GQKB04AycoJX8kPgqD8KexeGmlh6BxFUTsEx
KwjZGXTRR/cfAbo4LR17CQKr/e/XUw9LfPi2e868QgwlLdmzujzpAx9GZ+X1U3V5
piSQ9UMCgYBdegnYh2fqU/oGH+d9WahuO1LW9vz8sFEIhRgJyLfA/ypAg6WCgJF2
GtepEYBXL+QZnhudVxi0YPTmNN3+gtHdr+B4dKZ8z7m9NO2nk5AKdf0sYGWHEzhy
PAgZzG5OTZiu+YohUPnC66eFiyS6anLBj0DGNa9VA8j352ecgeNO4A==
-----END RSA PRIVATE KEY-----
29 changes: 28 additions & 1 deletion prov/terraform/aws/testdata/simpleAWSInstanceWithPrivateKey.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,32 @@ topology_template:
credentials:
user: centos
keys:
0: "-----BEGIN RSA PRIVATE KEY----- my secure private key -----END RSA PRIVATE KEY-----"
0: |
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAuEl5Wjgdvlqbz0x2vcllSQrDiRd+bWdA2MgpOl726ovxw9uE
QJSlXYBJbSCQg+q++OEtXmvfahN5Y9aemuPey/o/S9RWyQ/X+uVeXdNV4Xkgar6b
uYr1n1Ju7ltmdVJME7fr+Ti+2d+EMBs7V+jGXyZzBTdr6wCJuBHHXV/ZKDzw1cHd
bRF8obBmMcxyzNbXnhSUvBgXT+GQ0/CgkNdrTwGOgtckqNYTuw1Rd6wAsF5xgN23
uss5WJOg3/eMW2JMjyxNqaJhBUtA2CKcdnLjwyDxWdmC1NMHKL1umPOjuCyNczpl
axMKW//UZT3WyfVt/gcHGGNIuI0izwFJ6QjlrQIDAQABAoIBAAet8COlUP/8sJ97
1TrlaJYZn7pXw0n10or2FFm9WVa+zC1YOXOjfhyeWvD0OXF1181xPL3BiwbVlupl
KCjWNBOV8wtK5u7r/RkUc9E/HEYQERzBoqWht8iS29KM9oEPE+KCeI/jIHjdypli
mR95sMKITKS8AYBCfnqwKvmmI9t8VIXsrZWsg1dUD9TCa8QxoA66raSpXegDgjox
T8IjZW90BwD6oG/5+HfbuwtjKR1Lca5tMzqxDMvqBf3KdCuee1x2Uuzla9/MsK/4
Nuqv88gpoI7bDJOJnF/KrJqEH1ihF5zNVOs5c7XKmnAdry05tA7CjbiILOeFq3yn
elkdR5UCgYEA3RC0bAR/TjSKGBEzvzfJFNp2ipdlZ1svobHl5sqDJzQp7P9aIogU
qUhw2vr/nHg4dfmLnMHJYh6GCIjN1H7NZzaBiQSUcT+s2GRxYJqRV4geFHvKNzt3
a50Hi5rSsbKm0LvlUA3vGkMABICyzkETPDl2WSFtKWUYrTMZSKixCtsCgYEA1Wjj
fn+3HOmAv3lX4PzhHiBBfBj60BKPCnBbWG6TTF4ya7UEU+e5aAbLD10QdQyx77rL
V3G3Xda1BWA2wGKRDfC9ksFUuxH2egNPGadOVZH2U/a/87YGOFUmbf03jJ6mbeRV
BBBVcB8oGSD+NemiDPqYUi/G1lT+oRLFIkkYhBcCgYEApjKj4j2zVCFt3NA5/j27
gGEKFAHka8MDWWY8uLlxxuyRxKrpoeJ63hYnOorP11wO3qshClYqyAi4rfvj+yjl
1f4FfvShgU7k7L7++ijaslsUekPi8IlVq+MfxBY+5vewMGfC69+97hmHDtuPEj+c
bX+p+TKHNkLaPYSYMqcYi1cCgYEAxf6JSfyt48oT5BFtYdTb+zpL5xm54T/GrBWv
+eylBm5Cc0E/YaUUlBnxXTCnqyD7GQKB04AycoJX8kPgqD8KexeGmlh6BxFUTsEx
KwjZGXTRR/cfAbo4LR17CQKr/e/XUw9LfPi2e868QgwlLdmzujzpAx9GZ+X1U3V5
piSQ9UMCgYBdegnYh2fqU/oGH+d9WahuO1LW9vz8sFEIhRgJyLfA/ypAg6WCgJF2
GtepEYBXL+QZnhudVxi0YPTmNN3+gtHdr+B4dKZ8z7m9NO2nk5AKdf0sYGWHEzhy
PAgZzG5OTZiu+YohUPnC66eFiyS6anLBj0DGNa9VA8j352ecgeNO4A==
-----END RSA PRIVATE KEY-----
60 changes: 27 additions & 33 deletions prov/terraform/commons/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,41 +147,48 @@ func AddOutput(infrastructure *Infrastructure, outputName string, output *Output
}

// GetConnInfoFromEndpointCredentials allow to retrieve user and private key path for connection needs from endpoint credentials
func GetConnInfoFromEndpointCredentials(kv *api.KV, deploymentID, nodeName string) (string, string, error) {
func GetConnInfoFromEndpointCredentials(ctx context.Context, kv *api.KV, deploymentID, nodeName string) (string, *sshutil.PrivateKey, error) {
user, err := deployments.GetCapabilityPropertyValue(kv, deploymentID, nodeName, "endpoint", "credentials", "user")
if err != nil {
return "", "", err
return "", nil, err
} else if user == nil || user.RawString() == "" {
return "", "", errors.Errorf("Missing mandatory parameter 'user' node type for %s", nodeName)
return "", nil, errors.Errorf("Missing mandatory parameter 'user' node type for %s", nodeName)
}
var pkfp string
privateKeyFilePath, err := deployments.GetCapabilityPropertyValue(kv, deploymentID, nodeName, "endpoint", "credentials", "keys", "0")
keys, err := sshutil.GetKeysFromCredentialsAttribute(kv, deploymentID, nodeName, "0", "endpoint")
if err != nil {
return "", "", err
} else if privateKeyFilePath == nil || privateKeyFilePath.RawString() == "" {
// Using default value
pkfp = DefaultSSHPrivateKeyFilePath
log.Printf("No private key defined for user %s, using default %s", user.RawString(), pkfp)
return "", nil, err
}

var pk *sshutil.PrivateKey
if len(keys) == 0 {
pk, err = sshutil.GetDefaultKey()
if err != nil {
return "", nil, err
}
keys["default"] = pk
} else {
pkfp = privateKeyFilePath.RawString()
pk = sshutil.SelectPrivateKeyOnName(keys, false)

}
return user.RawString(), pkfp, nil
}

// AddConnectionCheckResource builds a null specific resource to check SSH connection with SSH key passed via env variable
func AddConnectionCheckResource(infrastructure *Infrastructure, user, privateKey, accessIP, resourceName string, env *[]string) error {
// Check the connection in order to be sure that ansible will be able to log on the instance
pkeyContent, err := sshutil.ToPrivateKeyContent(privateKey)
if err != nil {
return errors.Wrapf(err, "failed to retrieve private key content")
keysList := make([]*sshutil.PrivateKey, 0, len(keys))
for _, k := range keys {
keysList = append(keysList, k)
}

addKeysToContextualSSHAgent(ctx, keysList)

return user.RawString(), pk, nil
}

// AddConnectionCheckResource builds a null specific resource to check SSH connection with SSH key passed via env variable
func AddConnectionCheckResource(infrastructure *Infrastructure, user string, privateKey *sshutil.PrivateKey, accessIP, resourceName string, env *[]string) error {
// Define private_key variable
infrastructure.Variable = make(map[string]interface{})
infrastructure.Variable["private_key"] = struct{}{}

// Add env TF variable for private key
*env = append(*env, fmt.Sprintf("%s=%s", "TF_VAR_private_key", string(pkeyContent)))
*env = append(*env, fmt.Sprintf("%s=%s", "TF_VAR_private_key", string(privateKey.Content)))

// Build null Resource
nullResource := Resource{}
Expand All @@ -200,19 +207,6 @@ func AddConnectionCheckResource(infrastructure *Infrastructure, user, privateKey
return nil
}

// GetSSHAgent provides an SSH-agent for specific Terraform needs
func GetSSHAgent(ctx context.Context, privateKey string) (*sshutil.SSHAgent, error) {
sshAgent, err := sshutil.NewSSHAgent(ctx)
if err != nil {
return nil, err
}
err = sshAgent.AddKey(privateKey, 3600)
if err != nil {
return nil, err
}
return sshAgent, nil
}

// GetBackendConfiguration returns the Terraform Backend configuration
// to store the state in the Consul KV store at a given path
func GetBackendConfiguration(path string, cfg config.Configuration) map[string]interface{} {
Expand Down
58 changes: 58 additions & 0 deletions prov/terraform/commons/ssh_agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2019 Bull S.A.S. Atos Technologies - Bull, Rue Jean Jaures, B.P.68, 78340, Les Clayes-sous-Bois, France.
//
// 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 commons

import (
"context"

"github.com/pkg/errors"

"github.com/ystia/yorc/v3/helper/sshutil"
)

// The contextKey type is unexported to prevent collisions with context keys defined in
// other packages.
type contextKey int

// sshAgentkey is the context key for the ssh agent. Its value of zero is
// arbitrary. If this package defined other context keys, they would have
// different integer values.
const sshAgentkey contextKey = 0

// StoreSSHAgentInContext allows to store an sshutil.SSHAgent into the given Context
func StoreSSHAgentInContext(ctx context.Context, agent *sshutil.SSHAgent) context.Context {
return context.WithValue(ctx, sshAgentkey, agent)
}

// SSHAgentFromContext retrieves an sshutil.SSHAgent from the given Context
func SSHAgentFromContext(ctx context.Context) (*sshutil.SSHAgent, bool) {
existingAgent, ok := ctx.Value(sshAgentkey).(*sshutil.SSHAgent)
return existingAgent, ok
}

func addKeysToContextualSSHAgent(ctx context.Context, keys []*sshutil.PrivateKey) {
addKeysToContextualSSHAgentOrFail(ctx, keys)
}

func addKeysToContextualSSHAgentOrFail(ctx context.Context, keys []*sshutil.PrivateKey) error {
agent, ok := SSHAgentFromContext(ctx)
if !ok {
return errors.New("no contextual ssh-agent")
}
for _, key := range keys {
agent.AddPrivateKey(key, 3600)
}
return nil
}
29 changes: 25 additions & 4 deletions prov/terraform/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/ystia/yorc/v3/events"
"github.com/ystia/yorc/v3/helper/consulutil"
"github.com/ystia/yorc/v3/helper/executil"
"github.com/ystia/yorc/v3/helper/sshutil"
"github.com/ystia/yorc/v3/log"
"github.com/ystia/yorc/v3/prov"
"github.com/ystia/yorc/v3/prov/terraform/commons"
Expand Down Expand Up @@ -93,16 +94,36 @@ func (e *defaultExecutor) installNode(ctx context.Context, kv *api.KV, cfg confi
}
}

infraGenerated, outputs, env, cb, err := e.generator.GenerateTerraformInfraForNode(ctx, cfg, deploymentID, nodeName, infrastructurePath)
if err != nil {
return err
if !cfg.DisableSSHAgent {
sshAgent, err := sshutil.NewSSHAgent(ctx)
if err != nil {
return err
}
ctx = commons.StoreSSHAgentInContext(ctx, sshAgent)
defer func() {
// Stop the sshAgent if used during provisioning
// Do not return any error if failure occured during this
err := sshAgent.RemoveAllKeys()
if err != nil {
log.Debugf("Warning: failed to remove all SSH agents keys due to error:%+v", err)
}
err = sshAgent.Stop()
if err != nil {
log.Debugf("Warning: failed to stop SSH agent due to error:%+v", err)
}
}()
}
// Execute callback if needed

infraGenerated, outputs, env, cb, err := e.generator.GenerateTerraformInfraForNode(ctx, cfg, deploymentID, nodeName, infrastructurePath)
// Execute callback if needed even if there is an error
defer func() {
if cb != nil {
cb()
}
}()
if err != nil {
return err
}
if infraGenerated {
if err = e.applyInfrastructure(ctx, kv, cfg, deploymentID, nodeName, infrastructurePath, outputs, env); err != nil {
return err
Expand Down
39 changes: 12 additions & 27 deletions prov/terraform/google/compute_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,24 @@ package google
import (
"context"
"fmt"
"github.com/hashicorp/consul/api"
"github.com/pkg/errors"
"golang.org/x/crypto/ssh"
"path"
"strings"

"github.com/hashicorp/consul/api"
"github.com/pkg/errors"

"github.com/ystia/yorc/v3/config"
"github.com/ystia/yorc/v3/deployments"
"github.com/ystia/yorc/v3/helper/consulutil"
"github.com/ystia/yorc/v3/helper/pathutil"
"github.com/ystia/yorc/v3/helper/sshutil"
"github.com/ystia/yorc/v3/helper/stringutil"
"github.com/ystia/yorc/v3/log"
"github.com/ystia/yorc/v3/prov/terraform/commons"
)

func (g *googleGenerator) generateComputeInstance(ctx context.Context, kv *api.KV,
cfg config.Configuration, deploymentID, nodeName, instanceName string, instanceID int,
infrastructure *commons.Infrastructure,
outputs map[string]string, env *[]string, sshAgent *sshutil.SSHAgent) error {
outputs map[string]string, env *[]string) error {

nodeType, err := deployments.GetNodeType(kv, deploymentID, nodeName)
if err != nil {
Expand Down Expand Up @@ -194,7 +192,7 @@ func (g *googleGenerator) generateComputeInstance(ctx context.Context, kv *api.K
}

// Get connection info (user, private key)
user, privateKey, err := commons.GetConnInfoFromEndpointCredentials(kv, deploymentID, nodeName)
user, privateKey, err := commons.GetConnInfoFromEndpointCredentials(ctx, kv, deploymentID, nodeName)
if err != nil {
return err
}
Expand Down Expand Up @@ -273,23 +271,15 @@ func (g *googleGenerator) generateComputeInstance(ctx context.Context, kv *api.K

// Retrieve devices
if len(devices) > 0 {
// need to use an SSH Agent to make it if allowed by config
if !cfg.DisableSSHAgent && sshAgent == nil {
sshAgent, err = commons.GetSSHAgent(ctx, privateKey)
if err != nil {
return err
}
}

if err = handleDeviceAttributes(cfg, infrastructure, &instance, devices, user, privateKey, accessIP, sshAgent); err != nil {
if err = handleDeviceAttributes(ctx, cfg, infrastructure, &instance, devices, user, privateKey, accessIP); err != nil {
return err
}
}

return nil
}

func handleDeviceAttributes(cfg config.Configuration, infrastructure *commons.Infrastructure, instance *ComputeInstance, devices []string, user, privateKey, accessIP string, sshAgent *sshutil.SSHAgent) error {
func handleDeviceAttributes(ctx context.Context, cfg config.Configuration, infrastructure *commons.Infrastructure, instance *ComputeInstance, devices []string, user string, privateKey *sshutil.PrivateKey, accessIP string) error {
var env map[string]interface{}
// Retrieve devices {
for _, dev := range devices {
Expand All @@ -310,21 +300,16 @@ func handleDeviceAttributes(cfg config.Configuration, infrastructure *commons.In

// local exec to scp the stdout file locally (use ssh-agent to make it if allowed by config)
var scpCommand string
if !cfg.DisableSSHAgent {
sshAgent, ok := commons.SSHAgentFromContext(ctx)
if ok {
scpCommand = fmt.Sprintf("scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null %s@%s:~/%s %s", user, accessIP, dev, dev)
env = make(map[string]interface{})
env["SSH_AUTH_SOCK"] = sshAgent.Socket
} else {
// check privateKey's a valid path
if is, err := pathutil.IsValidPath(privateKey); err != nil || !is {
// Truncate it if it's a private key
ufo := privateKey
if _, err = ssh.ParsePrivateKey([]byte(privateKey)); err == nil {
ufo = stringutil.Truncate(privateKey, 20)
}
return errors.Errorf("%q is not a valid path", ufo)
if privateKey.Path == "" {
return errors.New("trying to get GCP volumes devices with an ssh private key that is not stored on disk, this is possible only with an ssh agent, unfortunately ssh-agent is currently disabled by configuration")
}
scpCommand = fmt.Sprintf("scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i %s %s@%s:~/%s %s", privateKey, user, accessIP, dev, dev)
scpCommand = fmt.Sprintf("scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i %s %s@%s:~/%s %s", privateKey.Path, user, accessIP, dev, dev)
}
loc := commons.LocalExec{
Command: scpCommand,
Expand Down
26 changes: 19 additions & 7 deletions prov/terraform/google/compute_instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package google
import (
"context"
"fmt"
"io/ioutil"
"path"
"testing"

Expand All @@ -31,6 +32,16 @@ import (
"github.com/ystia/yorc/v3/prov/terraform/commons"
)

var expectedKey []byte

func init() {
var err error
expectedKey, err = ioutil.ReadFile("./testdata/mykey.pem")
if err != nil {
panic(err)
}
}

func loadTestYaml(t *testing.T, kv *api.KV) string {
deploymentID := path.Base(t.Name())
yamlName := "testdata/" + deploymentID + ".yaml"
Expand All @@ -40,14 +51,14 @@ func loadTestYaml(t *testing.T, kv *api.KV) string {
}

func testSimpleComputeInstance(t *testing.T, kv *api.KV, cfg config.Configuration) {
privateKey := []byte(`-----BEGIN RSA PRIVATE KEY----- my secure private key -----END RSA PRIVATE KEY-----`)
privateKey := expectedKey
t.Parallel()
deploymentID := loadTestYaml(t, kv)
resourcePrefix := getResourcesPrefix(cfg, deploymentID)
infrastructure := commons.Infrastructure{}
g := googleGenerator{}
env := make([]string, 0)
err := g.generateComputeInstance(context.Background(), kv, cfg, deploymentID, "ComputeInstance", "0", 0, &infrastructure, make(map[string]string), &env, nil)
err := g.generateComputeInstance(context.Background(), kv, cfg, deploymentID, "ComputeInstance", "0", 0, &infrastructure, make(map[string]string), &env)
require.NoError(t, err, "Unexpected error attempting to generate compute instance for %s", deploymentID)

instanceName := resourcePrefix + "computeinstance-0"
Expand Down Expand Up @@ -105,7 +116,7 @@ func testSimpleComputeInstanceMissingMandatoryParameter(t *testing.T, kv *api.KV
env := make([]string, 0)
infrastructure := commons.Infrastructure{}

err := g.generateComputeInstance(context.Background(), kv, cfg, deploymentID, "ComputeInstance", "0", 0, &infrastructure, make(map[string]string), &env, nil)
err := g.generateComputeInstance(context.Background(), kv, cfg, deploymentID, "ComputeInstance", "0", 0, &infrastructure, make(map[string]string), &env)
require.Error(t, err, "Expected missing mandatory parameter error, but had no error")
assert.Contains(t, err.Error(), "mandatory parameter zone", "Expected an error on missing parameter zone")
}
Expand All @@ -123,7 +134,7 @@ func testSimpleComputeInstanceWithAddress(t *testing.T, kv *api.KV, srv1 *testut
infrastructure := commons.Infrastructure{}
g := googleGenerator{}
env := make([]string, 0)
err := g.generateComputeInstance(context.Background(), kv, cfg, deploymentID, "Compute", "0", 0, &infrastructure, make(map[string]string), &env, nil)
err := g.generateComputeInstance(context.Background(), kv, cfg, deploymentID, "Compute", "0", 0, &infrastructure, make(map[string]string), &env)
require.NoError(t, err, "Unexpected error attempting to generate compute instance for %s", deploymentID)
resourcePrefix := getResourcesPrefix(cfg, deploymentID)
instanceName := resourcePrefix + "compute-0"
Expand Down Expand Up @@ -157,7 +168,7 @@ func testSimpleComputeInstanceWithPersistentDisk(t *testing.T, kv *api.KV, srv1
g := googleGenerator{}
env := make([]string, 0)
outputs := make(map[string]string, 0)
err := g.generateComputeInstance(context.Background(), kv, cfg, deploymentID, "Compute", "0", 0, &infrastructure, outputs, &env, nil)
err := g.generateComputeInstance(context.Background(), kv, cfg, deploymentID, "Compute", "0", 0, &infrastructure, outputs, &env)
require.NoError(t, err, "Unexpected error attempting to generate compute instance for %s", deploymentID)

resourcePrefix := getResourcesPrefix(cfg, deploymentID)
Expand Down Expand Up @@ -208,6 +219,7 @@ func testSimpleComputeInstanceWithPersistentDisk(t *testing.T, kv *api.KV, srv1
require.Equal(t, deviceAttribute, outputs[path.Join(consulutil.DeploymentKVPrefix, deploymentID+"/topology/instances/", "BS1", "0", "attributes/device")], "output file value expected")
require.Equal(t, deviceAttribute, outputs[path.Join(consulutil.DeploymentKVPrefix, deploymentID+"/topology/relationship_instances/", "Compute", "0", "0", "attributes/device")], "output file value expected")
require.Equal(t, deviceAttribute, outputs[path.Join(consulutil.DeploymentKVPrefix, deploymentID+"/topology/relationship_instances/", "BS1", "0", "0", "attributes/device")], "output file value expected")

}

func testSimpleComputeInstanceWithAutoCreationModeNetwork(t *testing.T, kv *api.KV, srv1 *testutil.TestServer, cfg config.Configuration) {
Expand All @@ -225,7 +237,7 @@ func testSimpleComputeInstanceWithAutoCreationModeNetwork(t *testing.T, kv *api.
g := googleGenerator{}
env := make([]string, 0)
outputs := make(map[string]string, 0)
err := g.generateComputeInstance(context.Background(), kv, cfg, deploymentID, "Compute", "0", 0, &infrastructure, outputs, &env, nil)
err := g.generateComputeInstance(context.Background(), kv, cfg, deploymentID, "Compute", "0", 0, &infrastructure, outputs, &env)
require.NoError(t, err, "Unexpected error attempting to generate compute instance for %s", deploymentID)

require.Len(t, infrastructure.Resource["google_compute_instance"], 1, "Expected one compute instance")
Expand Down Expand Up @@ -255,7 +267,7 @@ func testSimpleComputeInstanceWithSimpleNetwork(t *testing.T, kv *api.KV, srv1 *
g := googleGenerator{}
env := make([]string, 0)
outputs := make(map[string]string, 0)
err := g.generateComputeInstance(context.Background(), kv, cfg, deploymentID, "Comp1", "0", 0, &infrastructure, outputs, &env, nil)
err := g.generateComputeInstance(context.Background(), kv, cfg, deploymentID, "Comp1", "0", 0, &infrastructure, outputs, &env)
require.NoError(t, err, "Unexpected error attempting to generate compute instance for %s", deploymentID)

require.Len(t, infrastructure.Resource["google_compute_instance"], 1, "Expected one compute instance")
Expand Down
3 changes: 1 addition & 2 deletions prov/terraform/google/consul_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,12 @@ func TestRunConsulGooglePackageTests(t *testing.T) {

// AWS infrastructure config
cfg := config.Configuration{
DisableSSHAgent: true,
DisableSSHAgent: false,
Infrastructures: map[string]config.DynamicMap{
infrastructureName: config.DynamicMap{
"credentials": "/tmp/creds.json",
"region": "europe-west-1",
}}}

t.Run("googleProvider", func(t *testing.T) {
t.Run("simpleComputeInstance", func(t *testing.T) {
testSimpleComputeInstance(t, kv, cfg)
Expand Down
25 changes: 2 additions & 23 deletions prov/terraform/google/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import (
"github.com/ystia/yorc/v3/config"
"github.com/ystia/yorc/v3/deployments"
"github.com/ystia/yorc/v3/helper/consulutil"
"github.com/ystia/yorc/v3/helper/sshutil"
"github.com/ystia/yorc/v3/log"
"github.com/ystia/yorc/v3/prov/terraform/commons"
"github.com/ystia/yorc/v3/tosca"
Expand Down Expand Up @@ -89,8 +88,6 @@ func (g *googleGenerator) GenerateTerraformInfraForNode(ctx context.Context, cfg
return false, nil, nil, nil, err
}

var sshAgent *sshutil.SSHAgent

for instNb, instanceName := range instances {
instanceState, err := deployments.GetInstanceState(kv, deploymentID, nodeName, instanceName)
if err != nil {
Expand All @@ -108,7 +105,7 @@ func (g *googleGenerator) GenerateTerraformInfraForNode(ctx context.Context, cfg
return false, nil, nil, nil, err
}

err = g.generateComputeInstance(ctx, kv, cfg, deploymentID, nodeName, instanceName, instNb, &infrastructure, outputs, &cmdEnv, sshAgent)
err := g.generateComputeInstance(ctx, kv, cfg, deploymentID, nodeName, instanceName, instNb, &infrastructure, outputs, &cmdEnv)
if err != nil {
return false, nil, nil, nil, err
}
Expand Down Expand Up @@ -138,24 +135,6 @@ func (g *googleGenerator) GenerateTerraformInfraForNode(ctx context.Context, cfg

}

// If ssh-agent has been created, it needs to be stopped after the infrastructure creation
// This is done with this callback
var postInstallCb func()
if sshAgent != nil {
postInstallCb = func() {
// Stop the sshAgent if used during provisioning
// Do not return any error if failure occured during this
err := sshAgent.RemoveAllKeys()
if err != nil {
log.Debugf("Warning: failed to remove all SSH agents keys due to error:%+v", err)
}
err = sshAgent.Stop()
if err != nil {
log.Debugf("Warning: failed to stop SSH agent due to error:%+v", err)
}
}
}

jsonInfra, err := json.MarshalIndent(infrastructure, "", " ")
if err != nil {
return false, nil, nil, nil, errors.Wrap(err, "Failed to generate JSON of terraform Infrastructure description")
Expand All @@ -166,5 +145,5 @@ func (g *googleGenerator) GenerateTerraformInfraForNode(ctx context.Context, cfg
}

log.Debugf("Infrastructure generated for deployment with id %s", deploymentID)
return true, outputs, cmdEnv, postInstallCb, nil
return true, outputs, cmdEnv, nil, nil
}
2 changes: 1 addition & 1 deletion prov/terraform/google/testdata/simpleComputeInstance.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,5 @@ topology_template:
credentials:
user: centos
keys:
0: "-----BEGIN RSA PRIVATE KEY----- my secure private key -----END RSA PRIVATE KEY-----"
0: "./testdata/mykey.pem"

11 changes: 5 additions & 6 deletions prov/terraform/openstack/osinstance.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,6 @@ func (g *osGenerator) generateOSInstance(ctx context.Context, kv *api.KV, cfg co
instance.Networks = append(instance.Networks, ComputeNetwork{Name: defaultPrivateNetName, AccessNetwork: true})
}

// Get connection info (user, private key)
user, privateKey, err := commons.GetConnInfoFromEndpointCredentials(kv, deploymentID, nodeName)
if err != nil {
return err
}

storageKeys, err := deployments.GetRequirementsKeysByTypeForNode(kv, deploymentID, nodeName, "local_storage")
if err != nil {
return err
Expand Down Expand Up @@ -364,6 +358,11 @@ func (g *osGenerator) generateOSInstance(ctx context.Context, kv *api.KV, cfg co
return errors.Wrapf(err, "failed to add serverGroup membership for deploymentID:%q, nodeName:%q, instance:%q", deploymentID, nodeName, instanceName)
}

// Get connection info (user, private key)
user, privateKey, err := commons.GetConnInfoFromEndpointCredentials(ctx, kv, deploymentID, nodeName)
if err != nil {
return err
}
return commons.AddConnectionCheckResource(infrastructure, user, privateKey, accessIP, instance.Name, env)
}

Expand Down
192 changes: 192 additions & 0 deletions prov/terraform/openstack/testdata/topology_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
tosca_definitions_version: alien_dsl_2_0_0

metadata:
template_name: Test
template_version: 0.1.0-SNAPSHOT
template_author: ${template_author}

description: ""

imports:
- <yorc-types.yml>
- <normative-types.yml>
- <yorc-openstack-types.yml>

topology_template:
node_templates:
BlockStorage:
type: yorc.nodes.openstack.BlockStorage
properties:
deletable: false
size: "10 GB"
requirements:
- attachToComputeAttach:
type_requirement: attachment
node: Compute
capability: tosca.capabilities.Attachment
relationship: tosca.relationships.AttachTo
Compute:
type: yorc.nodes.openstack.Compute
properties:
image: "a460db41-e574-416f-9634-96f2862f10fe"
flavor: 3
key_pair: yorc
requirements:
- networkNetwork2Connection:
type_requirement: network
node: Network_2
capability: tosca.capabilities.Connectivity
relationship: tosca.relationships.Network
- Compute_FIPCompute:
type_requirement: network
node: FIPCompute
capability: yorc.capabilities.openstack.FIPConnectivity
relationship: tosca.relationships.Network
capabilities:
endpoint:
properties:
credentials:
keys:
0: |
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAuEl5Wjgdvlqbz0x2vcllSQrDiRd+bWdA2MgpOl726ovxw9uE
QJSlXYBJbSCQg+q++OEtXmvfahN5Y9aemuPey/o/S9RWyQ/X+uVeXdNV4Xkgar6b
uYr1n1Ju7ltmdVJME7fr+Ti+2d+EMBs7V+jGXyZzBTdr6wCJuBHHXV/ZKDzw1cHd
bRF8obBmMcxyzNbXnhSUvBgXT+GQ0/CgkNdrTwGOgtckqNYTuw1Rd6wAsF5xgN23
uss5WJOg3/eMW2JMjyxNqaJhBUtA2CKcdnLjwyDxWdmC1NMHKL1umPOjuCyNczpl
axMKW//UZT3WyfVt/gcHGGNIuI0izwFJ6QjlrQIDAQABAoIBAAet8COlUP/8sJ97
1TrlaJYZn7pXw0n10or2FFm9WVa+zC1YOXOjfhyeWvD0OXF1181xPL3BiwbVlupl
KCjWNBOV8wtK5u7r/RkUc9E/HEYQERzBoqWht8iS29KM9oEPE+KCeI/jIHjdypli
mR95sMKITKS8AYBCfnqwKvmmI9t8VIXsrZWsg1dUD9TCa8QxoA66raSpXegDgjox
T8IjZW90BwD6oG/5+HfbuwtjKR1Lca5tMzqxDMvqBf3KdCuee1x2Uuzla9/MsK/4
Nuqv88gpoI7bDJOJnF/KrJqEH1ihF5zNVOs5c7XKmnAdry05tA7CjbiILOeFq3yn
elkdR5UCgYEA3RC0bAR/TjSKGBEzvzfJFNp2ipdlZ1svobHl5sqDJzQp7P9aIogU
qUhw2vr/nHg4dfmLnMHJYh6GCIjN1H7NZzaBiQSUcT+s2GRxYJqRV4geFHvKNzt3
a50Hi5rSsbKm0LvlUA3vGkMABICyzkETPDl2WSFtKWUYrTMZSKixCtsCgYEA1Wjj
fn+3HOmAv3lX4PzhHiBBfBj60BKPCnBbWG6TTF4ya7UEU+e5aAbLD10QdQyx77rL
V3G3Xda1BWA2wGKRDfC9ksFUuxH2egNPGadOVZH2U/a/87YGOFUmbf03jJ6mbeRV
BBBVcB8oGSD+NemiDPqYUi/G1lT+oRLFIkkYhBcCgYEApjKj4j2zVCFt3NA5/j27
gGEKFAHka8MDWWY8uLlxxuyRxKrpoeJ63hYnOorP11wO3qshClYqyAi4rfvj+yjl
1f4FfvShgU7k7L7++ijaslsUekPi8IlVq+MfxBY+5vewMGfC69+97hmHDtuPEj+c
bX+p+TKHNkLaPYSYMqcYi1cCgYEAxf6JSfyt48oT5BFtYdTb+zpL5xm54T/GrBWv
+eylBm5Cc0E/YaUUlBnxXTCnqyD7GQKB04AycoJX8kPgqD8KexeGmlh6BxFUTsEx
KwjZGXTRR/cfAbo4LR17CQKr/e/XUw9LfPi2e868QgwlLdmzujzpAx9GZ+X1U3V5
piSQ9UMCgYBdegnYh2fqU/oGH+d9WahuO1LW9vz8sFEIhRgJyLfA/ypAg6WCgJF2
GtepEYBXL+QZnhudVxi0YPTmNN3+gtHdr+B4dKZ8z7m9NO2nk5AKdf0sYGWHEzhy
PAgZzG5OTZiu+YohUPnC66eFiyS6anLBj0DGNa9VA8j352ecgeNO4A==
-----END RSA PRIVATE KEY-----
user: centos
secure: true
protocol: tcp
network_name: PRIVATE
initiator: source
scalable:
properties:
min_instances: 1
max_instances: 1
default_instances: 1
Network_2:
type: yorc.nodes.openstack.Network
properties:
ip_version: 4
cidr: "10.1.0.0/24"
FIPCompute:
type: yorc.nodes.openstack.FloatingIP
properties:
floating_network_name: "public-net1"
workflows:
install:
steps:
Compute_install:
target: Compute
activities:
- delegate: install
FIPCompute_install:
target: FIPCompute
activities:
- delegate: install
on_success:
- Compute_install
Network_2_install:
target: Network_2
activities:
- delegate: install
on_success:
- Compute_install
BlockStorage_install:
target: BlockStorage
activities:
- delegate: install
on_success:
- Compute_install
uninstall:
steps:
Compute_uninstall:
target: Compute
activities:
- delegate: uninstall
on_success:
- Network_2_uninstall
- FIPCompute_uninstall
- BlockStorage_uninstall
Network_2_uninstall:
target: Network_2
activities:
- delegate: uninstall
FIPCompute_uninstall:
target: FIPCompute
activities:
- delegate: uninstall
BlockStorage_uninstall:
target: BlockStorage
activities:
- delegate: uninstall
start:
steps:
Network_2_start:
target: Network_2
activities:
- delegate: start
on_success:
- Compute_start
Compute_start:
target: Compute
activities:
- delegate: start
on_success:
- BlockStorage_start
FIPCompute_start:
target: FIPCompute
activities:
- delegate: start
on_success:
- Compute_start
BlockStorage_start:
target: BlockStorage
activities:
- delegate: start
stop:
steps:
FIPCompute_stop:
target: FIPCompute
activities:
- delegate: stop
Network_2_stop:
target: Network_2
activities:
- delegate: stop
BlockStorage_stop:
target: BlockStorage
activities:
- delegate: stop
on_success:
- Compute_stop
Compute_stop:
target: Compute
activities:
- delegate: stop
on_success:
- FIPCompute_stop
- Network_2_stop
run:
cancel:
24 changes: 24 additions & 0 deletions tosca/datatypes/credential.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2019 Bull S.A.S. Atos Technologies - Bull, Rue Jean Jaures, B.P.68, 78340, Les Clayes-sous-Bois, France.
//
// 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 datatypes

// Credential is a tosca.datatypes.Credential as defined in the specification https://docs.oasis-open.org/tosca/TOSCA-Simple-Profile-YAML/v1.3/csprd01/TOSCA-Simple-Profile-YAML-v1.3-csprd01.html#TYPE_TOSCA_DATA_CREDENTIAL
type Credential struct {
Protocol string `mapstructure:"protocol"`
TokenType string `mapstructure:"token_type"` // default: password
Token string `mapstructure:"token"`
Keys map[string]string `mapstructure:"keys"`
User string `mapstructure:"user"`
}