Skip to content

Commit fead52b

Browse files
merged in master
2 parents 4aec4fc + 72ba37d commit fead52b

34 files changed

+971
-404
lines changed

.secrets.baseline

+11-12
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "^.secrets.baseline$",
44
"lines": null
55
},
6-
"generated_at": "2023-09-27T14:21:34Z",
6+
"generated_at": "2023-10-13T20:28:05Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -238,7 +238,7 @@
238238
"hashed_secret": "fb5f2f1b65d1f2bc130ce9d5729b38d12f2b444e",
239239
"is_secret": false,
240240
"is_verified": false,
241-
"line_number": 259,
241+
"line_number": 274,
242242
"type": "Secret Keyword",
243243
"verified_result": null
244244
}
@@ -529,23 +529,22 @@
529529
"verified_result": null
530530
}
531531
],
532-
"tests/CLI/modules/securitygroup_tests.py": [
532+
"tests/CLI/modules/hardware/hardware_basic_tests.py": [
533533
{
534-
"hashed_secret": "bc553d847e40dd6f3f63638f16f57b28ce1425cc",
535-
"is_secret": false,
534+
"hashed_secret": "6367c48dd193d56ea7b0baad25b19455e529f5ee",
536535
"is_verified": false,
537-
"line_number": 339,
538-
"type": "Hex High Entropy String",
536+
"line_number": 57,
537+
"type": "Secret Keyword",
539538
"verified_result": null
540539
}
541540
],
542-
"tests/CLI/modules/server_tests.py": [
541+
"tests/CLI/modules/securitygroup_tests.py": [
543542
{
544-
"hashed_secret": "6367c48dd193d56ea7b0baad25b19455e529f5ee",
543+
"hashed_secret": "bc553d847e40dd6f3f63638f16f57b28ce1425cc",
545544
"is_secret": false,
546545
"is_verified": false,
547-
"line_number": 57,
548-
"type": "Secret Keyword",
546+
"line_number": 339,
547+
"type": "Hex High Entropy String",
549548
"verified_result": null
550549
}
551550
],
@@ -594,7 +593,7 @@
594593
"hashed_secret": "fb5f2f1b65d1f2bc130ce9d5729b38d12f2b444e",
595594
"is_secret": false,
596595
"is_verified": false,
597-
"line_number": 737,
596+
"line_number": 673,
598597
"type": "Secret Keyword",
599598
"verified_result": null
600599
}

SoftLayer/API.py

+38-8
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
import time
1010
import warnings
1111

12+
import concurrent.futures as cf
1213
import json
1314
import logging
15+
import math
1416
import requests
1517

16-
1718
from SoftLayer import auth as slauth
1819
from SoftLayer import config
1920
from SoftLayer import consts
@@ -289,13 +290,6 @@ def call(self, service, method, *args, **kwargs):
289290
request.verify = kwargs.get('verify')
290291

291292
if self.auth:
292-
extra_headers = self.auth.get_headers()
293-
if extra_headers:
294-
warnings.warn("auth.get_headers() is deprecated and will be "
295-
"removed in the next major version",
296-
DeprecationWarning)
297-
request.headers.update(extra_headers)
298-
299293
request = self.auth.get_request(request)
300294

301295
request.headers.update(kwargs.get('headers', {}))
@@ -352,6 +346,42 @@ def iter_call(self, service, method, *args, **kwargs):
352346

353347
offset += limit
354348

349+
def cf_call(self, service, method, *args, **kwargs):
350+
"""Uses threads to iterate through API calls.
351+
352+
:param service: the name of the SoftLayer API service
353+
:param method: the method to call on the service
354+
:param integer limit: result size for each API call (defaults to 100)
355+
:param \\*args: same optional arguments that ``Service.call`` takes
356+
:param \\*\\*kwargs: same optional keyword arguments that ``Service.call`` takes
357+
"""
358+
limit = kwargs.pop('limit', 100)
359+
offset = kwargs.pop('offset', 0)
360+
361+
if limit <= 0:
362+
raise AttributeError("Limit size should be greater than zero.")
363+
# This initial API call is to determine how many API calls we need to make after this first one.
364+
first_call = self.call(service, method, offset=offset, limit=limit, *args, **kwargs)
365+
366+
# This was not a list result, just return it.
367+
if not isinstance(first_call, transports.SoftLayerListResult):
368+
return first_call
369+
# How many more API calls we have to make
370+
api_calls = math.ceil((first_call.total_count - limit) / limit)
371+
372+
def this_api(offset):
373+
"""Used to easily call executor.map() on this fuction"""
374+
return self.call(service, method, offset=offset, limit=limit, *args, **kwargs)
375+
376+
with cf.ThreadPoolExecutor(max_workers=10) as executor:
377+
future_results = {}
378+
offset_map = [x * limit for x in range(1, api_calls)]
379+
future_results = list(executor.map(this_api, offset_map))
380+
# Append the results in the order they were called
381+
for call_result in future_results:
382+
first_call = first_call + call_result
383+
return first_call
384+
355385
def __repr__(self):
356386
return "Client(transport=%r, auth=%r)" % (self.transport, self.auth)
357387

SoftLayer/CLI/command.py

+17-6
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class OptionHighlighter(RegexHighlighter):
3030
r"(?P<option_choices>Choices: )",
3131
r"(?P<example_block>Example::)",
3232
r"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)"
33+
r"(?P<args_keyword>^[A-Z]+$)",
3334
]
3435

3536

@@ -179,6 +180,8 @@ def format_usage(self, ctx: click.Context, formatter: click.formatting.HelpForma
179180
pieces[index] = "[options][OPTIONS][/]"
180181
elif piece == "COMMAND [ARGS]...":
181182
pieces[index] = "[command]COMMAND[/] [args][ARGS][/] ..."
183+
else:
184+
pieces[index] = f"[args_keyword]{piece}[/]"
182185

183186
self.console.print(f"Usage: [path]{ctx.command_path}[/] {' '.join(pieces)}")
184187

@@ -205,16 +208,23 @@ def format_epilog(self, ctx: click.Context, formatter: click.formatting.HelpForm
205208
def format_options(self, ctx, formatter):
206209
"""Prints out the options in a table format"""
207210

208-
# NEXT support binary options --yes/--no
209-
# NEXT SUPPORT color for IDENTIFIER and such
210211
options_table = Table(highlight=True, box=box.SQUARE, show_header=False)
211212

212213
for param in self.get_params(ctx):
214+
# useful for showing whats in a param
215+
# print(param.to_info_dict())
216+
217+
# Set Arguments to all uppercase
218+
if param.param_type_name == 'argument':
219+
param.opts[0] = param.opts[0].upper()
220+
221+
# This option has a short (-v) and long (--verbose) options
213222
if len(param.opts) == 2:
214223
opt1 = self.highlighter(param.opts[1])
215224
opt2 = self.highlighter(param.opts[0])
216225
else:
217226
opt2 = self.highlighter(param.opts[0])
227+
# Needs to be the Text() type because rich.Text doesn't mesh with string
218228
opt1 = Text("")
219229

220230
# Ensures the short option is always in opt1.
@@ -224,19 +234,20 @@ def format_options(self, ctx, formatter):
224234
if param.metavar:
225235
opt2 += Text(f" {param.metavar}", style="bold yellow")
226236

227-
options = Text(" ".join(reversed(param.opts)))
237+
# secondary_opts are usually for flags --enable/--disable
238+
if len(param.secondary_opts) == 1:
239+
opt2 += Text("|") + self.highlighter(param.secondary_opts[0])
240+
228241
help_record = param.get_help_record(ctx)
229242
help_message = ""
230243
if help_record:
231-
help_message = param.get_help_record(ctx)[-1]
244+
help_message = Text(param.get_help_record(ctx)[-1])
232245

233246
# Add Click choices to help message
234247
if isinstance(param.type, click.Choice):
235248
choices = ", ".join(param.type.choices)
236249
help_message += f" Choices: {choices}"
237250

238-
if param.metavar:
239-
options += f" {param.metavar}"
240251
options_table.add_row(opt1, opt2, self.highlighter(help_message))
241252

242253
self.console.print(options_table)

SoftLayer/CLI/formatting.py

+20
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,22 @@ def __init__(self, columns, title=None, align=None):
318318
self.align = align or {}
319319
self.sortby = None
320320
self.title = title
321+
# Used to print a message if the table is empty
322+
self.empty_message = None
323+
324+
def __bool__(self):
325+
"""Useful for seeing if the table has any rows"""
326+
return len(self.rows) > 0
327+
328+
def set_empty_message(self, message):
329+
"""Sets the empty message for this table for env.fout
330+
331+
Set this message if you want to print a message instead of a table to the user
332+
but still want the json output to print an empty list `[]`
333+
334+
:param message str: Message to print if the table has no rows
335+
"""
336+
self.empty_message = message
321337

322338
def add_row(self, row):
323339
"""Add a row to the table.
@@ -337,6 +353,10 @@ def to_python(self):
337353

338354
def prettytable(self, fmt='table', theme=None):
339355
"""Returns a RICH table instance."""
356+
357+
# Used to print a message instead of a bad looking empty table
358+
if not self and self.empty_message:
359+
return self.empty_message
340360
box_style = box.SQUARE
341361
if fmt == 'raw':
342362
box_style = None

SoftLayer/CLI/hardware/detail.py

+25-6
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ def cli(env, identifier, passwords, price, components):
2929
table.align['value'] = 'l'
3030

3131
hardware_id = helpers.resolve_id(hardware.resolve_ids, identifier, 'hardware')
32-
result = hardware.get_hardware(hardware_id)
32+
result = hardware.get_hardware_fast(hardware_id)
3333
result = utils.NestedDict(result)
3434
hard_drives = hardware.get_hard_drives(hardware_id)
3535

3636
operating_system = utils.lookup(result, 'operatingSystem', 'softwareLicense', 'softwareDescription') or {}
3737
memory = formatting.gb(result.get('memoryCapacity', 0))
3838
owner = None
39-
if utils.lookup(result, 'billingItem') != []:
39+
if utils.lookup(result, 'billingItem'):
4040
owner = utils.lookup(result, 'billingItem', 'orderItem', 'order', 'userRecord', 'username')
4141

4242
table_hard_drives = formatting.Table(['Name', 'Capacity', 'Serial #'])
@@ -72,11 +72,30 @@ def cli(env, identifier, passwords, price, components):
7272
table.add_row(['last_transaction', last_transaction])
7373
table.add_row(['billing', 'Hourly' if result['hourlyBillingFlag'] else 'Monthly'])
7474

75-
vlan_table = formatting.Table(['type', 'number', 'id', 'name', 'netmask'])
75+
vlan_table = formatting.Table(['Network', 'Number', 'Id', 'Name', 'Type'])
7676
for vlan in result['networkVlans']:
77-
vlan_table.add_row([vlan['networkSpace'], vlan['vlanNumber'],
78-
vlan['id'], vlan['fullyQualifiedName'],
79-
vlan['primarySubnets'][0]['netmask']])
77+
vlan_table.add_row([
78+
vlan.get('networkSpace'),
79+
vlan.get('vlanNumber'),
80+
vlan['id'],
81+
vlan['fullyQualifiedName'],
82+
'Primary'
83+
])
84+
85+
# Shows any VLANS trunked/tagged on this server
86+
for component in result.get('networkComponents', []):
87+
# These are the Primary network components
88+
if component.get('primaryIpAddress', False):
89+
uplink = component.get('uplinkComponent', {})
90+
for trunk in uplink.get('networkVlanTrunks', []):
91+
trunk_vlan = trunk.get('networkVlan')
92+
vlan_table.add_row([
93+
trunk_vlan.get('networkSpace'),
94+
trunk_vlan.get('vlanNumber'),
95+
trunk_vlan.get('id'),
96+
trunk_vlan.get('fullyQualifiedName'),
97+
'Trunked'
98+
])
8099

81100
table.add_row(['vlans', vlan_table])
82101

SoftLayer/CLI/hardware/vlan_add.py

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Trunk a VLAN to this server."""
2+
# :license: MIT, see LICENSE for more details.
3+
4+
import click
5+
6+
import SoftLayer
7+
from SoftLayer.CLI import environment
8+
from SoftLayer.CLI import exceptions
9+
from SoftLayer.CLI import helpers
10+
11+
12+
@click.command(cls=SoftLayer.CLI.command.SLCommand, )
13+
@click.argument('hardware', nargs=1)
14+
@click.argument('vlans', nargs=-1)
15+
@environment.pass_env
16+
def cli(env, hardware, vlans):
17+
"""Trunk a VLAN to this server.
18+
19+
HARDWARE is the id of the server
20+
VLANS is the ID, name, or number of the VLANs you want to add. Multiple vlans can be added at the same time.
21+
It is recommended to use the vlan ID, especially if you have multiple vlans with the same name/number.
22+
"""
23+
24+
if not vlans:
25+
raise exceptions.ArgumentError("Error: Missing argument 'VLANS'.")
26+
h_mgr = SoftLayer.HardwareManager(env.client)
27+
n_mgr = SoftLayer.NetworkManager(env.client)
28+
hw_id = helpers.resolve_id(h_mgr.resolve_ids, hardware, 'hardware')
29+
# Enclosing in quotes is required for any input that has a space in it.
30+
# "Public DAL10" for example needs to be sent to search as \"Public DAL10\"
31+
sl_vlans = n_mgr.search_for_vlan(" ".join(f"\"{v}\"" for v in vlans))
32+
if not sl_vlans:
33+
raise exceptions.ArgumentError(f"No vlans found matching {' '.join(vlans)}")
34+
add_vlans = parse_vlans(sl_vlans)
35+
component_mask = "mask[id, name, port, macAddress, primaryIpAddress]"
36+
# NEXT: Add nice output / exception handling
37+
if len(add_vlans['public']) > 0:
38+
components = h_mgr.get_network_components(hw_id, mask=component_mask, space='public')
39+
for c in components:
40+
if c.get('primaryIpAddress'):
41+
h_mgr.trunk_vlan(c.get('id'), add_vlans['public'])
42+
if len(add_vlans['private']) > 0:
43+
components = h_mgr.get_network_components(hw_id, mask=component_mask, space='private')
44+
for c in components:
45+
if c.get('primaryIpAddress'):
46+
h_mgr.trunk_vlan(c.get('id'), add_vlans['private'])
47+
48+
49+
def parse_vlans(vlans):
50+
"""returns a dictionary mapping for public / private vlans"""
51+
52+
pub_vlan = []
53+
pri_vlan = []
54+
for vlan in vlans:
55+
print(f"{vlan.get('networkSpace')} | {vlan.get('id')} -> {vlan.get('vlanNumber')}")
56+
if vlan.get('networkSpace') == "PUBLIC":
57+
pub_vlan.append(vlan)
58+
else:
59+
pri_vlan.append(vlan)
60+
return {"public": pub_vlan, "private": pri_vlan}

0 commit comments

Comments
 (0)