Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 18567ee

Browse files
#2104 added 'hardware vlan-remove' command
1 parent c0ab2cc commit 18567ee

File tree

9 files changed

+203
-15
lines changed

9 files changed

+203
-15
lines changed

SoftLayer/CLI/formatting.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,23 @@ 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
337+
321338

322339
def add_row(self, row):
323340
"""Add a row to the table.
@@ -337,6 +354,10 @@ def to_python(self):
337354

338355
def prettytable(self, fmt='table', theme=None):
339356
"""Returns a RICH table instance."""
357+
358+
# Used to print a message instead of a bad looking empty table
359+
if not self and self.empty_message:
360+
return self.empty_message
340361
box_style = box.SQUARE
341362
if fmt == 'raw':
342363
box_style = None

SoftLayer/CLI/hardware/vlan_add.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ def cli(env, hardware, vlans):
2121
It is recommended to use the vlan ID, especially if you have multiple vlans with the same name/number.
2222
"""
2323

24+
if not vlans:
25+
raise exceptions.ArgumentError("Error: Missing argument 'VLANS'.")
2426
h_mgr = SoftLayer.HardwareManager(env.client)
2527
n_mgr = SoftLayer.NetworkManager(env.client)
2628
hw_id = helpers.resolve_id(h_mgr.resolve_ids, hardware, 'hardware')
@@ -31,7 +33,7 @@ def cli(env, hardware, vlans):
3133
raise exceptions.ArgumentError(f"No vlans found matching {' '.join(vlans)}")
3234
add_vlans = parse_vlans(sl_vlans)
3335
component_mask = "mask[id, name, port, macAddress, primaryIpAddress]"
34-
# TODO: Add nice output / exception handling
36+
# NEXT: Add nice output / exception handling
3537
if len(add_vlans['public']) > 0:
3638
components = h_mgr.get_network_components(hw_id, mask=component_mask, space='public')
3739
for c in components:

SoftLayer/CLI/hardware/vlan_remove.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Remove VLANs trunked 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+
@click.option('--all', 'all_vlans', is_flag=True, default=False, help="Remove ALL trunked vlans from this server.")
16+
@environment.pass_env
17+
def cli(env, hardware, vlans, all_vlans):
18+
"""Remove VLANs trunked to this server.
19+
20+
HARDWARE is the id of the server
21+
VLANS is the ID, name, or number of the VLANs you want to remove. Multiple vlans can be removed at the same time.
22+
It is recommended to use the vlan ID, especially if you have multiple vlans with the same name/number.
23+
"""
24+
if not vlans and not all_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+
30+
if all_vlans:
31+
h_mgr.clear_vlan(hw_id)
32+
env.fout("Done.")
33+
return
34+
35+
# Enclosing in quotes is required for any input that has a space in it.
36+
# "Public DAL10" for example needs to be sent to search as \"Public DAL10\"
37+
sl_vlans = n_mgr.search_for_vlan(" ".join(f"\"{v}\"" for v in vlans))
38+
if not sl_vlans:
39+
raise exceptions.ArgumentError(f"No vlans found matching {' '.join(vlans)}")
40+
del_vlans = parse_vlans(sl_vlans)
41+
component_mask = "mask[id, name, port, macAddress, primaryIpAddress]"
42+
# NEXT: Add nice output / exception handling
43+
if len(del_vlans['public']) > 0:
44+
components = h_mgr.get_network_components(hw_id, mask=component_mask, space='public')
45+
for c in components:
46+
if c.get('primaryIpAddress'):
47+
h_mgr.remove_vlan(c.get('id'), del_vlans['public'])
48+
if len(del_vlans['private']) > 0:
49+
components = h_mgr.get_network_components(hw_id, mask=component_mask, space='private')
50+
for c in components:
51+
if c.get('primaryIpAddress'):
52+
h_mgr.remove_vlan(c.get('id'), del_vlans['private'])
53+
54+
55+
def parse_vlans(vlans):
56+
"""returns a dictionary mapping for public / private vlans"""
57+
58+
pub_vlan = []
59+
pri_vlan = []
60+
for vlan in vlans:
61+
if vlan.get('networkSpace') == "PUBLIC":
62+
pub_vlan.append(vlan)
63+
else:
64+
pri_vlan.append(vlan)
65+
return {"public": pub_vlan, "private": pri_vlan}

SoftLayer/CLI/hardware/vlan_list.py renamed to SoftLayer/CLI/hardware/vlan_trunkable.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from SoftLayer.CLI import formatting
99
from SoftLayer.CLI import helpers
1010

11-
11+
from pprint import pprint as pp
1212
@click.command(cls=SoftLayer.CLI.command.SLCommand, )
1313
@click.argument('hardware')
1414
@environment.pass_env
@@ -21,14 +21,15 @@ def cli(env, hardware):
2121
"mask[id,primaryIpAddress,"
2222
"networkVlansTrunkable[id,name,vlanNumber,fullyQualifiedName,networkSpace]]"
2323
)
24-
table = formatting.Table([
25-
"ID", "VLAN", "Name", "Space"
26-
])
24+
table = formatting.Table(["ID", "VLAN", "Name", "Space"])
25+
table.set_empty_message("No trunkable vlans found.")
2726
hw_components = env.client.call('SoftLayer_Hardware_Server', 'getNetworkComponents', id=hw_id, mask=mask)
27+
2828
for component in hw_components:
2929
if component.get('primaryIpAddress'):
3030
for vlan in component.get('networkVlansTrunkable', []):
3131
table.add_row([
3232
vlan.get('id'), vlan.get('fullyQualifiedName'), vlan.get('name'), vlan.get('networkSpace')
3333
])
34+
3435
env.fout(table)

SoftLayer/CLI/routes.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,8 +315,9 @@
315315
('hardware:notification-add', 'SoftLayer.CLI.hardware.notification_add:cli'),
316316
('hardware:notification-delete', 'SoftLayer.CLI.hardware.notification_delete:cli'),
317317
('hardware:create-credential', 'SoftLayer.CLI.hardware.create_credential:cli'),
318-
('hardware:vlan-list', 'SoftLayer.CLI.hardware.vlan_list:cli'),
318+
('hardware:vlan-trunkable', 'SoftLayer.CLI.hardware.vlan_trunkable:cli'),
319319
('hardware:vlan-add', 'SoftLayer.CLI.hardware.vlan_add:cli'),
320+
('hardware:vlan-remove', 'SoftLayer.CLI.hardware.vlan_remove:cli'),
320321

321322
('securitygroup', 'SoftLayer.CLI.securitygroup'),
322323
('securitygroup:list', 'SoftLayer.CLI.securitygroup.list:cli'),

SoftLayer/managers/hardware.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,6 +1200,34 @@ def trunk_vlan(self, component_id, vlans):
12001200
"""
12011201
return self.client.call('SoftLayer_Network_Component', 'addNetworkVlanTrunks', vlans, id=component_id)
12021202

1203+
def remove_vlan(self, component_id, vlans):
1204+
"""Calls SoftLayer_Network_Component::removeNetworkVlanTrunks()
1205+
1206+
:param int component_id: SoftLayer_Network_Component id
1207+
:param list vlans: list of SoftLayer_Network_Vlan objects to remove. Each object needs at least id or vlanNumber
1208+
"""
1209+
return self.client.call('SoftLayer_Network_Component', 'removeNetworkVlanTrunks', vlans, id=component_id)
1210+
def clear_vlan(self, hardware_id):
1211+
"""Clears all vlan trunks from a hardware_id
1212+
1213+
:param int hardware_id: server to clear vlans from
1214+
"""
1215+
component_mask = (
1216+
"mask[id, "
1217+
"backendNetworkComponents[id,networkVlanTrunks[networkVlanId]], "
1218+
"frontendNetworkComponents[id,networkVlanTrunks[networkVlanId]]"
1219+
"]"
1220+
)
1221+
components = self.client.call('SoftLayer_Hardware_Server', 'getObject', id=hardware_id, mask=component_mask)
1222+
back_component_id = utils.lookup(components, 'backendNetworkComponent', 'id')
1223+
front_component_id = utils.lookup(components, 'frontendNetworkComponent', 'id')
1224+
for c in components.get('backendNetworkComponent', []):
1225+
if len(c.get('networkVlanTrunks')):
1226+
self.client.call('SoftLayer_Network_Component', 'clearNetworkVlanTrunks', id=c.get('id'))
1227+
for c in components.get('frontendNetworkComponent', []):
1228+
if len(c.get('networkVlanTrunks')):
1229+
self.client.call('SoftLayer_Network_Component', 'clearNetworkVlanTrunks', id=c.get('id'))
1230+
12031231
def get_sensors(self, hardware_id):
12041232
"""Returns Hardware sensor data"""
12051233
return self.client.call('Hardware', 'getSensorData', id=hardware_id)

docs/cli/hardware.rst

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,13 @@ This function updates the firmware of a server. If already at the latest version
149149
:show-nested:
150150

151151
.. click:: SoftLayer.CLI.hardware.vlan_add:cli
152-
:prog: hardware vlan_add
152+
:prog: hardware vlan-add
153153
:show-nested:
154154

155-
.. click:: SoftLayer.CLI.hardware.vlan_list:cli
156-
:prog: hardware vlan_list
155+
.. click:: SoftLayer.CLI.hardware.vlan_remove:cli
156+
:prog: hardware vlan-remove
157+
:show-nested:
158+
159+
.. click:: SoftLayer.CLI.hardware.vlan_trunkable:cli
160+
:prog: hardware vlan-trunkable
157161
:show-nested:

tests/CLI/formatting_table_tests.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""
2+
SoftLayer.tests.CLI.formatting_table_tests
3+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4+
5+
:license: MIT, see LICENSE for more details.
6+
"""
7+
import json
8+
import os
9+
import sys
10+
import tempfile
11+
12+
import click
13+
from rich.table import Table
14+
from unittest import mock as mock
15+
16+
from SoftLayer.CLI import core
17+
from SoftLayer.CLI import exceptions
18+
from SoftLayer.CLI import formatting
19+
from SoftLayer.CLI import helpers
20+
from SoftLayer.CLI import template
21+
from SoftLayer import testing
22+
23+
class TestTable(testing.TestCase):
24+
25+
def test_table_with_duplicated_columns(self):
26+
self.assertRaises(exceptions.CLIHalt, formatting.Table, ['col', 'col'])
27+
28+
def test_boolean_table(self):
29+
table = formatting.Table(["column1"], title="Test Title")
30+
self.assertFalse(table)
31+
table.add_row(["entry1"])
32+
self.assertTrue(table)
33+
34+
35+
class IterToTableTests(testing.TestCase):
36+
37+
def test_format_api_dict(self):
38+
result = formatting._format_dict({'key': 'value'})
39+
40+
self.assertIsInstance(result, formatting.Table)
41+
self.assertEqual(result.columns, ['name', 'value'])
42+
self.assertEqual(result.rows, [['key', 'value']])
43+
44+
def test_format_api_list(self):
45+
result = formatting._format_list([{'key': 'value'}])
46+
47+
self.assertIsInstance(result, formatting.Table)
48+
self.assertEqual(result.columns, ['key'])
49+
self.assertEqual(result.rows, [['value']])
50+
51+
def test_format_api_list_non_objects(self):
52+
result = formatting._format_list(['a', 'b', 'c'])
53+
54+
self.assertIsInstance(result, formatting.Table)
55+
self.assertEqual(result.columns, ['value'])
56+
self.assertEqual(result.rows, [['a'], ['b'], ['c']])
57+
58+
def test_format_api_list_with_none_value(self):
59+
result = formatting._format_list([{'key': [None, 'value']}, None])
60+
61+
self.assertIsInstance(result, formatting.Table)
62+
self.assertEqual(result.columns, ['key'])
63+
64+
def test_format_api_list_with_empty_array(self):
65+
result = formatting.iter_to_table([{'id': 130224450, 'activeTickets': []}])
66+
self.assertIsInstance(result, formatting.Table)
67+
self.assertIn('id', result.columns)
68+
self.assertIn('activeTickets', result.columns)
69+
formatted = formatting.format_output(result, "table")
70+
# No good ways to test whats actually in a Rich.Table without going through the hassel of
71+
# printing it out. As long as this didn't throw and exception it should be fine.
72+
self.assertEqual(formatted.row_count, 1)

tests/CLI/helper_tests.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -255,12 +255,6 @@ def test_resolve_id_multiple(self):
255255
exceptions.CLIAbort, helpers.resolve_id, lambda r: [12345, 54321], 'test')
256256

257257

258-
class TestTable(testing.TestCase):
259-
260-
def test_table_with_duplicated_columns(self):
261-
self.assertRaises(exceptions.CLIHalt, formatting.Table, ['col', 'col'])
262-
263-
264258
class TestFormatOutput(testing.TestCase):
265259

266260
def test_format_output_string(self):

0 commit comments

Comments
 (0)