Skip to content

Commit b5aeeb2

Browse files
authored
Merge branch 'develop' into master
2 parents 99fb85a + 271c3ce commit b5aeeb2

17 files changed

Lines changed: 275 additions & 32 deletions
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
cloudshell-shell-core>=2.2.0,<2.3.0
2-
cloudshell-cm-ansible>=1.0.0,<1.1.0
2+
cloudshell-cm-ansible>=1.1.0,<1.2.0

drivers/ansible_shellPackage/DataModel/datamodel.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
<Rule Name="Execution Server Selector" />
1212
</Rules>
1313
</AttributeInfo>
14+
<AttributeInfo Name="Timeout Minutes" Type="Numeric" DefaultValue="0" Description="Maximum number of minutes to connect to the target machine." IsReadOnly="false">
15+
</AttributeInfo>
1416
</Attributes>
1517
<ResourceFamilies>
1618
<ResourceFamily Name="Configuration Services" Description="" IsAdminOnly="true" IsService="true" ServiceType="Regular">
@@ -31,11 +33,13 @@
3133
<AttachedAttribute Name="Supports Ansible" IsOverridable="true" IsLocal="true">
3234
<AllowedValues />
3335
</AttachedAttribute>
36+
<AttachedAttribute Name="Timeout Minutes" IsLocal="true" IsOverridable="true"/>
3437
</AttachedAttributes>
3538
<AttributeValues>
3639
<AttributeValue Name="Ansible Additional Arguments" Value="" />
3740
<AttributeValue Name="Supports Ansible" Value="True" />
3841
<AttributeValue Name="Execution Server Selector" Value="" />
42+
<AttributeValue Name="Timeout Minutes" Value="20" />
3943
</AttributeValues>
4044
<ParentModels />
4145
<Drivers>

drivers/version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.0.0
1+
1.1.0

package/cloudshell/cm/ansible/ansible_shell.py

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import os
22

3+
from cloudshell.cm.ansible.domain.Helpers.ansible_connection_helper import AnsibleConnectionHelper
34
from cloudshell.cm.ansible.domain.cancellation_sampler import CancellationSampler
5+
from cloudshell.cm.ansible.domain.connection_service import ConnectionService
46
from cloudshell.cm.ansible.domain.exceptions import AnsibleException
57
from cloudshell.cm.ansible.domain.ansible_command_executor import AnsibleCommandExecutor, ReservationOutputWriter
68
from cloudshell.cm.ansible.domain.ansible_config_file import AnsibleConfigFile
@@ -30,12 +32,16 @@ def __init__(self, file_system=None, playbook_downloader=None, playbook_executor
3032
:type playbook_executor: AnsibleCommandExecutor
3133
:type session_provider: CloudShellSessionProvider
3234
"""
35+
3336
http_request_service = http_request_service or HttpRequestService()
3437
zip_service = zip_service or ZipService()
3538
self.file_system = file_system or FileSystemService()
3639
filename_extractor = FilenameExtractor()
37-
self.downloader = playbook_downloader or PlaybookDownloader(self.file_system, zip_service, http_request_service, filename_extractor)
40+
self.downloader = playbook_downloader or PlaybookDownloader(self.file_system, zip_service, http_request_service,
41+
filename_extractor)
3842
self.executor = playbook_executor or AnsibleCommandExecutor()
43+
self.connection_service = ConnectionService()
44+
self.ansible_connection_helper = AnsibleConnectionHelper()
3945

4046
def execute_playbook(self, command_context, ansi_conf_json, cancellation_context):
4147
"""
@@ -56,6 +62,7 @@ def execute_playbook(self, command_context, ansi_conf_json, cancellation_context
5662
with TempFolderScope(self.file_system, logger):
5763
self._add_ansible_config_file(logger)
5864
self._add_host_vars_files(ansi_conf, logger)
65+
self._wait_for_all_hosts_to_be_deployed(ansi_conf, logger, output_writer)
5966
self._add_inventory_file(ansi_conf, logger)
6067
playbook_name = self._download_playbook(ansi_conf, cancellation_sampler, logger)
6168
self._run_playbook(ansi_conf, playbook_name, output_writer, cancellation_sampler, logger)
@@ -87,12 +94,13 @@ def _add_host_vars_files(self, ansi_conf, logger):
8794
with HostVarsFile(self.file_system, host_conf.ip, logger) as file:
8895
file.add_vars(host_conf.parameters)
8996
file.add_connection_type(host_conf.connection_method)
90-
if host_conf.connection_method == 'winrm':
91-
if host_conf.connection_secured == True:
92-
file.add_port('5986')
97+
ansible_port = self.ansible_connection_helper.get_ansible_port(host_conf)
98+
file.add_port(ansible_port)
99+
100+
if host_conf.connection_method == AnsibleConnectionHelper.CONNECTION_METHOD_WIN_RM:
101+
if host_conf.connection_secured:
93102
file.add_ignore_winrm_cert_validation()
94-
else:
95-
file.add_port('5985')
103+
96104
file.add_username(host_conf.username)
97105
if host_conf.password:
98106
file.add_password(host_conf.password)
@@ -130,4 +138,37 @@ def _run_playbook(self, ansi_conf, playbook_name, output_writer, cancellation_sa
130138
ansible_result = AnsibleResult(output, error, [h.ip for h in ansi_conf.hosts_conf])
131139

132140
if not ansible_result.success:
133-
raise AnsibleException(ansible_result.to_json())
141+
raise AnsibleException(ansible_result.to_json())
142+
143+
def _wait_for_all_hosts_to_be_deployed(self, ansi_conf, logger, output_writer):
144+
"""
145+
146+
:param cloudshell.cm.ansible.domain.ansible_configurationa.AnsibleConfiguration ansi_conf:
147+
:param Logger logger:
148+
:param domain.ansible_command_executor.ReservationOutputWriter output_writer:
149+
:return:
150+
"""
151+
wait_for_deploy_msg = "Waiting for all hosts to deploy"
152+
153+
logger.info(wait_for_deploy_msg)
154+
output_writer.write(wait_for_deploy_msg)
155+
for host in ansi_conf.hosts_conf:
156+
157+
logger.info("Trying to connect to host:" + host.ip)
158+
ansible_port = self.ansible_connection_helper.get_ansible_port(host)
159+
160+
if HostVarsFile.ANSIBLE_PORT in host.parameters.keys() and (
161+
host.parameters[HostVarsFile.ANSIBLE_PORT] != '' and
162+
host.parameters[HostVarsFile.ANSIBLE_PORT] is not None):
163+
ansible_port = host.parameters[HostVarsFile.ANSIBLE_PORT]
164+
165+
port_ansible_port = "Ansible Timeout:" + str(ansi_conf.timeout_minutes) + " Ansible port : " + ansible_port
166+
167+
logger.info(port_ansible_port)
168+
output_writer.write("Waiting for host :" + host.ip)
169+
output_writer.write(port_ansible_port)
170+
171+
self.connection_service.check_connection(logger, host, ansible_port=ansible_port,
172+
timeout_minutes=ansi_conf.timeout_minutes)
173+
174+
output_writer.write("Communication check completed.")

package/cloudshell/cm/ansible/domain/Helpers/__init__.py

Whitespace-only changes.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
class AnsibleConnectionHelper(object):
2+
WIN_RM_SECURED_PORT = '5986'
3+
WIN_RM_PORT = '5985'
4+
CONNECTION_METHOD_WIN_RM = 'winrm'
5+
CONNECTION_METHOD_SSH = 'ssh'
6+
SSH_PORT = '22'
7+
8+
def __init__(self):
9+
pass
10+
11+
def get_ansible_port(self, host):
12+
ansible_port = None
13+
if host.connection_method == self.CONNECTION_METHOD_WIN_RM:
14+
if host.connection_secured:
15+
ansible_port = self.WIN_RM_SECURED_PORT
16+
else:
17+
ansible_port = self.WIN_RM_PORT
18+
if host.connection_method == self.CONNECTION_METHOD_SSH:
19+
ansible_port = self.SSH_PORT
20+
return ansible_port

package/cloudshell/cm/ansible/domain/ansible_command_executor.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
from logging import Logger
55
from cloudshell.api.cloudshell_api import CloudShellAPISession
6+
import re
67

78
from cloudshell.cm.ansible.domain.cancellation_sampler import CancellationSampler
89
from cloudshell.cm.ansible.domain.output.unixToHtmlConverter import UnixToHtmlColorConverter
@@ -35,8 +36,8 @@ def execute_playbook(self, playbook_file, inventory_file, args, output_writer, l
3536

3637
with StdoutAccumulator(process.stdout) as stdout:
3738
with StderrAccumulator(process.stderr) as stderr:
39+
txt_lines = []
3840
while True:
39-
txt_lines = []
4041
txt_err = stderr.read_all_txt()
4142
txt_out = stdout.read_all_txt()
4243
if txt_err:
@@ -45,21 +46,24 @@ def execute_playbook(self, playbook_file, inventory_file, args, output_writer, l
4546
if txt_out:
4647
all_txt_out += txt_out
4748
txt_lines.append(txt_out)
48-
if txt_lines:
49-
txt_html = UnixToHtmlColorConverter().convert(os.linesep.join(txt_lines))
50-
try:
51-
output_writer.write(txt_html)
52-
except Exception as e:
53-
output_writer.write('failed to write text of %s characters (%s)'%(len(txt_html),e))
54-
logger.debug("failed to write:" + txt_html)
55-
logger.debug("failed to write.")
5649
if process.poll() is not None:
5750
break
5851
if cancel_sampler.is_cancelled():
5952
process.kill()
6053
cancel_sampler.throw()
6154
time.sleep(2)
6255

56+
converter = UnixToHtmlColorConverter()
57+
try:
58+
full_output = converter.convert(os.linesep.join(txt_lines))
59+
full_output = converter.remove_strike(full_output)
60+
output_writer.write(full_output)
61+
logger.error(full_output)
62+
except Exception as e:
63+
output_writer.write('failed to write text of %s characters (%s)' % (len(full_output), e))
64+
logger.debug("failed to write:" + full_output)
65+
logger.debug("failed to write.")
66+
6367
elapsed = time.time() - start_time
6468
err_line_count = len(all_txt_err.split(os.linesep))
6569
out_line_count = len(all_txt_out.split(os.linesep))

package/cloudshell/cm/ansible/domain/ansible_configuration.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55

66
class AnsibleConfiguration(object):
7-
def __init__(self, playbook_repo=None, hosts_conf=None, additional_cmd_args=None):
7+
def __init__(self, playbook_repo=None, hosts_conf=None, additional_cmd_args=None, timeout_minutes = None):
88
"""
99
:type playbook_repo: PlaybookRepository
1010
:type hosts_conf: list[HostConfiguration]
1111
:type additional_cmd_args: str
12+
:type timeout_minutes: float
1213
"""
14+
self.timeout_minutes = timeout_minutes or 0.0
1315
self.playbook_repo = playbook_repo or PlaybookRepository()
1416
self.hosts_conf = hosts_conf or []
1517
self.additional_cmd_args = additional_cmd_args
@@ -53,6 +55,7 @@ def json_to_object(self, json_str):
5355

5456
ansi_conf = AnsibleConfiguration()
5557
ansi_conf.additional_cmd_args = json_obj.get('additionalArgs')
58+
ansi_conf.timeout_minutes = json_obj.get('timeoutMinutes', 0.0)
5659

5760
if json_obj.get('repositoryDetails'):
5861
ansi_conf.playbook_repo.url = json_obj['repositoryDetails'].get('url')
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import re
2+
import socket
3+
from StringIO import StringIO
4+
from abc import ABCMeta, abstractmethod
5+
from multiprocessing.pool import ThreadPool
6+
from uuid import uuid4
7+
8+
import time
9+
10+
import requests
11+
import winrm
12+
from paramiko.ssh_exception import NoValidConnectionsError
13+
from winrm.exceptions import WinRMTransportError
14+
15+
from pip._vendor.requests import ConnectionError
16+
from paramiko import SSHClient, AutoAddPolicy, RSAKey
17+
18+
19+
class IVMConnectionService(object):
20+
__metaclass__ = ABCMeta
21+
22+
@abstractmethod
23+
def check_connection(self, target_host, logger, ansible_port):
24+
pass
25+
26+
27+
class ExcutorConnectionError(EnvironmentError):
28+
def __init__(self, error_code, inner_error):
29+
self.errno = error_code
30+
self.inner_error = inner_error
31+
32+
33+
class WindowsConnectionService(IVMConnectionService):
34+
def check_connection(self, target_host, logger, ansible_port):
35+
36+
ip = target_host.ip + ":" + ansible_port if ansible_port else target_host.ip
37+
logger.info("Session IP: " + ip)
38+
39+
logger.info("Creating a session.")
40+
if target_host.connection_secured:
41+
logger.info("session connection_secured=True")
42+
session = winrm.Session(ip, auth=(target_host.username, target_host.password), transport='ssl')
43+
else:
44+
logger.info("session connection_secured=False")
45+
session = winrm.Session(ip, auth=(target_host.username, target_host.password))
46+
47+
logger.info("Session created.")
48+
49+
try:
50+
logger.info("test connection")
51+
uid = str(uuid4())
52+
result = session.run_cmd('@echo ' + uid)
53+
assert uid in result.std_out
54+
except requests.ConnectionError as e:
55+
# Time out is allowed exception
56+
raise ExcutorConnectionError(10060, e)
57+
except ConnectionError as e:
58+
match = re.search(r'\[Errno (?P<errno>\d+)\]', str(e.message))
59+
error_code = int(match.group('errno')) if match else 0
60+
raise ExcutorConnectionError(error_code, e)
61+
except WinRMTransportError as e:
62+
match = re.search(r'Code (?P<errno>\d+)', str(e.message))
63+
error_code = int(match.group('errno')) if match else 0
64+
raise ExcutorConnectionError(error_code, e)
65+
except Exception as e:
66+
raise ExcutorConnectionError(0, e)
67+
68+
69+
class LinuxConnectionService(IVMConnectionService):
70+
def check_connection(self, target_host, logger, ansible_port):
71+
"""
72+
73+
:param target_host:
74+
:param logger Logger:
75+
:return:
76+
"""
77+
try:
78+
logger.info("Creating a session.")
79+
80+
session = SSHClient()
81+
session.set_missing_host_key_policy(AutoAddPolicy())
82+
logger.info("Test connection")
83+
if target_host.password:
84+
session.connect(target_host.ip, port=ansible_port, username=target_host.username,
85+
password=target_host.password)
86+
elif target_host.access_key:
87+
key_stream = StringIO(target_host.access_key)
88+
key_obj = RSAKey.from_private_key(key_stream)
89+
session.connect(target_host.ip, port=ansible_port, username=target_host.username, pkey=key_obj)
90+
else:
91+
raise Exception('Both password and access key are empty.')
92+
logger.info("Done testing connection")
93+
except NoValidConnectionsError as e:
94+
error_code = next(e.errors.itervalues(), type('e', (object,), {'errno': 0})).errno
95+
raise ExcutorConnectionError(error_code, e)
96+
except socket.error as e:
97+
raise ExcutorConnectionError(e.errno, e)
98+
except Exception as e:
99+
raise ExcutorConnectionError(0, e)
100+
101+
102+
class ConnectionService(object):
103+
def __init__(self):
104+
self.valid_errnos = [10060, 10061, 10064, 10065, 500, 113, 111, 110]
105+
self.linuxConnectionService = LinuxConnectionService()
106+
self.windowsConnectionService = WindowsConnectionService()
107+
108+
def check_connection(self, logger, target_host, ansible_port=None, timeout_minutes=10):
109+
"""
110+
111+
:param timeout_minutes:
112+
:param ansible_port:
113+
:param Logger logger:
114+
:param cloudshell.cm.ansible.domain.ansible_configuration.HostConfiguration target_host:
115+
:return:
116+
"""
117+
# 10060 ETIMEDOUT Operation timed out
118+
# 10061 ECONNREFUSED Connection refused (happense when host found, port not)
119+
# 10064 EHOSTDOWN Host is down
120+
# 10065 EHOSTUNREACH Host is unreachable
121+
# 500 Bad http response (winrm)
122+
# 113 EHOSTUNREACH No route to host (winrm - OpenStack)
123+
# 111 ERROR_SSH_APPLICATION_CLOSED User on the other side of connection closed
124+
# application that led to disconnection
125+
# 110 ERROR_SSH_CONNECTION_LOST Connection was lost by some reason
126+
interval_seconds = 10
127+
start_time = time.time()
128+
while True:
129+
try:
130+
logger.info("check connection")
131+
if target_host.connection_method == 'winrm':
132+
logger.info("Check connection on windows")
133+
134+
self.windowsConnectionService.check_connection(target_host=target_host,
135+
logger=logger,
136+
ansible_port=ansible_port)
137+
138+
logger.info("Done checking connection on windows")
139+
else:
140+
logger.info("Check connection on linux")
141+
self.linuxConnectionService.check_connection(target_host=target_host,
142+
logger=logger,
143+
ansible_port=int(ansible_port))
144+
logger.info("Done checking connection on linux")
145+
break
146+
except ExcutorConnectionError as e:
147+
if e.errno not in self.valid_errnos:
148+
raise e.inner_error
149+
if time.time() - start_time >= timeout_minutes * 60:
150+
raise e.inner_error
151+
time.sleep(interval_seconds)

package/cloudshell/cm/ansible/domain/host_vars_file.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ def add_password(self, password):
5454
self.vars[HostVarsFile.ANSIBLE_PASSWORD] = password
5555

5656
def add_port(self, port):
57-
self.vars[HostVarsFile.ANSIBLE_PORT] = port
57+
if HostVarsFile.ANSIBLE_PORT not in self.vars.keys() or \
58+
(self.vars[HostVarsFile.ANSIBLE_PORT] == '') or \
59+
self.vars[HostVarsFile.ANSIBLE_PORT] is None:
60+
self.vars[HostVarsFile.ANSIBLE_PORT] = port
5861

5962
def add_ignore_winrm_cert_validation(self):
6063
self.vars[HostVarsFile.ANSIBLE_WINRM_CERT_VALIDATION] = 'ignore'

0 commit comments

Comments
 (0)