Skip to content

Commit 07ee664

Browse files
Merge pull request #978 from allmightyspiff/issues827
User Management
2 parents 77b299f + cf4c884 commit 07ee664

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1486
-33
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ dist/*
1414
*.egg-info
1515
.cache
1616
.idea
17+
.pytest_cache/*
18+
slcli

SoftLayer/API.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ class Service(object):
333333
:param name str: The service name
334334
335335
"""
336+
336337
def __init__(self, client, name):
337338
self.client = client
338339
self.name = name

SoftLayer/CLI/columns.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
class Column(object):
1616
"""Column desctribes an attribute and how to fetch/display it."""
17+
1718
def __init__(self, name, path, mask=None):
1819
self.name = name
1920
self.path = path
@@ -26,6 +27,7 @@ def __init__(self, name, path, mask=None):
2627

2728
class ColumnFormatter(object):
2829
"""Maps each column using a function"""
30+
2931
def __init__(self):
3032
self.columns = []
3133
self.column_funcs = []

SoftLayer/CLI/core.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,17 +115,22 @@ def cli(env,
115115
**kwargs):
116116
"""Main click CLI entry-point."""
117117

118-
logger = logging.getLogger()
119-
logger.addHandler(logging.StreamHandler())
120-
logger.setLevel(DEBUG_LOGGING_MAP.get(verbose, logging.DEBUG))
121-
122118
# Populate environement with client and set it as the context object
123119
env.skip_confirmations = really
124120
env.config_file = config
125121
env.format = format
126122
env.ensure_client(config_file=config, is_demo=demo, proxy=proxy)
127-
128123
env.vars['_start'] = time.time()
124+
logger = logging.getLogger()
125+
126+
if demo is False:
127+
logger.addHandler(logging.StreamHandler())
128+
else:
129+
# This section is for running CLI tests.
130+
logging.getLogger("urllib3").setLevel(logging.WARNING)
131+
logger.addHandler(logging.NullHandler())
132+
133+
logger.setLevel(DEBUG_LOGGING_MAP.get(verbose, logging.DEBUG))
129134
env.vars['_timings'] = SoftLayer.DebugTransport(env.client.transport)
130135
env.client.transport = env.vars['_timings']
131136

SoftLayer/CLI/exceptions.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
# pylint: disable=keyword-arg-before-vararg
1111
class CLIHalt(SystemExit):
1212
"""Smoothly halt the execution of the command. No error."""
13+
1314
def __init__(self, code=0, *args):
1415
super(CLIHalt, self).__init__(*args)
1516
self.code = code
@@ -23,13 +24,15 @@ def __str__(self):
2324

2425
class CLIAbort(CLIHalt):
2526
"""Halt the execution of the command. Gives an exit code of 2."""
27+
2628
def __init__(self, msg, *args):
2729
super(CLIAbort, self).__init__(code=2, *args)
2830
self.message = msg
2931

3032

3133
class ArgumentError(CLIAbort):
3234
"""Halt the execution of the command because of invalid arguments."""
35+
3336
def __init__(self, msg, *args):
3437
super(ArgumentError, self).__init__(msg, *args)
3538
self.message = "Argument Error: %s" % msg

SoftLayer/CLI/file/snapshot/schedule_list.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def cli(env, volume_id):
6363
file_schedule_type,
6464
replication,
6565
schedule.get('createDate', '')
66-
]
66+
]
6767
table_row.extend(schedule_properties)
6868
table.add_row(table_row)
6969

SoftLayer/CLI/formatting.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
"""
22
SoftLayer.formatting
33
~~~~~~~~~~~~~~~~~~~~
4-
Provider classes and helper functions to display output onto a
5-
command-line.
4+
Provider classes and helper functions to display output onto a command-line.
65
7-
:license: MIT, see LICENSE for more details.
86
"""
97
# pylint: disable=E0202, consider-merging-isinstance, arguments-differ, keyword-arg-before-vararg
108
import collections
119
import json
1210
import os
1311

1412
import click
15-
import prettytable
13+
14+
# If both PTable and prettytable are installed, its impossible to use the new version
15+
try:
16+
from prettytable import prettytable
17+
except ImportError:
18+
import prettytable
1619

1720
from SoftLayer.CLI import exceptions
1821
from SoftLayer import utils
@@ -229,6 +232,7 @@ class SequentialOutput(list):
229232
230233
:param separator str: string to use as a default separator
231234
"""
235+
232236
def __init__(self, separator=os.linesep, *args, **kwargs):
233237
self.separator = separator
234238
super(SequentialOutput, self).__init__(*args, **kwargs)
@@ -243,6 +247,7 @@ def __str__(self):
243247

244248
class CLIJSONEncoder(json.JSONEncoder):
245249
"""A JSON encoder which is able to use a .to_python() method on objects."""
250+
246251
def default(self, obj):
247252
"""Encode object if it implements to_python()."""
248253
if hasattr(obj, 'to_python'):
@@ -255,7 +260,8 @@ class Table(object):
255260
256261
:param list columns: a list of column names
257262
"""
258-
def __init__(self, columns):
263+
264+
def __init__(self, columns, title=None):
259265
duplicated_cols = [col for col, count
260266
in collections.Counter(columns).items()
261267
if count > 1]
@@ -267,6 +273,7 @@ def __init__(self, columns):
267273
self.rows = []
268274
self.align = {}
269275
self.sortby = None
276+
self.title = title
270277

271278
def add_row(self, row):
272279
"""Add a row to the table.
@@ -287,6 +294,7 @@ def to_python(self):
287294
def prettytable(self):
288295
"""Returns a new prettytable instance."""
289296
table = prettytable.PrettyTable(self.columns)
297+
290298
if self.sortby:
291299
if self.sortby in self.columns:
292300
table.sortby = self.sortby
@@ -296,6 +304,8 @@ def prettytable(self):
296304
for a_col, alignment in self.align.items():
297305
table.align[a_col] = alignment
298306

307+
if self.title:
308+
table.title = self.title
299309
# Adding rows
300310
for row in self.rows:
301311
table.add_row(row)
@@ -304,6 +314,7 @@ def prettytable(self):
304314

305315
class KeyValueTable(Table):
306316
"""A table that is oriented towards key-value pairs."""
317+
307318
def to_python(self):
308319
"""Decode this KeyValueTable object to standard Python types."""
309320
mapping = {}
@@ -318,6 +329,7 @@ class FormattedItem(object):
318329
:param original: raw (machine-readable) value
319330
:param string formatted: human-readable value
320331
"""
332+
321333
def __init__(self, original, formatted=None):
322334
self.original = original
323335
if formatted is not None:

SoftLayer/CLI/routes.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,14 @@
282282
('ticket:attach', 'SoftLayer.CLI.ticket.attach:cli'),
283283
('ticket:detach', 'SoftLayer.CLI.ticket.detach:cli'),
284284

285+
('user', 'SoftLayer.CLI.user'),
286+
('user:list', 'SoftLayer.CLI.user.list:cli'),
287+
('user:detail', 'SoftLayer.CLI.user.detail:cli'),
288+
('user:permissions', 'SoftLayer.CLI.user.permissions:cli'),
289+
('user:edit-permissions', 'SoftLayer.CLI.user.edit_permissions:cli'),
290+
('user:edit-details', 'SoftLayer.CLI.user.edit_details:cli'),
291+
('user:create', 'SoftLayer.CLI.user.create:cli'),
292+
285293
('vlan', 'SoftLayer.CLI.vlan'),
286294
('vlan:detail', 'SoftLayer.CLI.vlan.detail:cli'),
287295
('vlan:list', 'SoftLayer.CLI.vlan.list:cli'),

SoftLayer/CLI/user/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Manage Users."""

SoftLayer/CLI/user/create.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Creates a user """
2+
# :license: MIT, see LICENSE for more details.
3+
4+
import json
5+
import string
6+
import sys
7+
8+
import click
9+
10+
import SoftLayer
11+
from SoftLayer.CLI import environment
12+
from SoftLayer.CLI import exceptions
13+
from SoftLayer.CLI import formatting
14+
from SoftLayer.CLI import helpers
15+
16+
17+
@click.command()
18+
@click.argument('username')
19+
@click.option('--email', '-e', required=True,
20+
help="Email address for this user. Required for creation.")
21+
@click.option('--password', '-p', default=None, show_default=True,
22+
help="Password to set for this user. If no password is provided, user will be sent an email "
23+
"to generate one, which expires in 24 hours. '-p generate' will create a password for you "
24+
"(Requires Python 3.6+). Passwords require 8+ characters, upper and lowercase, a number "
25+
"and a symbol.")
26+
@click.option('--from-user', '-u', default=None,
27+
help="Base user to use as a template for creating this user. "
28+
"Will default to the user running this command. Information provided in --template "
29+
"supersedes this template.")
30+
@click.option('--template', '-t', default=None,
31+
help="A json string describing https://softlayer.github.io/reference/datatypes/SoftLayer_User_Customer/")
32+
@click.option('--api-key', '-a', default=False, is_flag=True, help="Create an API key for this user.")
33+
@environment.pass_env
34+
def cli(env, username, email, password, from_user, template, api_key):
35+
"""Creates a user Users.
36+
37+
:Example: slcli user create my@email.com -e my@email.com -p generate -a
38+
-t '{"firstName": "Test", "lastName": "Testerson"}'
39+
40+
Remember to set the permissions and access for this new user.
41+
"""
42+
43+
mgr = SoftLayer.UserManager(env.client)
44+
user_mask = ("mask[id, firstName, lastName, email, companyName, address1, city, country, postalCode, "
45+
"state, userStatusId, timezoneId]")
46+
from_user_id = None
47+
if from_user is None:
48+
user_template = mgr.get_current_user(objectmask=user_mask)
49+
from_user_id = user_template['id']
50+
else:
51+
from_user_id = helpers.resolve_id(mgr.resolve_ids, from_user, 'username')
52+
user_template = mgr.get_user(from_user_id, objectmask=user_mask)
53+
# If we send the ID back to the API, an exception will be thrown
54+
del user_template['id']
55+
56+
if template is not None:
57+
try:
58+
template_object = json.loads(template)
59+
for key in template_object:
60+
user_template[key] = template_object[key]
61+
except ValueError as ex:
62+
raise exceptions.ArgumentError("Unable to parse --template. %s" % ex)
63+
64+
user_template['username'] = username
65+
if password == 'generate':
66+
password = generate_password()
67+
68+
user_template['email'] = email
69+
70+
if not env.skip_confirmations:
71+
table = formatting.KeyValueTable(['name', 'value'])
72+
for key in user_template:
73+
table.add_row([key, user_template[key]])
74+
table.add_row(['password', password])
75+
click.secho("You are about to create the following user...", fg='green')
76+
env.fout(table)
77+
if not formatting.confirm("Do you wish to continue?"):
78+
raise exceptions.CLIAbort("Canceling creation!")
79+
80+
result = mgr.create_user(user_template, password)
81+
new_api_key = None
82+
if api_key:
83+
click.secho("Adding API key...", fg='green')
84+
new_api_key = mgr.add_api_authentication_key(result['id'])
85+
86+
table = formatting.Table(['Username', 'Email', 'Password', 'API Key'])
87+
table.add_row([result['username'], result['email'], password, new_api_key])
88+
env.fout(table)
89+
90+
91+
def generate_password():
92+
"""Returns a 23 character random string, with 3 special characters at the end"""
93+
if sys.version_info > (3, 6):
94+
import secrets # pylint: disable=import-error
95+
alphabet = string.ascii_letters + string.digits
96+
password = ''.join(secrets.choice(alphabet) for i in range(20))
97+
special = ''.join(secrets.choice(string.punctuation) for i in range(3))
98+
return password + special
99+
else:
100+
raise ImportError("Generating passwords require python 3.6 or higher")

0 commit comments

Comments
 (0)