File: setup.py

package info (click to toggle)
python-softlayer 6.1.4-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 5,100 kB
  • sloc: python: 53,771; makefile: 289; sh: 57
file content (281 lines) | stat: -rw-r--r-- 10,658 bytes parent folder | download
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
"""Setup CLI configuration."""
# :license: MIT, see LICENSE for more details.
import webbrowser

import configparser
import json
import os.path
import requests

import click

import SoftLayer
from SoftLayer.CLI.command import SLCommand as SLCommand
from SoftLayer.CLI import config
from SoftLayer.CLI import environment
from SoftLayer.CLI import exceptions
from SoftLayer.CLI import formatting
from SoftLayer.consts import USER_AGENT
from SoftLayer import utils


def get_api_key(client, username, secret):  # pylint: disable=inconsistent-return-statements
    """Attempts API-Key and password auth to get an API key.

    This will also generate an API key if one doesn't exist
    """

    # Try to use a client with username/api key
    if len(secret) == 64 or username == 'apikey':
        try:
            client['Account'].getCurrentUser()
            return secret
        except SoftLayer.SoftLayerAPIError as ex:
            if 'invalid api token' not in ex.faultString.lower():
                raise
    else:
        if isinstance(client, SoftLayer.API.IAMClient):
            client.authenticate_with_iam_token(secret)
        else:
            # Try to use a client with username/password
            client.authenticate_with_password(username, secret)

        user_record = client.call('Account', 'getCurrentUser', mask='id, apiAuthenticationKeys')
        api_keys = user_record['apiAuthenticationKeys']
        if len(api_keys) == 0:
            return client.call('User_Customer', 'addApiAuthenticationKey', id=user_record['id'])
        return api_keys[0]['authenticationKey']


@click.command(cls=SLCommand)
@click.option('-a', '--auth', type=click.Choice(['ibmid', 'cloud_key', 'classic_key', 'sso']),
              help="Select a method of authentication.", default='classic_key', show_default=True)
@environment.pass_env
def cli(env, auth):
    """Setup the ~/.softlayer file with username and apikey.

    [Auth Types]

    ibmid: Requires your cloud.ibm.com username and password, and generates a classic infrastructure API key.

    cloud_key: A 32 character API key. Username will be 'apikey'

    classic_key: A 64 character API key used in the Softlayer/Classic Infrastructure systems.

    sso: For users with @ibm.com email addresses.
    """
    username = None
    api_key = None

    timeout = 0
    defaults = config.get_settings_from_client(env.client, env.theme)
    endpoint_url = get_endpoint_url(env, defaults.get('endpoint_url', 'public'))
    # Get ths username and API key
    if auth == 'ibmid':
        username, api_key = ibmid_login(env)

    elif auth == 'cloud_key':
        username = 'apikey'
        secret = env.getpass('Classic Infrastructue API Key', default=defaults['api_key'])
        new_client = SoftLayer.Client(username=username, api_key=secret, endpoint_url=endpoint_url, timeout=timeout)
        api_key = get_api_key(new_client, username, secret)

    elif auth == 'sso':
        username, api_key = sso_login(env)

    else:
        username = env.input('Classic Infrastructue Username', default=defaults['username'])
        secret = env.getpass('Classic Infrastructue API Key', default=defaults['api_key'])
        new_client = SoftLayer.Client(username=username, api_key=secret, endpoint_url=endpoint_url, timeout=timeout)
        api_key = get_api_key(new_client, username, secret)

    # Ask for timeout, convert to float, then to int
    timeout = int(float(env.input('Timeout', default=defaults['timeout'] or 0)))

    # Ask theme for console
    theme = env.input('Theme (dark/light)', default=defaults['theme'] or 'dark')

    path = '~/.softlayer'
    if env.config_file:
        path = env.config_file
    config_path = os.path.expanduser(path)

    env.out(env.fmt(config.config_table({'username': username,
                                         'api_key': api_key,
                                         'endpoint_url': endpoint_url,
                                         'timeout': timeout,
                                         'theme': theme})))

    if not formatting.confirm('Are you sure you want to write settings to "%s"?' % config_path, default=True):
        raise exceptions.CLIAbort('Aborted.')

    # Persist the config file. Read the target config file in before
    # setting the values to avoid clobbering settings
    parsed_config = configparser.RawConfigParser()
    parsed_config.read(config_path)
    try:
        parsed_config.add_section('softlayer')
    except configparser.DuplicateSectionError:
        pass

    parsed_config.set('softlayer', 'username', username)
    parsed_config.set('softlayer', 'api_key', api_key)
    parsed_config.set('softlayer', 'endpoint_url', endpoint_url)
    parsed_config.set('softlayer', 'timeout', timeout)
    parsed_config.set('softlayer', 'theme', theme)

    config_fd = os.fdopen(os.open(config_path, (os.O_WRONLY | os.O_CREAT | os.O_TRUNC), 0o600), 'w')
    try:
        parsed_config.write(config_fd)
    finally:
        config_fd.close()

    env.fout("Configuration Updated Successfully")


def get_endpoint_url(env, endpoint='public'):
    """Gets the Endpoint to use."""

    endpoint_type = env.input('Endpoint (public|private|custom)', default=endpoint)
    endpoint_type = endpoint_type.lower()

    if endpoint_type == 'public':
        endpoint_url = SoftLayer.API_PUBLIC_ENDPOINT
    elif endpoint_type == 'private':
        endpoint_url = SoftLayer.API_PRIVATE_ENDPOINT
    else:
        if endpoint_type == 'custom':
            endpoint_url = env.input('Endpoint URL', default=endpoint)
        else:
            endpoint_url = endpoint_type
    return endpoint_url


def ibmid_login(env):
    """Uses an IBMid and Password to get an access token, and that access token to get an API key"""
    email = env.input("Email").strip()
    password = env.getpass("Password").strip()

    client = SoftLayer.API.IAMClient(config_file=env.config_file)

    # STEP 1: Get the base IAM Token with a username/password
    tokens = client.authenticate_with_password(email, password)

    # STEP 2: Figure out which account we want to use
    account = get_accounts(env, tokens['access_token'])

    # STEP 3: Refresh Token, using a specific account this time.
    tokens = client.refresh_iam_token(tokens['refresh_token'], account['account_id'], account['ims_id'])

    # STEP 4: Get or create the Classic Infrastructure API key
    user = client.call('SoftLayer_Account', 'getCurrentUser', mask="mask[id,username,apiAuthenticationKeys]")

    if len(user.get('apiAuthenticationKeys', [])) == 0:
        env.fout("Creating a Classic Infrastrucutre API key for {}".format(user['username']))
        api_key = client.call('User_Customer', 'addApiAuthenticationKey', id=user['id'])
    else:
        api_key = user['apiAuthenticationKeys'][0]['authenticationKey']

    return user.get('username'), api_key


def get_accounts(env, a_token):
    """Gets account list from accounts.cloud.ibm.com/v1/accounts"""
    iam_client = requests.Session()

    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        'User-Agent': USER_AGENT,
        'Accept': 'application/json'
    }
    headers['Authorization'] = 'Bearer {}'.format(a_token)
    response = iam_client.request(
        'GET',
        'https://accounts.cloud.ibm.com/v1/accounts',
        headers=headers
    )

    response.raise_for_status()

    accounts = json.loads(response.text)
    selected = None
    ims_id = None
    if accounts.get('total_results', 0) == 1:
        selected = accounts['resources'][0]
    else:
        env.fout("Select an Account...")
        counter = 1
        for selected in accounts.get('resources', []):
            links = utils.lookup(selected, 'metadata', 'linked_accounts') or []
            for link in links:
                if link.get('origin') == "IMS":
                    ims_id = link.get('id')
            if ims_id is None:
                ims_id = "Unlinked"
            env.fout("{}: {} ({})".format(counter, utils.lookup(selected, 'entity', 'name'), ims_id))
            counter = counter + 1
        ims_id = None  # Reset ims_id to avoid any mix-match or something.
        choice = click.prompt('Enter a number', type=int)
        # Test to make sure choice is not out of bounds...
        selected = accounts['resources'][choice - 1]

    account_id = utils.lookup(selected, 'metadata', 'guid')
    links = utils.lookup(selected, 'metadata', 'linked_accounts') or []
    for link in links:
        if link.get('origin') == "IMS":
            ims_id = link.get('id')

    print("Using account {}".format(utils.lookup(selected, 'entity', 'name')))
    return {"account_id": account_id, "ims_id": ims_id}


def get_sso_url():
    """Gets the URL for using SSO Tokens"""

    iam_client = requests.Session()

    headers = {
        'Content-Type': 'application/json',
        'User-Agent': USER_AGENT,
        'Accept': 'application/json'
    }
    response = iam_client.request(
        'GET',
        'https://iam.cloud.ibm.com/identity/.well-known/openid-configuration',
        headers=headers
    )

    response.raise_for_status()
    data = json.loads(response.text)
    return data.get('passcode_endpoint')


def sso_login(env):
    """Uses a SSO token to get a SL apikey"""
    passcode_url = get_sso_url()
    env.fout("Get a one-time code from {} to proceed.".format(passcode_url))
    open_browser = env.input("Open the URL in the default browser? [Y/n]", default='Y')
    if open_browser.lower() == 'y':
        webbrowser.open(passcode_url)
    passcode = env.input("One-time code")
    client = SoftLayer.API.IAMClient(config_file=env.config_file)

    # STEP 1: Get the base IAM Token with a username/password
    tokens = client.authenticate_with_passcode(passcode)

    # STEP 2: Figure out which account we want to use
    account = get_accounts(env, tokens['access_token'])

    # STEP 3: Refresh Token, using a specific account this time.
    tokens = client.refresh_iam_token(tokens['refresh_token'], account['account_id'], account['ims_id'])

    # STEP 4: Get or create the Classic Infrastructure API key
    # client.authenticate_with_iam_token(tokens['access_token'])
    user = client.call('SoftLayer_Account', 'getCurrentUser', mask="mask[id,username,apiAuthenticationKeys]")

    if len(user.get('apiAuthenticationKeys', [])) == 0:
        env.fout("Creating a Classic Infrastrucutre API key for {}".format(user['username']))
        api_key = client.call('User_Customer', 'addApiAuthenticationKey', id=user['id'])
    else:
        api_key = user['apiAuthenticationKeys'][0]['authenticationKey']
    return user.get('username'), api_key