forked from pulumi/examples
-
Notifications
You must be signed in to change notification settings - Fork 0
/
provisioners.py
170 lines (153 loc) · 6.15 KB
/
provisioners.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# Copyright 2020, Pulumi Corporation. All rights reserved.
import abc
import json
import io
import paramiko
import pulumi
from pulumi import dynamic
import sys
import time
from typing import Any, Optional
from typing_extensions import TypedDict
from uuid import uuid4
# ConnectionArgs tells a provisioner how to access a remote resource. It includes the hostname
# and optional port (default is 22), username, password, and private key information.
class ConnectionArgs(TypedDict):
host: pulumi.Input[str]
"""The host to SSH into."""
port: Optional[pulumi.Input[int]] = None
"""The port to SSH into (default 22)."""
username: Optional[pulumi.Input[str]] = None
"""The username for the SSH login."""
password: Optional[pulumi.Input[str]] = None
"""The optional password for the SSH login (private key is recommended instead)."""
private_key: Optional[pulumi.Input[str]] = None
"""The private key, as an ASCII string, to use for the SSH connection."""
private_key_passphrase: Optional[pulumi.Input[str]] = None
"""The private key passphrase, if any, to use for the SSH private key."""
def connect(conn: ConnectionArgs) -> paramiko.SSHClient:
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
skey = io.StringIO(conn['private_key'])
pkey = paramiko.RSAKey.from_private_key(skey, password=conn.get('private_key_passphrase'))
# Retry the connection until the endpoint is available (up to 2 minutes).
retries = 0
# print(conn['host'])
while True:
try:
ssh.connect(
hostname=conn['host'],
port=conn.get('port') or 22,
username=conn.get('username'),
password=conn.get('password'),
pkey=pkey,
)
return ssh
except paramiko.ssh_exception.NoValidConnectionsError:
if retries == 24:
raise
time.sleep(5)
retries = retries + 1
pass
class ProvisionerProvider(dynamic.ResourceProvider):
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def on_create(self, inputs: Any) -> Any:
return
def create(self, inputs):
outputs = self.on_create(inputs)
return dynamic.CreateResult(id_=uuid4().hex, outs=outputs)
def diff(self, _id, olds, news):
# If anything changed in the inputs, replace the resource.
diffs = []
for key in olds:
if key not in news:
diffs.append(key)
else:
olds_value = json.dumps(olds[key], sort_keys=True, indent=2)
news_value = json.dumps(news[key], sort_keys=True, indent=2)
if olds_value != news_value:
diffs.append(key)
for key in news:
if key not in olds:
diffs.append(key)
return dynamic.DiffResult(changes=len(diffs) > 0, replaces=diffs, delete_before_replace=True)
# CopyFileProvider implements the resource lifecycle for the CopyFile resource type below.
class CopyFileProvider(ProvisionerProvider):
def on_create(self, inputs: Any) -> Any:
ssh = connect(inputs['conn'])
scp = ssh.open_sftp()
try:
scp.put(inputs['src'], inputs['dest'])
finally:
scp.close()
ssh.close()
return inputs
# CopyFile is a provisioner step that can copy a file over an SSH connection.
class CopyFile(dynamic.Resource):
def __init__(self, name: str, conn: pulumi.Input[ConnectionArgs],
src: str, dest: str, opts: Optional[pulumi.ResourceOptions] = None):
self.conn = conn
"""conn contains information on how to connect to the destination, in addition to dependency information."""
self.src = src
"""
src is the source of the file or directory to copy. It can be specified as relative to the current
working directory or as an absolute path. This cannot be specified if content is set.
"""
self.dest = dest
"""dest is required and specifies the absolute path on the target where the file will be copied to."""
super().__init__(
CopyFileProvider(),
name,
{
'dep': conn,
'conn': conn,
'src': src,
'dest': dest,
},
opts,
)
# RunCommandResult is the result of running a command.
class RunCommandResult(TypedDict):
stdout: str
"""The stdout of the command that was executed."""
stderr: str
"""The stderr of the command that was executed."""
# RemoteExecProvider implements the resource lifecycle for the RemoteExec resource type below.
class RemoteExecProvider(ProvisionerProvider):
def on_create(self, inputs: Any) -> Any:
ssh = connect(inputs['conn'])
try:
results = []
for command in inputs['commands']:
stdin, stdout, stderr = ssh.exec_command(command)
results.append({
'stdout': ''.join(stdout.readlines()),
'stderr': ''.join(stderr.readlines()),
})
inputs['results'] = results
print(f'results: {results}')
finally:
ssh.close()
return inputs
# RemoteExec runs remote one or more commands over an SSH connection. It returns the resulting
# stdout and stderr from the commands in the results property.
class RemoteExec(dynamic.Resource):
results: pulumi.Output[list]
def __init__(self, name: str, conn: ConnectionArgs, commands: list, opts: Optional[pulumi.ResourceOptions] = None):
self.conn = conn
"""conn contains information on how to connect to the destination, in addition to dependency information."""
self.commands = commands
"""The commands to execute. Exactly one of 'command' and 'commands' is required."""
self.results = []
"""The resulting command outputs."""
super().__init__(
RemoteExecProvider(),
name,
{
'conn': conn,
'commands': commands,
'results': None,
},
opts,
)