twc-cli 2.3.0__py3-none-any.whl → 2.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of twc-cli might be problematic. Click here for more details.
- CHANGELOG.md +24 -0
- twc/__main__.py +11 -1
- twc/__version__.py +1 -1
- twc/api/base.py +3 -2
- twc/api/client.py +349 -6
- twc/api/types.py +28 -2
- twc/commands/__init__.py +3 -0
- twc/commands/account.py +30 -40
- twc/commands/balancer.py +38 -36
- twc/commands/common.py +8 -4
- twc/commands/config.py +28 -1
- twc/commands/domain.py +519 -0
- twc/commands/firewall.py +654 -0
- twc/commands/image.py +14 -25
- twc/commands/project.py +2 -2
- twc/commands/server.py +16 -5
- twc/commands/vpc.py +293 -0
- twc/fmt.py +15 -0
- twc/typerx.py +7 -2
- twc/vars.py +3 -2
- {twc_cli-2.3.0.dist-info → twc_cli-2.4.1.dist-info}/METADATA +1 -1
- twc_cli-2.4.1.dist-info/RECORD +35 -0
- twc_cli-2.3.0.dist-info/RECORD +0 -32
- {twc_cli-2.3.0.dist-info → twc_cli-2.4.1.dist-info}/COPYING +0 -0
- {twc_cli-2.3.0.dist-info → twc_cli-2.4.1.dist-info}/WHEEL +0 -0
- {twc_cli-2.3.0.dist-info → twc_cli-2.4.1.dist-info}/entry_points.txt +0 -0
twc/commands/firewall.py
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
"""Manage Cloud Firewall rules and groups."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
import json
|
|
6
|
+
import textwrap
|
|
7
|
+
from typing import Optional, List, Tuple
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from ipaddress import ip_network
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from click import UsageError
|
|
17
|
+
import yaml
|
|
18
|
+
from requests import Response
|
|
19
|
+
|
|
20
|
+
from twc import fmt
|
|
21
|
+
from twc.api import TimewebCloud, FirewallProto
|
|
22
|
+
from twc.typerx import TyperAlias
|
|
23
|
+
from twc.apiwrap import create_client
|
|
24
|
+
from .common import (
|
|
25
|
+
verbose_option,
|
|
26
|
+
config_option,
|
|
27
|
+
profile_option,
|
|
28
|
+
filter_option,
|
|
29
|
+
output_format_option,
|
|
30
|
+
OutputFormat,
|
|
31
|
+
yes_option,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
firewall = TyperAlias(help=__doc__)
|
|
36
|
+
firewall_group = TyperAlias(help="Manage firewall groups.")
|
|
37
|
+
firewall_rule = TyperAlias(help="Manage firewall rules.")
|
|
38
|
+
firewall.add_typer(firewall_group, name="group", aliases=["groups"])
|
|
39
|
+
firewall.add_typer(firewall_rule, name="rule", aliases=["rules"])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _ResourceType(str, Enum):
|
|
43
|
+
SERVER = "server"
|
|
44
|
+
DATABASE = "database"
|
|
45
|
+
BALANCER = "balancer"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _ResourceType2(str, Enum):
|
|
49
|
+
# Ugly class for 'show' command
|
|
50
|
+
SERVER = "server"
|
|
51
|
+
DATABASE = "database"
|
|
52
|
+
BALANCER = "balancer"
|
|
53
|
+
ALL = "all"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# API issue: inconsistent resource naming: database->dbaas
|
|
57
|
+
RESOURCE_TYPES = {
|
|
58
|
+
"server": "server",
|
|
59
|
+
"database": "dbaas", # fix naming
|
|
60
|
+
"balancer": "balancer",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ------------------------------------------------------------- #
|
|
65
|
+
# $ twc firewall show #
|
|
66
|
+
# ------------------------------------------------------------- #
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def print_firewall_status(data: list):
|
|
70
|
+
print("Groups total:", len(data))
|
|
71
|
+
rules_total = 0
|
|
72
|
+
for group in data:
|
|
73
|
+
rules_total += len(group["rules"])
|
|
74
|
+
print("Rules total:", rules_total)
|
|
75
|
+
for group in data:
|
|
76
|
+
info = f"Group: {group['name']} ({group['id']})"
|
|
77
|
+
for rule in group["rules"]:
|
|
78
|
+
info += "\n" + textwrap.indent(
|
|
79
|
+
textwrap.dedent(
|
|
80
|
+
f"""
|
|
81
|
+
Rule: {rule['id']}
|
|
82
|
+
Direction: {rule['direction']}
|
|
83
|
+
Protocol: {rule['protocol']}
|
|
84
|
+
Port: {rule['port']}
|
|
85
|
+
CIDR: {rule['cidr']}
|
|
86
|
+
"""
|
|
87
|
+
).strip(),
|
|
88
|
+
" ",
|
|
89
|
+
)
|
|
90
|
+
if group["resources"]:
|
|
91
|
+
info += "\n Resources:\n"
|
|
92
|
+
servers = [
|
|
93
|
+
r["id"] for r in group["resources"] if r["type"] == "server"
|
|
94
|
+
]
|
|
95
|
+
databases = [
|
|
96
|
+
r["id"] for r in group["resources"] if r["type"] == "dbaas"
|
|
97
|
+
]
|
|
98
|
+
balancers = [
|
|
99
|
+
r["id"] for r in group["resources"] if r["type"] == "balancer"
|
|
100
|
+
]
|
|
101
|
+
if servers:
|
|
102
|
+
info += textwrap.indent(f"Servers: {servers}\n", " " * 4)
|
|
103
|
+
if databases:
|
|
104
|
+
info += textwrap.indent(f"Databases: {databases}\n", " " * 4)
|
|
105
|
+
if balancers:
|
|
106
|
+
info += textwrap.indent(
|
|
107
|
+
f"Load Balancers: {balancers}\n", " " * 4
|
|
108
|
+
)
|
|
109
|
+
print(info.strip())
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def print_rules_by_service(rules, filters):
|
|
113
|
+
if filters:
|
|
114
|
+
rules = fmt.filter_list(rules, filters)
|
|
115
|
+
table = fmt.Table()
|
|
116
|
+
table.header(
|
|
117
|
+
[
|
|
118
|
+
"GROUP ID",
|
|
119
|
+
"RULE ID",
|
|
120
|
+
"DIRECTION",
|
|
121
|
+
"PROTO",
|
|
122
|
+
"PORTS",
|
|
123
|
+
"CIDR",
|
|
124
|
+
]
|
|
125
|
+
)
|
|
126
|
+
for rule in rules:
|
|
127
|
+
table.row(
|
|
128
|
+
[
|
|
129
|
+
rule["group_id"],
|
|
130
|
+
rule["id"],
|
|
131
|
+
rule["direction"],
|
|
132
|
+
rule["protocol"],
|
|
133
|
+
rule["port"],
|
|
134
|
+
rule["cidr"],
|
|
135
|
+
]
|
|
136
|
+
)
|
|
137
|
+
table.print()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@firewall.command("show")
|
|
141
|
+
def firewall_status(
|
|
142
|
+
resource_type: _ResourceType2 = typer.Argument(
|
|
143
|
+
...,
|
|
144
|
+
metavar="(server|database|balancer|all)",
|
|
145
|
+
),
|
|
146
|
+
resource_id: str = typer.Argument(None),
|
|
147
|
+
verbose: Optional[bool] = verbose_option,
|
|
148
|
+
config: Optional[Path] = config_option,
|
|
149
|
+
profile: Optional[str] = profile_option,
|
|
150
|
+
output_format: Optional[str] = output_format_option,
|
|
151
|
+
filters: Optional[str] = filter_option,
|
|
152
|
+
):
|
|
153
|
+
"""Display firewall status."""
|
|
154
|
+
client = create_client(config, profile)
|
|
155
|
+
data = []
|
|
156
|
+
|
|
157
|
+
if resource_type in [r.value for r in _ResourceType]:
|
|
158
|
+
rules_total = []
|
|
159
|
+
if resource_id is None:
|
|
160
|
+
raise UsageError(
|
|
161
|
+
"Resource ID is required for "
|
|
162
|
+
f"{[r.value for r in _ResourceType]}"
|
|
163
|
+
)
|
|
164
|
+
groups_ = client.get_resource_firewall_groups(
|
|
165
|
+
int(resource_id), RESOURCE_TYPES[resource_type]
|
|
166
|
+
).json()["groups"]
|
|
167
|
+
for group_ in groups_:
|
|
168
|
+
rules_ = client.get_firewall_rules(group_["id"]).json()["rules"]
|
|
169
|
+
rules_total.extend(rules_)
|
|
170
|
+
print_rules_by_service(rules_total, filters)
|
|
171
|
+
|
|
172
|
+
if resource_type == "all":
|
|
173
|
+
if resource_id:
|
|
174
|
+
groups = [client.get_firewall_group(resource_id).json()["group"]]
|
|
175
|
+
else:
|
|
176
|
+
groups = client.get_firewall_groups().json()["groups"]
|
|
177
|
+
|
|
178
|
+
for group in groups:
|
|
179
|
+
rules = client.get_firewall_rules(group["id"]).json()["rules"]
|
|
180
|
+
resources = client.get_firewall_group_resources(
|
|
181
|
+
group["id"]
|
|
182
|
+
).json()["resources"]
|
|
183
|
+
data.append(
|
|
184
|
+
{
|
|
185
|
+
"id": group["id"],
|
|
186
|
+
"name": group["name"],
|
|
187
|
+
"rules": rules,
|
|
188
|
+
"resources": resources,
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
formats = [f.value for f in OutputFormat]
|
|
192
|
+
if output_format in formats:
|
|
193
|
+
if output_format == "raw":
|
|
194
|
+
print(data)
|
|
195
|
+
else:
|
|
196
|
+
encoders = {
|
|
197
|
+
"yaml": yaml.dump,
|
|
198
|
+
"json": json.dumps,
|
|
199
|
+
}
|
|
200
|
+
fmt.print_colored(
|
|
201
|
+
encoders[output_format](data), lang=output_format
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
print_firewall_status(data)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ------------------------------------------------------------- #
|
|
208
|
+
# $ twc firewall group list #
|
|
209
|
+
# ------------------------------------------------------------- #
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def print_firewall_groups(response: Response):
|
|
213
|
+
groups = response.json()["groups"]
|
|
214
|
+
table = fmt.Table()
|
|
215
|
+
table.header(["ID", "NAME"])
|
|
216
|
+
for group in groups:
|
|
217
|
+
table.row(
|
|
218
|
+
[
|
|
219
|
+
group["id"],
|
|
220
|
+
group["name"],
|
|
221
|
+
]
|
|
222
|
+
)
|
|
223
|
+
table.print()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@firewall_group.command("list")
|
|
227
|
+
def firewall_group_list(
|
|
228
|
+
verbose: Optional[bool] = verbose_option,
|
|
229
|
+
config: Optional[Path] = config_option,
|
|
230
|
+
profile: Optional[str] = profile_option,
|
|
231
|
+
output_format: Optional[str] = output_format_option,
|
|
232
|
+
):
|
|
233
|
+
"""List groups."""
|
|
234
|
+
client = create_client(config, profile)
|
|
235
|
+
response = client.get_firewall_groups()
|
|
236
|
+
fmt.printer(
|
|
237
|
+
response,
|
|
238
|
+
output_format=output_format,
|
|
239
|
+
func=print_firewall_groups,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ------------------------------------------------------------- #
|
|
244
|
+
# $ twc firewall group create #
|
|
245
|
+
# ------------------------------------------------------------- #
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@firewall_group.command("create")
|
|
249
|
+
def firewall_group_create(
|
|
250
|
+
verbose: Optional[bool] = verbose_option,
|
|
251
|
+
config: Optional[Path] = config_option,
|
|
252
|
+
profile: Optional[str] = profile_option,
|
|
253
|
+
output_format: Optional[str] = output_format_option,
|
|
254
|
+
name: str = typer.Option(..., help="Group display name."),
|
|
255
|
+
desc: Optional[str] = typer.Option(None, help="Description."),
|
|
256
|
+
):
|
|
257
|
+
"""Create new group of firewall rules."""
|
|
258
|
+
client = create_client(config, profile)
|
|
259
|
+
response = client.create_firewall_group(
|
|
260
|
+
name=name,
|
|
261
|
+
description=desc,
|
|
262
|
+
)
|
|
263
|
+
fmt.printer(
|
|
264
|
+
response,
|
|
265
|
+
output_format=output_format,
|
|
266
|
+
func=lambda response: print(response.json()["group"]["id"]),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ------------------------------------------------------------- #
|
|
271
|
+
# $ twc firewall group remove #
|
|
272
|
+
# ------------------------------------------------------------- #
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@firewall_group.command("remove", "rm")
|
|
276
|
+
def firewall_group_remove(
|
|
277
|
+
group_ids: List[UUID] = typer.Argument(..., metavar="GROUP_ID..."),
|
|
278
|
+
verbose: Optional[bool] = verbose_option,
|
|
279
|
+
config: Optional[Path] = config_option,
|
|
280
|
+
profile: Optional[str] = profile_option,
|
|
281
|
+
yes: Optional[bool] = yes_option,
|
|
282
|
+
):
|
|
283
|
+
"""Remove rules group. All rules in group will lost."""
|
|
284
|
+
if not yes:
|
|
285
|
+
typer.confirm("This action cannot be undone. Continue?", abort=True)
|
|
286
|
+
client = create_client(config, profile)
|
|
287
|
+
for group_id in group_ids:
|
|
288
|
+
response = client.delete_firewall_group(group_id)
|
|
289
|
+
if response.status_code == 204:
|
|
290
|
+
print(group_id)
|
|
291
|
+
else:
|
|
292
|
+
sys.exit(fmt.printer(response))
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# ------------------------------------------------------------- #
|
|
296
|
+
# $ twc firewall group set #
|
|
297
|
+
# ------------------------------------------------------------- #
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@firewall_group.command("set")
|
|
301
|
+
def firewall_group_set(
|
|
302
|
+
group_id: UUID,
|
|
303
|
+
verbose: Optional[bool] = verbose_option,
|
|
304
|
+
config: Optional[Path] = config_option,
|
|
305
|
+
profile: Optional[str] = profile_option,
|
|
306
|
+
output_format: Optional[str] = output_format_option,
|
|
307
|
+
name: Optional[str] = typer.Option(None, help="Group display name"),
|
|
308
|
+
desc: Optional[str] = typer.Option(None, help="Description."),
|
|
309
|
+
):
|
|
310
|
+
"""Set rules group properties."""
|
|
311
|
+
client = create_client(config, profile)
|
|
312
|
+
if not name and not desc:
|
|
313
|
+
raise UsageError(
|
|
314
|
+
"Nothing to do. Set one of options: ['--name', '--desc']"
|
|
315
|
+
)
|
|
316
|
+
if not name:
|
|
317
|
+
# Get old firewall group name, because name is required
|
|
318
|
+
name = client.get_firewall_group(group_id).json()["group"]["name"]
|
|
319
|
+
response = client.update_firewall_group(group_id, name, desc)
|
|
320
|
+
fmt.printer(
|
|
321
|
+
response,
|
|
322
|
+
output_format=output_format,
|
|
323
|
+
func=lambda response: print(response.json()["group"]["id"]),
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# ------------------------------------------------------------- #
|
|
328
|
+
# $ twc firewall link #
|
|
329
|
+
# ------------------------------------------------------------- #
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@firewall.command("link")
|
|
333
|
+
def firewall_link(
|
|
334
|
+
resource_type: _ResourceType = typer.Argument(
|
|
335
|
+
...,
|
|
336
|
+
metavar="(server|database|balancer)",
|
|
337
|
+
),
|
|
338
|
+
resource_id: int = typer.Argument(...),
|
|
339
|
+
group_id: UUID = typer.Argument(...),
|
|
340
|
+
verbose: Optional[bool] = verbose_option,
|
|
341
|
+
config: Optional[Path] = config_option,
|
|
342
|
+
profile: Optional[str] = profile_option,
|
|
343
|
+
output_format: Optional[str] = output_format_option,
|
|
344
|
+
):
|
|
345
|
+
"""Link rules group to service."""
|
|
346
|
+
client = create_client(config, profile)
|
|
347
|
+
response = client.link_resource_to_firewall(
|
|
348
|
+
group_id,
|
|
349
|
+
resource_id,
|
|
350
|
+
RESOURCE_TYPES[resource_type],
|
|
351
|
+
)
|
|
352
|
+
fmt.printer(
|
|
353
|
+
response,
|
|
354
|
+
output_format=output_format,
|
|
355
|
+
func=lambda response: print(response.json()["resource"]["id"]),
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ------------------------------------------------------------- #
|
|
360
|
+
# $ twc firewall unlink #
|
|
361
|
+
# ------------------------------------------------------------- #
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@firewall.command("unlink")
|
|
365
|
+
def firewall_unlink(
|
|
366
|
+
resource_type: _ResourceType = typer.Argument(
|
|
367
|
+
...,
|
|
368
|
+
metavar="(server|database|balancer)",
|
|
369
|
+
),
|
|
370
|
+
resource_id: int = typer.Argument(...),
|
|
371
|
+
group_id: UUID = typer.Argument(None),
|
|
372
|
+
verbose: Optional[bool] = verbose_option,
|
|
373
|
+
config: Optional[Path] = config_option,
|
|
374
|
+
profile: Optional[str] = profile_option,
|
|
375
|
+
all_groups: bool = typer.Option(
|
|
376
|
+
False,
|
|
377
|
+
"--all",
|
|
378
|
+
"-a",
|
|
379
|
+
help="Unlink all linked firewall groups.",
|
|
380
|
+
),
|
|
381
|
+
):
|
|
382
|
+
"""Unlink rules group from service."""
|
|
383
|
+
if not all_groups and group_id is None:
|
|
384
|
+
raise UsageError(
|
|
385
|
+
"One of parameters is required: ['--all', 'GROUP_ID']"
|
|
386
|
+
)
|
|
387
|
+
client = create_client(config, profile)
|
|
388
|
+
if all_groups:
|
|
389
|
+
groups_ = client.get_resource_firewall_groups(
|
|
390
|
+
resource_id, RESOURCE_TYPES[resource_type]
|
|
391
|
+
)
|
|
392
|
+
groups = [g["id"] for g in groups_.json()["groups"]]
|
|
393
|
+
else:
|
|
394
|
+
groups = [group_id]
|
|
395
|
+
for group in groups:
|
|
396
|
+
response = client.unlink_resource_from_firewall(
|
|
397
|
+
group,
|
|
398
|
+
resource_id,
|
|
399
|
+
RESOURCE_TYPES[resource_type],
|
|
400
|
+
)
|
|
401
|
+
if response.status_code == 204:
|
|
402
|
+
print("Unlinked:", group)
|
|
403
|
+
else:
|
|
404
|
+
sys.exit(fmt.printer(response))
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# ------------------------------------------------------------- #
|
|
408
|
+
# $ twc firewall rule list #
|
|
409
|
+
# ------------------------------------------------------------- #
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def print_rules(response: Response):
|
|
413
|
+
rules = response.json()["rules"]
|
|
414
|
+
table = fmt.Table()
|
|
415
|
+
table.header(
|
|
416
|
+
[
|
|
417
|
+
"ID",
|
|
418
|
+
"DIRECTION",
|
|
419
|
+
"PROTO",
|
|
420
|
+
"PORTS",
|
|
421
|
+
"CIDR",
|
|
422
|
+
]
|
|
423
|
+
)
|
|
424
|
+
for rule in rules:
|
|
425
|
+
table.row(
|
|
426
|
+
[
|
|
427
|
+
rule["id"],
|
|
428
|
+
rule["direction"],
|
|
429
|
+
rule["protocol"],
|
|
430
|
+
rule["port"],
|
|
431
|
+
rule["cidr"],
|
|
432
|
+
]
|
|
433
|
+
)
|
|
434
|
+
table.print()
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@firewall_rule.command("list", "ls")
|
|
438
|
+
def filrewall_rule_list(
|
|
439
|
+
group_id: UUID,
|
|
440
|
+
verbose: Optional[bool] = verbose_option,
|
|
441
|
+
config: Optional[Path] = config_option,
|
|
442
|
+
profile: Optional[str] = profile_option,
|
|
443
|
+
output_format: Optional[str] = output_format_option,
|
|
444
|
+
):
|
|
445
|
+
"""List rules in group."""
|
|
446
|
+
client = create_client(config, profile)
|
|
447
|
+
response = client.get_firewall_rules(group_id)
|
|
448
|
+
fmt.printer(
|
|
449
|
+
response,
|
|
450
|
+
output_format=output_format,
|
|
451
|
+
func=print_rules,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
# ------------------------------------------------------------- #
|
|
456
|
+
# $ twc firewall rule add #
|
|
457
|
+
# ------------------------------------------------------------- #
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def port_proto_callback(values) -> List[Tuple[Optional[str], str]]:
|
|
461
|
+
new_values = []
|
|
462
|
+
for value in values:
|
|
463
|
+
if not re.match(r"((^\d+(-\d+)?/)?(tcp|udp)$)|(^icmp$)", value, re.I):
|
|
464
|
+
sys.exit(
|
|
465
|
+
f"Error: Malformed argument: '{value}': "
|
|
466
|
+
"correct patterns: '22/TCP', '2000-3000/UDP', 'ICMP', etc."
|
|
467
|
+
)
|
|
468
|
+
if re.match(r"^icmp$", value, re.I):
|
|
469
|
+
new_values.append((None, "icmp"))
|
|
470
|
+
else:
|
|
471
|
+
ports, proto = value.split("/")
|
|
472
|
+
new_values.append((ports, proto.lower()))
|
|
473
|
+
return new_values
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def validate_cidr_callback(value):
|
|
477
|
+
if value is not None:
|
|
478
|
+
try:
|
|
479
|
+
assert ip_network(value)
|
|
480
|
+
except ValueError as err:
|
|
481
|
+
sys.exit(f"Error: Invalid CIDR: {err}")
|
|
482
|
+
return value
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
@firewall_rule.command("add")
|
|
486
|
+
def firewall_allow(
|
|
487
|
+
ports: List[str] = typer.Argument(
|
|
488
|
+
...,
|
|
489
|
+
metavar="[PORT[-PORT]/]PROTO...",
|
|
490
|
+
callback=port_proto_callback,
|
|
491
|
+
help="List of port/protocol pairs e.g. 22/TCP, 2000-3000/UDP, ICMP",
|
|
492
|
+
),
|
|
493
|
+
verbose: Optional[bool] = verbose_option,
|
|
494
|
+
config: Optional[Path] = config_option,
|
|
495
|
+
profile: Optional[str] = profile_option,
|
|
496
|
+
output_format: Optional[str] = output_format_option,
|
|
497
|
+
group: Optional[UUID] = typer.Option(
|
|
498
|
+
None,
|
|
499
|
+
"--group",
|
|
500
|
+
"-g",
|
|
501
|
+
help="Firewall rules group UUID.",
|
|
502
|
+
),
|
|
503
|
+
make_group: Optional[bool] = typer.Option(
|
|
504
|
+
None,
|
|
505
|
+
"--make-group",
|
|
506
|
+
"-G",
|
|
507
|
+
help="Add rules in new rules group.",
|
|
508
|
+
),
|
|
509
|
+
group_name: Optional[str] = typer.Option(
|
|
510
|
+
None,
|
|
511
|
+
help="Rules group name, can be used with '--make-group'",
|
|
512
|
+
),
|
|
513
|
+
direction_: bool = typer.Option(
|
|
514
|
+
True, "--ingress/--egress", help="Traffic direction."
|
|
515
|
+
),
|
|
516
|
+
cidr: Optional[str] = typer.Option(
|
|
517
|
+
"0.0.0.0/0",
|
|
518
|
+
metavar="IP_NETWORK",
|
|
519
|
+
callback=validate_cidr_callback,
|
|
520
|
+
help="IPv4 or IPv6 CIDR.",
|
|
521
|
+
),
|
|
522
|
+
):
|
|
523
|
+
"""Add new firewall rule."""
|
|
524
|
+
client = create_client(config, profile)
|
|
525
|
+
if make_group is not None and group is not None:
|
|
526
|
+
raise UsageError(
|
|
527
|
+
"'--group' and '--make-group' options is mutually exclusive."
|
|
528
|
+
)
|
|
529
|
+
if make_group is None and group is None:
|
|
530
|
+
raise UsageError(
|
|
531
|
+
"One of options is required: ['--group', '--make-group']"
|
|
532
|
+
)
|
|
533
|
+
if make_group:
|
|
534
|
+
if group_name is None:
|
|
535
|
+
group_name = "Firewall Group " + datetime.now().strftime(
|
|
536
|
+
"%Y.%m.%d-%H:%M:%S"
|
|
537
|
+
)
|
|
538
|
+
group_resp = client.create_firewall_group(group_name)
|
|
539
|
+
group = group_resp.json()["group"]["id"]
|
|
540
|
+
logging.debug("New firewall rules group: %s", group)
|
|
541
|
+
fmt.printer(
|
|
542
|
+
group_resp,
|
|
543
|
+
output_format=output_format,
|
|
544
|
+
func=lambda x: print("Created rules group:", group),
|
|
545
|
+
)
|
|
546
|
+
for port in ports:
|
|
547
|
+
if direction_ is True:
|
|
548
|
+
direction = "ingress"
|
|
549
|
+
else:
|
|
550
|
+
direction = "egress"
|
|
551
|
+
response = client.create_firewall_rule(
|
|
552
|
+
group,
|
|
553
|
+
direction=direction,
|
|
554
|
+
port=port[0], # :str port or port range
|
|
555
|
+
proto=port[1], # :str protocol name
|
|
556
|
+
cidr=cidr,
|
|
557
|
+
)
|
|
558
|
+
fmt.printer(
|
|
559
|
+
response,
|
|
560
|
+
output_format=output_format,
|
|
561
|
+
func=lambda response: print(response.json()["rule"]["id"]),
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
# ------------------------------------------------------------- #
|
|
566
|
+
# $ twc firewall rule remove #
|
|
567
|
+
# ------------------------------------------------------------- #
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def get_group_id_by_rule(client: TimewebCloud, rule_id: UUID) -> str:
|
|
571
|
+
groups = client.get_firewall_groups().json()["groups"]
|
|
572
|
+
for group in groups:
|
|
573
|
+
rules = client.get_firewall_rules(group["id"]).json()["rules"]
|
|
574
|
+
for rule in rules:
|
|
575
|
+
if str(rule_id) == rule["id"]:
|
|
576
|
+
return group["id"]
|
|
577
|
+
sys.exit(f"Error: Rule '{rule_id}' not found")
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
@firewall_rule.command("remove", "rm")
|
|
581
|
+
def firewall_rule_remove(
|
|
582
|
+
rule_id: UUID,
|
|
583
|
+
verbose: Optional[bool] = verbose_option,
|
|
584
|
+
config: Optional[Path] = config_option,
|
|
585
|
+
profile: Optional[str] = profile_option,
|
|
586
|
+
):
|
|
587
|
+
"""Remove firewall rule."""
|
|
588
|
+
client = create_client(config, profile)
|
|
589
|
+
group_id = get_group_id_by_rule(client, rule_id)
|
|
590
|
+
response = client.delete_firewall_rule(group_id, rule_id)
|
|
591
|
+
if response.status_code == 204:
|
|
592
|
+
print(rule_id)
|
|
593
|
+
else:
|
|
594
|
+
sys.exit(fmt.printer(response))
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
# ------------------------------------------------------------- #
|
|
598
|
+
# $ twc firewall rule update #
|
|
599
|
+
# ------------------------------------------------------------- #
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
@firewall_rule.command("update", "upd")
|
|
603
|
+
def filrewa_rule_update(
|
|
604
|
+
rule_id: UUID,
|
|
605
|
+
verbose: Optional[bool] = verbose_option,
|
|
606
|
+
config: Optional[Path] = config_option,
|
|
607
|
+
profile: Optional[str] = profile_option,
|
|
608
|
+
output_format: Optional[str] = output_format_option,
|
|
609
|
+
direction_: Optional[bool] = typer.Option(
|
|
610
|
+
None,
|
|
611
|
+
"--ingress/--egress",
|
|
612
|
+
help="Traffic direction.",
|
|
613
|
+
),
|
|
614
|
+
cidr: Optional[str] = typer.Option(
|
|
615
|
+
None,
|
|
616
|
+
metavar="IP_NETWORK",
|
|
617
|
+
callback=validate_cidr_callback,
|
|
618
|
+
help="IPv4 or IPv6 CIDR.",
|
|
619
|
+
),
|
|
620
|
+
port: Optional[str] = typer.Option(
|
|
621
|
+
None,
|
|
622
|
+
metavar="PORT[-PORT]",
|
|
623
|
+
help="Port or ports range e.g. 22, 2000-3000",
|
|
624
|
+
),
|
|
625
|
+
proto: Optional[FirewallProto] = typer.Option(None, help="Protocol."),
|
|
626
|
+
):
|
|
627
|
+
"""Change firewall rule."""
|
|
628
|
+
client = create_client(config, profile)
|
|
629
|
+
group_id = get_group_id_by_rule(client, rule_id)
|
|
630
|
+
old_state = client.get_firewall_rule(group_id, rule_id).json()["rule"]
|
|
631
|
+
if direction_ is None:
|
|
632
|
+
direction = old_state["direction"]
|
|
633
|
+
elif direction is True:
|
|
634
|
+
direction = ("ingress",)
|
|
635
|
+
else:
|
|
636
|
+
direction = "egress"
|
|
637
|
+
if proto is None:
|
|
638
|
+
proto = old_state["protocol"]
|
|
639
|
+
if cidr is None:
|
|
640
|
+
cidr = old_state["cidr"]
|
|
641
|
+
payload = {
|
|
642
|
+
"group_id": group_id,
|
|
643
|
+
"rule_id": rule_id,
|
|
644
|
+
"direction": direction,
|
|
645
|
+
"port": port,
|
|
646
|
+
"proto": proto,
|
|
647
|
+
"cidr": cidr,
|
|
648
|
+
}
|
|
649
|
+
response = client.update_firewall_rule(**payload)
|
|
650
|
+
fmt.printer(
|
|
651
|
+
response,
|
|
652
|
+
output_format=output_format,
|
|
653
|
+
func=lambda response: print(response.json()["rule"]["id"]),
|
|
654
|
+
)
|