Skip to content

Commit fb97c3a

Browse files
Merge pull request #2243 from softlayer/internal-updates
Internal updates
2 parents bd365de + 7a65a89 commit fb97c3a

File tree

6 files changed

+127
-68
lines changed

6 files changed

+127
-68
lines changed

SoftLayer/API.py

Lines changed: 77 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,38 @@
5050
))
5151

5252

53+
def _build_transport(url, proxy, timeout, user_agent, verify):
54+
"""Construct the appropriate transport based on the endpoint URL.
55+
56+
Selects RestTransport when the URL contains '/rest', otherwise falls back
57+
to XmlRpcTransport. Extracted to avoid duplicating this logic across
58+
``create_client_from_env``, ``employee_client``, and ``BaseClient``.
59+
60+
:param str url: The API endpoint URL.
61+
:param str proxy: Optional proxy URL.
62+
:param timeout: Request timeout in seconds (``None`` means no timeout).
63+
:param str user_agent: Optional User-Agent string override.
64+
:param verify: SSL verification — ``True``, ``False``, or a path to a CA bundle.
65+
:returns: A :class:`~SoftLayer.transports.RestTransport` or
66+
:class:`~SoftLayer.transports.XmlRpcTransport` instance.
67+
"""
68+
if url is not None and '/rest' in url:
69+
return transports.RestTransport(
70+
endpoint_url=url,
71+
proxy=proxy,
72+
timeout=timeout,
73+
user_agent=user_agent,
74+
verify=verify,
75+
)
76+
return transports.XmlRpcTransport(
77+
endpoint_url=url,
78+
proxy=proxy,
79+
timeout=timeout,
80+
user_agent=user_agent,
81+
verify=verify,
82+
)
83+
84+
5385
def create_client_from_env(username=None,
5486
api_key=None,
5587
endpoint_url=None,
@@ -62,7 +94,7 @@ def create_client_from_env(username=None,
6294
verify=True):
6395
"""Creates a SoftLayer API client using your environment.
6496
65-
Settings are loaded via keyword arguments, environemtal variables and
97+
Settings are loaded via keyword arguments, environmental variables and
6698
config file.
6799
68100
:param username: an optional API username if you wish to bypass the
@@ -104,25 +136,13 @@ def create_client_from_env(username=None,
104136
config_file=config_file)
105137

106138
if transport is None:
107-
url = settings.get('endpoint_url')
108-
if url is not None and '/rest' in url:
109-
# If this looks like a rest endpoint, use the rest transport
110-
transport = transports.RestTransport(
111-
endpoint_url=settings.get('endpoint_url'),
112-
proxy=settings.get('proxy'),
113-
timeout=settings.get('timeout'),
114-
user_agent=user_agent,
115-
verify=verify,
116-
)
117-
else:
118-
# Default the transport to use XMLRPC
119-
transport = transports.XmlRpcTransport(
120-
endpoint_url=settings.get('endpoint_url'),
121-
proxy=settings.get('proxy'),
122-
timeout=settings.get('timeout'),
123-
user_agent=user_agent,
124-
verify=verify,
125-
)
139+
transport = _build_transport(
140+
url=settings.get('endpoint_url'),
141+
proxy=settings.get('proxy'),
142+
timeout=settings.get('timeout'),
143+
user_agent=user_agent,
144+
verify=verify,
145+
)
126146

127147
# If we have enough information to make an auth driver, let's do it
128148
if auth is None and settings.get('username') and settings.get('api_key'):
@@ -157,13 +177,13 @@ def employee_client(username=None,
157177
verify=True):
158178
"""Creates an INTERNAL SoftLayer API client using your environment.
159179
160-
Settings are loaded via keyword arguments, environemtal variables and config file.
180+
Settings are loaded via keyword arguments, environmental variables and config file.
161181
162182
:param username: your user ID
163-
:param access_token: hash from SoftLayer_User_Employee::performExternalAuthentication(username, password, token)
164-
:param password: password to use for employee authentication
183+
:param access_token: hash from SoftLayer_User_Employee::performExternalAuthentication
165184
:param endpoint_url: the API endpoint base URL you wish to connect to.
166-
Set this to API_PRIVATE_ENDPOINT to connect via SoftLayer's private network.
185+
Must contain 'internal'. Set this to API_PRIVATE_ENDPOINT to connect
186+
via SoftLayer's private network.
167187
:param proxy: proxy to be used to make API calls
168188
:param integer timeout: timeout for API requests
169189
:param auth: an object which responds to get_headers() to be inserted into the xml-rpc headers.
@@ -173,56 +193,54 @@ def employee_client(username=None,
173193
calls if you wish to bypass the packages built in User Agent string
174194
:param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request)
175195
:param bool verify: decide to verify the server's SSL/TLS cert.
196+
DO NOT SET TO FALSE WITHOUT UNDERSTANDING THE IMPLICATIONS.
176197
"""
198+
# Pass caller-supplied verify so it is not silently discarded; the config
199+
# file value will take precedence if present (via get_client_settings).
177200
settings = config.get_client_settings(username=username,
178201
api_key=None,
179202
endpoint_url=endpoint_url,
180203
timeout=timeout,
181204
proxy=proxy,
182-
verify=None,
205+
verify=verify,
183206
config_file=config_file)
184207

185208
url = settings.get('endpoint_url', '')
186-
verify = settings.get('verify', True)
209+
# Honour the config-file value; fall back to the caller-supplied default.
210+
verify = settings.get('verify', verify)
187211

188212
if 'internal' not in url:
189213
raise exceptions.SoftLayerError(f"{url} does not look like an Internal Employee url.")
190214

215+
# url is guaranteed non-empty here (the guard above ensures it contains
216+
# 'internal'), so no additional None-check is needed.
191217
if transport is None:
192-
if url is not None and '/rest' in url:
193-
# If this looks like a rest endpoint, use the rest transport
194-
transport = transports.RestTransport(
195-
endpoint_url=url,
196-
proxy=settings.get('proxy'),
197-
timeout=settings.get('timeout'),
198-
user_agent=user_agent,
199-
verify=verify,
200-
)
201-
else:
202-
# Default the transport to use XMLRPC
203-
transport = transports.XmlRpcTransport(
204-
endpoint_url=url,
205-
proxy=settings.get('proxy'),
206-
timeout=settings.get('timeout'),
207-
user_agent=user_agent,
208-
verify=verify,
209-
)
210-
218+
transport = _build_transport(
219+
url=url,
220+
proxy=settings.get('proxy'),
221+
timeout=settings.get('timeout'),
222+
user_agent=user_agent,
223+
verify=verify,
224+
)
225+
226+
# Resolve all settings-derived credentials together before auth selection.
211227
if access_token is None:
212228
access_token = settings.get('access_token')
213-
214229
user_id = settings.get('userid')
215-
# Assume access_token is valid for now, user has logged in before at least.
216-
if settings.get('auth_cert', False):
217-
auth = slauth.X509Authentication(settings.get('auth_cert'), verify)
218-
return EmployeeClient(auth=auth, transport=transport, config_file=config_file)
219-
elif access_token and user_id:
220-
auth = slauth.EmployeeAuthentication(user_id, access_token)
221-
return EmployeeClient(auth=auth, transport=transport, config_file=config_file)
222-
else:
223-
# This is for logging in mostly.
224-
LOGGER.info("No access_token or userid found in settings, creating a No Auth client for now.")
225-
return EmployeeClient(auth=None, transport=transport, config_file=config_file)
230+
231+
# Select the appropriate auth driver only when the caller has not already
232+
# supplied one. A single return keeps construction separate from selection.
233+
if auth is None:
234+
if settings.get('auth_cert'):
235+
auth = slauth.X509Authentication(settings.get('auth_cert'), verify)
236+
elif access_token and user_id:
237+
auth = slauth.EmployeeAuthentication(user_id, access_token)
238+
else:
239+
# No credentials available — caller must authenticate explicitly
240+
# (e.g. via EmployeeClient.authenticate_with_internal).
241+
LOGGER.info("No access_token or userid found in settings, creating a No Auth client for now.")
242+
243+
return EmployeeClient(auth=auth, transport=transport, config_file=config_file)
226244

227245

228246
def Client(**kwargs):
@@ -751,9 +769,7 @@ def refresh_token(self, userId, auth_token):
751769

752770
def call(self, service, method, *args, **kwargs):
753771
"""Handles refreshing Employee tokens in case of a HTTP 401 error"""
754-
if (service == 'SoftLayer_Account' or service == 'Account') and not kwargs.get('id'):
755-
if not self.account_id:
756-
raise exceptions.SoftLayerError("SoftLayer_Account service requires an ID")
772+
if self.account_id and not kwargs.get('id', False):
757773
kwargs['id'] = self.account_id
758774

759775
try:
@@ -763,6 +779,7 @@ def call(self, service, method, *args, **kwargs):
763779
userId = self.settings['softlayer'].get('userid')
764780
access_token = self.settings['softlayer'].get('access_token')
765781
LOGGER.warning("Token has expired, trying to refresh. %s", ex.faultString)
782+
print("Token has expired, trying to refresh. %s", ex.faultString)
766783
self.refresh_token(userId, access_token)
767784
# Try the Call again this time....
768785
return BaseClient.call(self, service, method, *args, **kwargs)

SoftLayer/CLI/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from SoftLayer.CLI import formatting
2323
from SoftLayer import consts
2424

25-
# pylint: disable=too-many-public-methods, broad-except, unused-argument
25+
# pylint: disable=too-many-public-methods, broad-except, unused-argument, invalid-name
2626
# pylint: disable=redefined-builtin, super-init-not-called, arguments-differ
2727

2828
START_TIME = time.time()

SoftLayer/CLI/login.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,60 @@ def censor_password(value):
1717

1818

1919
@click.command(cls=SLCommand)
20+
@click.option('--session-token',
21+
default=None,
22+
help='An existing employee session token (hash). Click the "Copy Session Token" in the internal portal '
23+
'to get this value.'
24+
'Can also be set via the SLCLI_SESSION_TOKEN environment variable.',
25+
envvar='SLCLI_SESSION_TOKEN')
26+
@click.option('--user-id',
27+
default=None,
28+
type=int,
29+
help='Employee IMS ID. The number in the url when you click your username in the internal portal, '
30+
'under "user information". Can also be set via the SLCLI_USER_ID environment variable. '
31+
'Or read from the configuration file.',
32+
envvar='SLCLI_USER_ID')
33+
@click.option('--legacy',
34+
default=False,
35+
type=bool,
36+
is_flag=True,
37+
help='Login with username, password, yubi key combination. Only valid if ISV is not required. '
38+
'If using ISV, use your session token.')
2039
@environment.pass_env
21-
def cli(env):
40+
def cli(env, session_token, user_id, legacy):
2241
"""Logs you into the internal SoftLayer Network.
2342
2443
username: Set this in either the softlayer config, or SL_USER ENV variable
2544
password: Set this in SL_PASSWORD env variable. You will be prompted for them otherwise.
45+
46+
To log in with an existing session token instead of username/password/2FA:
47+
48+
slcli login --session-token <token> --user-id <id>
49+
50+
Or via environment variables:
51+
52+
SLCLI_SESSION_TOKEN=<token> SLCLI_USER_ID=<id> slcli login
2653
"""
2754
config_settings = config.get_config(config_file=env.config_file)
2855
settings = config_settings['softlayer']
56+
57+
if not user_id:
58+
user_id = int(settings.get('userid', 0)) or int(os.environ.get('SLCLI_USER_ID', 0))
59+
# --session-token supplied on the CLI (or via SLCLI_SESSION_TOKEN env var):
60+
# authenticate directly, persist to config, and return immediately.
61+
if not legacy:
62+
if not user_id:
63+
user_id = int(input("User ID (number): "))
64+
if not session_token:
65+
session_token = os.environ.get('SLCLI_SESSION_TOKEN', '') or input("Session Token: ")
66+
env.client.authenticate_with_hash(user_id, session_token)
67+
settings['access_token'] = session_token
68+
settings['userid'] = str(user_id)
69+
config_settings['softlayer'] = settings
70+
config.write_config(config_settings, env.config_file)
71+
click.echo(f"Logged in with session token for user ID {user_id}.")
72+
return
73+
2974
username = settings.get('username') or os.environ.get('SLCLI_USER', None)
3075
password = os.environ.get('SLCLI_PASSWORD', '')
3176
yubi = None

tests/api_tests.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -389,10 +389,7 @@ def test_expired_token_is_really_expired(self, api_response):
389389
@mock.patch('SoftLayer.API.BaseClient.call')
390390
def test_account_check(self, _call):
391391
self.client.transport = self.mocks
392-
exception = self.assertRaises(
393-
exceptions.SoftLayerError,
394-
self.client.call, "SoftLayer_Account", "getObject")
395-
self.assertEqual(str(exception), "SoftLayer_Account service requires an ID")
392+
396393
self.client.account_id = 1234
397394
self.client.call("SoftLayer_Account", "getObject")
398395
self.client.call("SoftLayer_Account", "getObject1", id=9999)

tools/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
click >= 8.0.4
2+
click == 8.0.4
33
requests >= 2.32.2
44
prompt_toolkit >= 2
55
pygments >= 2.0.0

tools/test-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ pytest
44
pytest-cov
55
mock
66
sphinx
7-
click >= 8.0.4
7+
click == 8.0.4
88
requests >= 2.32.2
99
prompt_toolkit >= 2
1010
pygments >= 2.0.0

0 commit comments

Comments
 (0)