twc-cli 2.4.1__py3-none-any.whl → 2.6.0__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 +46 -0
- twc/__main__.py +2 -0
- twc/__version__.py +1 -1
- twc/api/base.py +5 -8
- twc/api/client.py +95 -11
- twc/api/types.py +64 -7
- twc/apiwrap.py +2 -2
- twc/commands/__init__.py +1 -0
- twc/commands/common.py +18 -2
- twc/commands/firewall.py +238 -29
- twc/commands/floating_ip.py +296 -0
- twc/commands/image.py +10 -7
- twc/commands/project.py +1 -2
- twc/commands/server.py +75 -21
- twc/commands/storage.py +7 -3
- twc/commands/vpc.py +23 -9
- twc/fmt.py +1 -1
- twc/vars.py +3 -5
- twc_cli-2.6.0.dist-info/METADATA +83 -0
- twc_cli-2.6.0.dist-info/RECORD +36 -0
- {twc_cli-2.4.1.dist-info → twc_cli-2.6.0.dist-info}/WHEEL +1 -1
- twc_cli-2.4.1.dist-info/METADATA +0 -66
- twc_cli-2.4.1.dist-info/RECORD +0 -35
- {twc_cli-2.4.1.dist-info → twc_cli-2.6.0.dist-info}/COPYING +0 -0
- {twc_cli-2.4.1.dist-info → twc_cli-2.6.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Manage floating IPs."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Optional, List
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from requests import Response
|
|
10
|
+
|
|
11
|
+
from twc import fmt
|
|
12
|
+
from twc.api import TimewebCloud, ResourceType
|
|
13
|
+
from twc.apiwrap import create_client
|
|
14
|
+
from twc.typerx import TyperAlias
|
|
15
|
+
from .common import (
|
|
16
|
+
verbose_option,
|
|
17
|
+
config_option,
|
|
18
|
+
profile_option,
|
|
19
|
+
yes_option,
|
|
20
|
+
output_format_option,
|
|
21
|
+
load_from_config_callback,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
floating_ip = TyperAlias(help=__doc__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_floating_ip_id(client: TimewebCloud, ip_addr: str) -> Optional[str]:
|
|
29
|
+
ips = client.get_floating_ips().json()["ips"]
|
|
30
|
+
for ip in ips:
|
|
31
|
+
if ip["ip"] == ip_addr:
|
|
32
|
+
return ip["id"]
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ------------------------------------------------------------- #
|
|
37
|
+
# $ twc floating-ip list #
|
|
38
|
+
# ------------------------------------------------------------- #
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _print_floating_ips(response: Response):
|
|
42
|
+
table = fmt.Table()
|
|
43
|
+
table.header(["IP", "PTR", "ZONE", "ANTI_DDOS", "USED_ON"])
|
|
44
|
+
ips = response.json()["ips"]
|
|
45
|
+
for ip in ips:
|
|
46
|
+
used = None
|
|
47
|
+
if ip["resource_type"]:
|
|
48
|
+
used = f"{ip['resource_type']}:{ip['resource_id']}"
|
|
49
|
+
table.row(
|
|
50
|
+
[
|
|
51
|
+
ip["ip"],
|
|
52
|
+
ip["ptr"],
|
|
53
|
+
ip["availability_zone"],
|
|
54
|
+
ip["is_ddos_guard"],
|
|
55
|
+
used,
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
table.print()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@floating_ip.command("list", "ls")
|
|
62
|
+
def floating_ip_list(
|
|
63
|
+
verbose: Optional[bool] = verbose_option,
|
|
64
|
+
config: Optional[Path] = config_option,
|
|
65
|
+
profile: Optional[str] = profile_option,
|
|
66
|
+
output_format: Optional[str] = output_format_option,
|
|
67
|
+
):
|
|
68
|
+
"""List floating IPs."""
|
|
69
|
+
client = create_client(config, profile)
|
|
70
|
+
response = client.get_floating_ips()
|
|
71
|
+
fmt.printer(
|
|
72
|
+
response,
|
|
73
|
+
output_format=output_format,
|
|
74
|
+
func=_print_floating_ips,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ------------------------------------------------------------- #
|
|
79
|
+
# $ twc floating-ip get #
|
|
80
|
+
# ------------------------------------------------------------- #
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _print_floating_ip(response: Response):
|
|
84
|
+
table = fmt.Table()
|
|
85
|
+
table.header(["IP", "PTR", "ZONE", "ANTI_DDOS", "USED_ON"])
|
|
86
|
+
ip = response.json()["ip"]
|
|
87
|
+
used = None
|
|
88
|
+
if ip["resource_type"]:
|
|
89
|
+
used = f"{ip['resource_type']}:{ip['resource_id']}"
|
|
90
|
+
table.row(
|
|
91
|
+
[
|
|
92
|
+
ip["ip"],
|
|
93
|
+
ip["ptr"],
|
|
94
|
+
ip["availability_zone"],
|
|
95
|
+
ip["is_ddos_guard"],
|
|
96
|
+
used,
|
|
97
|
+
]
|
|
98
|
+
)
|
|
99
|
+
table.print()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@floating_ip.command("get")
|
|
103
|
+
def floating_ip_get(
|
|
104
|
+
ip: str,
|
|
105
|
+
verbose: Optional[bool] = verbose_option,
|
|
106
|
+
config: Optional[Path] = config_option,
|
|
107
|
+
profile: Optional[str] = profile_option,
|
|
108
|
+
output_format: Optional[str] = output_format_option,
|
|
109
|
+
):
|
|
110
|
+
"""Get floating IP."""
|
|
111
|
+
client = create_client(config, profile)
|
|
112
|
+
try:
|
|
113
|
+
_ = UUID(ip)
|
|
114
|
+
except ValueError:
|
|
115
|
+
ip = get_floating_ip_id(client, ip)
|
|
116
|
+
response = client.get_floating_ip(ip)
|
|
117
|
+
fmt.printer(
|
|
118
|
+
response,
|
|
119
|
+
output_format=output_format,
|
|
120
|
+
func=_print_floating_ip,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ------------------------------------------------------------- #
|
|
125
|
+
# $ twc floating-ip create #
|
|
126
|
+
# ------------------------------------------------------------- #
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@floating_ip.command("create")
|
|
130
|
+
def floating_ip_create(
|
|
131
|
+
verbose: Optional[bool] = verbose_option,
|
|
132
|
+
config: Optional[Path] = config_option,
|
|
133
|
+
profile: Optional[str] = profile_option,
|
|
134
|
+
output_format: Optional[str] = output_format_option,
|
|
135
|
+
availability_zone: str = typer.Option(
|
|
136
|
+
...,
|
|
137
|
+
metavar="ZONE",
|
|
138
|
+
envvar="TWC_AVAILABILITY_ZONE",
|
|
139
|
+
show_envvar=False,
|
|
140
|
+
callback=load_from_config_callback,
|
|
141
|
+
help="Availability zone.",
|
|
142
|
+
),
|
|
143
|
+
ddos_protection: bool = typer.Option(
|
|
144
|
+
False,
|
|
145
|
+
"--ddos-protection",
|
|
146
|
+
show_default=True,
|
|
147
|
+
help="Request IP-address with L3/L4 DDoS protection.",
|
|
148
|
+
),
|
|
149
|
+
):
|
|
150
|
+
"""Create new floating IP."""
|
|
151
|
+
client = create_client(config, profile)
|
|
152
|
+
response = client.create_floating_ip(
|
|
153
|
+
availability_zone=availability_zone,
|
|
154
|
+
ddos_protection=ddos_protection,
|
|
155
|
+
)
|
|
156
|
+
fmt.printer(
|
|
157
|
+
response,
|
|
158
|
+
output_format=output_format,
|
|
159
|
+
func=lambda response: print(response.json()["ip"]["ip"]),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ------------------------------------------------------------- #
|
|
164
|
+
# $ twc floating-ip remove #
|
|
165
|
+
# ------------------------------------------------------------- #
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@floating_ip.command("remove", "rm")
|
|
169
|
+
def floating_ip_remove(
|
|
170
|
+
floating_ips: List[str] = typer.Argument(..., metavar="IP..."),
|
|
171
|
+
verbose: Optional[bool] = verbose_option,
|
|
172
|
+
config: Optional[Path] = config_option,
|
|
173
|
+
profile: Optional[str] = profile_option,
|
|
174
|
+
yes: Optional[bool] = yes_option,
|
|
175
|
+
):
|
|
176
|
+
"""Remove floating IPs."""
|
|
177
|
+
if not yes:
|
|
178
|
+
typer.confirm("This action cannot be undone, continue?", abort=True)
|
|
179
|
+
|
|
180
|
+
client = create_client(config, profile)
|
|
181
|
+
for ip in floating_ips:
|
|
182
|
+
try:
|
|
183
|
+
_ = UUID(ip)
|
|
184
|
+
except ValueError:
|
|
185
|
+
ip = get_floating_ip_id(client, ip)
|
|
186
|
+
response = client.delete_floating_ip(ip)
|
|
187
|
+
if response.status_code == 204:
|
|
188
|
+
print(ip)
|
|
189
|
+
else:
|
|
190
|
+
sys.exit(fmt.printer(response))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ------------------------------------------------------------- #
|
|
194
|
+
# $ twc floating-ip attach #
|
|
195
|
+
# ------------------------------------------------------------- #
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@floating_ip.command("attach")
|
|
199
|
+
def floating_ip_attach(
|
|
200
|
+
ip: str,
|
|
201
|
+
verbose: Optional[bool] = verbose_option,
|
|
202
|
+
config: Optional[Path] = config_option,
|
|
203
|
+
profile: Optional[str] = profile_option,
|
|
204
|
+
server: Optional[int] = typer.Option(
|
|
205
|
+
None, help="Attach IP to Cloud Server."
|
|
206
|
+
),
|
|
207
|
+
balancer: Optional[int] = typer.Option(
|
|
208
|
+
None, help="Attach IP to Load Balancer."
|
|
209
|
+
),
|
|
210
|
+
database: Optional[int] = typer.Option(
|
|
211
|
+
None, help="Attach IP to managed database cluster."
|
|
212
|
+
),
|
|
213
|
+
):
|
|
214
|
+
"""Attach floating IP to service."""
|
|
215
|
+
client = create_client(config, profile)
|
|
216
|
+
try:
|
|
217
|
+
_ = UUID(ip)
|
|
218
|
+
except ValueError:
|
|
219
|
+
ip = get_floating_ip_id(client, ip)
|
|
220
|
+
resource_type = resource_id = None
|
|
221
|
+
if server:
|
|
222
|
+
resource_type = ResourceType.SERVER
|
|
223
|
+
resource_id = server
|
|
224
|
+
if balancer:
|
|
225
|
+
resource_type = ResourceType.BALANCER
|
|
226
|
+
resource_id = balancer
|
|
227
|
+
if database:
|
|
228
|
+
resource_type = ResourceType.DATABASE
|
|
229
|
+
resource_id = database
|
|
230
|
+
response = client.attach_floating_ip(
|
|
231
|
+
ip,
|
|
232
|
+
resource_type=resource_type,
|
|
233
|
+
resource_id=resource_id,
|
|
234
|
+
)
|
|
235
|
+
if not resource_type or not resource_id:
|
|
236
|
+
sys.exit(
|
|
237
|
+
"Error: Please set one of options: ['--server', '--balancer', '--database']"
|
|
238
|
+
)
|
|
239
|
+
if response.status_code == 204:
|
|
240
|
+
print(resource_id)
|
|
241
|
+
else:
|
|
242
|
+
sys.exit(fmt.printer(response))
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ------------------------------------------------------------- #
|
|
246
|
+
# $ twc floating-ip detach #
|
|
247
|
+
# ------------------------------------------------------------- #
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@floating_ip.command("detach")
|
|
251
|
+
def floating_ip_detach(
|
|
252
|
+
ip: str,
|
|
253
|
+
verbose: Optional[bool] = verbose_option,
|
|
254
|
+
config: Optional[Path] = config_option,
|
|
255
|
+
profile: Optional[str] = profile_option,
|
|
256
|
+
):
|
|
257
|
+
"""Detach floating IP from service."""
|
|
258
|
+
client = create_client(config, profile)
|
|
259
|
+
try:
|
|
260
|
+
_ = UUID(ip)
|
|
261
|
+
except ValueError:
|
|
262
|
+
ip = get_floating_ip_id(client, ip)
|
|
263
|
+
response = client.detach_floating_ip(ip)
|
|
264
|
+
if response.status_code == 204:
|
|
265
|
+
print(ip)
|
|
266
|
+
else:
|
|
267
|
+
sys.exit(fmt.printer(response))
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ------------------------------------------------------------- #
|
|
271
|
+
# $ twc floating-ip set #
|
|
272
|
+
# ------------------------------------------------------------- #
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@floating_ip.command("set")
|
|
276
|
+
def floating_ip_set(
|
|
277
|
+
ip: str,
|
|
278
|
+
verbose: Optional[bool] = verbose_option,
|
|
279
|
+
config: Optional[Path] = config_option,
|
|
280
|
+
profile: Optional[str] = profile_option,
|
|
281
|
+
output_format: Optional[str] = output_format_option,
|
|
282
|
+
comment: Optional[str] = typer.Option(None, help="Set comment."),
|
|
283
|
+
ptr: Optional[str] = typer.Option(None, help="Set reverse DNS pointer."),
|
|
284
|
+
):
|
|
285
|
+
"""Set floating IP parameters."""
|
|
286
|
+
client = create_client(config, profile)
|
|
287
|
+
try:
|
|
288
|
+
_ = UUID(ip)
|
|
289
|
+
except ValueError:
|
|
290
|
+
ip = get_floating_ip_id(client, ip)
|
|
291
|
+
response = client.update_floating_ip(ip, comment=comment, ptr=ptr)
|
|
292
|
+
fmt.printer(
|
|
293
|
+
response,
|
|
294
|
+
output_format=output_format,
|
|
295
|
+
func=lambda response: print(response.json()["ip"]["ip"]),
|
|
296
|
+
)
|
twc/commands/image.py
CHANGED
|
@@ -262,13 +262,16 @@ def image_upload(
|
|
|
262
262
|
client = create_client(config, profile)
|
|
263
263
|
if re.match(r"https?://", file):
|
|
264
264
|
debug(f"Upload URL: {file}")
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
265
|
+
else:
|
|
266
|
+
sys.exit(f"Invalid link: {file}")
|
|
267
|
+
|
|
268
|
+
response = client.create_image(
|
|
269
|
+
upload_url=file,
|
|
270
|
+
name=name,
|
|
271
|
+
description=desc,
|
|
272
|
+
os_type=os_type,
|
|
273
|
+
location=region,
|
|
274
|
+
)
|
|
272
275
|
|
|
273
276
|
# FUTURE: Implement file upload from local disk
|
|
274
277
|
|
twc/commands/project.py
CHANGED
|
@@ -295,11 +295,10 @@ def project_resource_move(
|
|
|
295
295
|
if bucket:
|
|
296
296
|
for bucket_id in bucket:
|
|
297
297
|
if not bucket_id.isdigit():
|
|
298
|
-
bucket_name = bucket_id
|
|
299
298
|
bucket_id = resolve_bucket_id(client, bucket_id)
|
|
300
299
|
response = client.add_bucket_to_project(bucket_id, project_id)
|
|
301
300
|
if response.status_code == 200:
|
|
302
|
-
print(
|
|
301
|
+
print(bucket_id)
|
|
303
302
|
else:
|
|
304
303
|
sys.exit(fmt.printer(response))
|
|
305
304
|
if cluster:
|
twc/commands/server.py
CHANGED
|
@@ -7,7 +7,7 @@ from logging import debug
|
|
|
7
7
|
from typing import Optional, List, Union
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from datetime import date, datetime
|
|
10
|
-
from ipaddress import IPv4Address, IPv6Address
|
|
10
|
+
from ipaddress import IPv4Address, IPv6Address, IPv4Network
|
|
11
11
|
|
|
12
12
|
import typer
|
|
13
13
|
from click import UsageError
|
|
@@ -28,7 +28,6 @@ from twc.api import (
|
|
|
28
28
|
BackupAction,
|
|
29
29
|
)
|
|
30
30
|
from twc.vars import (
|
|
31
|
-
REGIONS_WITH_CONFIGURATOR,
|
|
32
31
|
REGIONS_WITH_IPV6,
|
|
33
32
|
CONTROL_PANEL_URL,
|
|
34
33
|
)
|
|
@@ -40,6 +39,7 @@ from .common import (
|
|
|
40
39
|
yes_option,
|
|
41
40
|
output_format_option,
|
|
42
41
|
region_option,
|
|
42
|
+
zone_option,
|
|
43
43
|
load_from_config_callback,
|
|
44
44
|
)
|
|
45
45
|
|
|
@@ -48,7 +48,7 @@ server = TyperAlias(help=__doc__)
|
|
|
48
48
|
server_ip = TyperAlias(help="Manage public IPs.")
|
|
49
49
|
server_disk = TyperAlias(help="Manage Cloud Server disks.")
|
|
50
50
|
server_backup = TyperAlias(help="Manage Cloud Server disk backups.")
|
|
51
|
-
server.add_typer(server_ip, name="ip")
|
|
51
|
+
server.add_typer(server_ip, name="ip", deprecated=True)
|
|
52
52
|
server.add_typer(server_disk, name="disk")
|
|
53
53
|
server.add_typer(server_backup, name="backup")
|
|
54
54
|
|
|
@@ -80,10 +80,9 @@ def print_servers(
|
|
|
80
80
|
]
|
|
81
81
|
)
|
|
82
82
|
for srv in servers:
|
|
83
|
+
main_ipv4 = None
|
|
83
84
|
for network in srv["networks"]:
|
|
84
85
|
if network["type"] == "public":
|
|
85
|
-
if not network["ips"]:
|
|
86
|
-
main_ipv4 = None
|
|
87
86
|
for addr in network["ips"]:
|
|
88
87
|
if addr["type"] == "ipv4" and addr["is_main"]:
|
|
89
88
|
main_ipv4 = addr["ip"]
|
|
@@ -139,10 +138,9 @@ def print_server(response: Response):
|
|
|
139
138
|
"IPV4",
|
|
140
139
|
]
|
|
141
140
|
)
|
|
141
|
+
main_ipv4 = None
|
|
142
142
|
for network in srv["networks"]:
|
|
143
143
|
if network["type"] == "public":
|
|
144
|
-
if not network["ips"]:
|
|
145
|
-
main_ipv4 = None
|
|
146
144
|
for addr in network["ips"]:
|
|
147
145
|
if addr["type"] == "ipv4" and addr["is_main"]:
|
|
148
146
|
main_ipv4 = addr["ip"]
|
|
@@ -509,11 +507,14 @@ def server_create(
|
|
|
509
507
|
ssh_keys: Optional[List[str]] = typer.Option(
|
|
510
508
|
None, "--ssh-key", help="SSH-key file, name or ID. Can be multiple."
|
|
511
509
|
),
|
|
510
|
+
user_data: Optional[typer.FileText] = typer.Option(
|
|
511
|
+
None, help="user-data file for cloud-init."
|
|
512
|
+
),
|
|
512
513
|
ddos_protection: bool = typer.Option(
|
|
513
514
|
False,
|
|
514
515
|
"--ddos-protection",
|
|
515
516
|
show_default=True,
|
|
516
|
-
help="
|
|
517
|
+
help="Request public IPv4 with L3/L4 DDoS protection.",
|
|
517
518
|
),
|
|
518
519
|
local_network: Optional[bool] = typer.Option(
|
|
519
520
|
# is_local_network paramenter is deprecated!
|
|
@@ -524,6 +525,15 @@ def server_create(
|
|
|
524
525
|
hidden=True,
|
|
525
526
|
),
|
|
526
527
|
network: Optional[str] = typer.Option(None, help="Private network ID."),
|
|
528
|
+
private_ip: Optional[str] = typer.Option(
|
|
529
|
+
None, help="Private IPv4 address."
|
|
530
|
+
),
|
|
531
|
+
public_ip: Optional[str] = typer.Option(
|
|
532
|
+
None, help="Public IPv4 address. New address by default."
|
|
533
|
+
),
|
|
534
|
+
no_public_ip: Optional[bool] = typer.Option(
|
|
535
|
+
False, "--no-public-ip", help="Do not add public IPv4 address."
|
|
536
|
+
),
|
|
527
537
|
nat_mode: ServerNATMode = typer.Option(
|
|
528
538
|
None,
|
|
529
539
|
"--nat-mode",
|
|
@@ -531,6 +541,7 @@ def server_create(
|
|
|
531
541
|
help="Apply NAT mode.",
|
|
532
542
|
),
|
|
533
543
|
region: Optional[str] = region_option,
|
|
544
|
+
availability_zone: Optional[str] = zone_option,
|
|
534
545
|
project_id: int = typer.Option(
|
|
535
546
|
None,
|
|
536
547
|
envvar="TWC_PROJECT",
|
|
@@ -541,17 +552,14 @@ def server_create(
|
|
|
541
552
|
):
|
|
542
553
|
"""Create Cloud Server."""
|
|
543
554
|
client = create_client(config, profile)
|
|
544
|
-
|
|
545
|
-
if nat_mode:
|
|
546
|
-
if not network:
|
|
547
|
-
sys.exit("Error: Pass '--network' option first.")
|
|
548
|
-
|
|
549
555
|
payload = {
|
|
550
556
|
"name": name,
|
|
551
557
|
"comment": comment,
|
|
552
558
|
"avatar_id": avatar_id,
|
|
553
559
|
"software_id": software_id,
|
|
554
560
|
"is_ddos_guard": ddos_protection,
|
|
561
|
+
"availability_zone": availability_zone,
|
|
562
|
+
"network": {},
|
|
555
563
|
**(
|
|
556
564
|
{"is_local_network": local_network}
|
|
557
565
|
if local_network is not None
|
|
@@ -559,8 +567,44 @@ def server_create(
|
|
|
559
567
|
),
|
|
560
568
|
}
|
|
561
569
|
|
|
570
|
+
# Check availability zone
|
|
571
|
+
usable_zones = ServiceRegion.get_zones(region)
|
|
572
|
+
if availability_zone is not None and availability_zone not in usable_zones:
|
|
573
|
+
sys.exit(
|
|
574
|
+
f"Error: Wrong availability zone, usable zones are: {usable_zones}"
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
# Set network parameters
|
|
578
|
+
if nat_mode or private_ip:
|
|
579
|
+
if not network:
|
|
580
|
+
sys.exit("Error: Pass '--network' option first.")
|
|
562
581
|
if network:
|
|
563
|
-
payload["network"]
|
|
582
|
+
payload["network"]["id"] = network
|
|
583
|
+
if private_ip:
|
|
584
|
+
net = IPv4Network(
|
|
585
|
+
client.get_vpc(network).json()["vpc"]["subnet_v4"]
|
|
586
|
+
)
|
|
587
|
+
if IPv4Address(private_ip) > IPv4Address(
|
|
588
|
+
int(net.network_address) + 4
|
|
589
|
+
):
|
|
590
|
+
payload["network"]["ip"] = private_ip
|
|
591
|
+
else:
|
|
592
|
+
# First 3 addresses is reserved for networks OVN based networks
|
|
593
|
+
sys.exit(
|
|
594
|
+
f"Error: Private address '{private_ip}' is not allowed. "
|
|
595
|
+
"IP must be at least the fourth in order in the network."
|
|
596
|
+
)
|
|
597
|
+
if public_ip:
|
|
598
|
+
try:
|
|
599
|
+
_ = IPv4Address(public_ip)
|
|
600
|
+
payload["network"]["floating_ip"] = public_ip
|
|
601
|
+
except ValueError:
|
|
602
|
+
sys.exit(f"Error: '{public_ip}' is not valid IPv4 address.")
|
|
603
|
+
else:
|
|
604
|
+
# New public IPv4 address will be automatically requested with
|
|
605
|
+
# correct availability zone. This is official dirty hack.
|
|
606
|
+
if no_public_ip is False:
|
|
607
|
+
payload["network"]["floating_ip"] = "create_ip"
|
|
564
608
|
|
|
565
609
|
# Set server configuration parameters
|
|
566
610
|
if preset_id and (cpu or ram or disk):
|
|
@@ -609,6 +653,10 @@ def server_create(
|
|
|
609
653
|
ssh_keys_ids.append(process_ssh_key(client, key))
|
|
610
654
|
payload["ssh_keys_ids"] = ssh_keys_ids
|
|
611
655
|
|
|
656
|
+
# Set cloud-init user-data
|
|
657
|
+
if user_data:
|
|
658
|
+
payload["cloud_init"] = user_data.read()
|
|
659
|
+
|
|
612
660
|
# Check project_id before creating server
|
|
613
661
|
if project_id:
|
|
614
662
|
if not project_id in [
|
|
@@ -738,12 +786,6 @@ def server_resize(
|
|
|
738
786
|
# Return error if user tries to switch from preset to configurator in
|
|
739
787
|
# location where configurator is unavailable.
|
|
740
788
|
if cpu or ram or disk:
|
|
741
|
-
if old_state["location"] not in REGIONS_WITH_CONFIGURATOR:
|
|
742
|
-
sys.exit(
|
|
743
|
-
"Error: Can not change configuration in location "
|
|
744
|
-
+ f"'{old_state['location']}'. Change preset_id instead."
|
|
745
|
-
)
|
|
746
|
-
|
|
747
789
|
# Get original server configurator_id
|
|
748
790
|
configurator_id = old_state["configurator_id"]
|
|
749
791
|
configurator = None
|
|
@@ -775,6 +817,7 @@ def server_resize(
|
|
|
775
817
|
payload["configuration"] = {}
|
|
776
818
|
|
|
777
819
|
# Get original size of primary disk
|
|
820
|
+
primary_disk_size = 0
|
|
778
821
|
for old_disk in old_state["disks"]:
|
|
779
822
|
if old_disk["is_system"]: # is True
|
|
780
823
|
primary_disk_size = old_disk["size"]
|
|
@@ -963,13 +1006,19 @@ def server_remove(
|
|
|
963
1006
|
config: Optional[Path] = config_option,
|
|
964
1007
|
profile: Optional[str] = profile_option,
|
|
965
1008
|
yes: Optional[bool] = yes_option,
|
|
1009
|
+
keep_public_ip: Optional[bool] = typer.Option(
|
|
1010
|
+
False,
|
|
1011
|
+
"--keep-public-ip",
|
|
1012
|
+
help="Do not remove public IP attached to server. [default: false]",
|
|
1013
|
+
),
|
|
966
1014
|
):
|
|
967
|
-
"""
|
|
1015
|
+
"""Remove Cloud Server."""
|
|
968
1016
|
if not yes:
|
|
969
1017
|
typer.confirm("This action cannot be undone. Continue?", abort=True)
|
|
970
1018
|
|
|
971
1019
|
client = create_client(config, profile)
|
|
972
1020
|
for server_id in server_ids:
|
|
1021
|
+
server_data = client.get_server(server_id).json()["server"]
|
|
973
1022
|
response = client.delete_server(server_id)
|
|
974
1023
|
if response.status_code == 200:
|
|
975
1024
|
del_hash = response.json()["server_delete"]["hash"]
|
|
@@ -981,6 +1030,11 @@ def server_remove(
|
|
|
981
1030
|
print(server_id)
|
|
982
1031
|
else:
|
|
983
1032
|
sys.exit(fmt.printer(response))
|
|
1033
|
+
if keep_public_ip is False:
|
|
1034
|
+
for network in server_data["networks"]:
|
|
1035
|
+
for ip in network["ips"]:
|
|
1036
|
+
if ip.get("id") is not None:
|
|
1037
|
+
client.delete_floating_ip(ip["id"])
|
|
984
1038
|
|
|
985
1039
|
|
|
986
1040
|
# ------------------------------------------------------------- #
|
twc/commands/storage.py
CHANGED
|
@@ -5,7 +5,6 @@ Cloud specific API methods instead. Use third party S3 clients to manage
|
|
|
5
5
|
objects e.g. s3cmd, rclone, etc.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
|
|
9
8
|
import sys
|
|
10
9
|
from logging import debug
|
|
11
10
|
from typing import Optional, List
|
|
@@ -19,7 +18,6 @@ from twc import fmt
|
|
|
19
18
|
from twc.typerx import TyperAlias
|
|
20
19
|
from twc.apiwrap import create_client
|
|
21
20
|
from twc.api import TimewebCloud, ServiceRegion, BucketType
|
|
22
|
-
from twc.vars import S3_ENDPOINT
|
|
23
21
|
from .common import (
|
|
24
22
|
verbose_option,
|
|
25
23
|
config_option,
|
|
@@ -614,10 +612,16 @@ def storage_genconfig(
|
|
|
614
612
|
"rclone": RCLONE_CONFIG_TEMPLATE.strip(),
|
|
615
613
|
}
|
|
616
614
|
|
|
615
|
+
endpoint = "s3.timeweb.cloud"
|
|
616
|
+
if not access_key.isupper():
|
|
617
|
+
# Legacy object storage service have lowercase usernames only.
|
|
618
|
+
# New storage, on the contrary, always has keys in uppercase.
|
|
619
|
+
endpoint = "s3.timeweb.com"
|
|
620
|
+
|
|
617
621
|
file_content = templates[s3_client].format(
|
|
618
622
|
access_key=access_key,
|
|
619
623
|
secret_key=secret_key,
|
|
620
|
-
endpoint=
|
|
624
|
+
endpoint=endpoint,
|
|
621
625
|
)
|
|
622
626
|
|
|
623
627
|
if save_to:
|
twc/commands/vpc.py
CHANGED
|
@@ -13,9 +13,10 @@ import typer
|
|
|
13
13
|
from requests import Response
|
|
14
14
|
|
|
15
15
|
from twc import fmt
|
|
16
|
+
from twc.api import ServiceRegion
|
|
16
17
|
from twc.typerx import TyperAlias
|
|
17
18
|
from twc.apiwrap import create_client
|
|
18
|
-
from twc.vars import REGIONS_WITH_LAN
|
|
19
|
+
from twc.vars import REGIONS_WITH_LAN, ZONES_WITH_LAN
|
|
19
20
|
from .common import (
|
|
20
21
|
verbose_option,
|
|
21
22
|
config_option,
|
|
@@ -24,6 +25,7 @@ from .common import (
|
|
|
24
25
|
output_format_option,
|
|
25
26
|
filter_option,
|
|
26
27
|
region_option,
|
|
28
|
+
zone_option,
|
|
27
29
|
)
|
|
28
30
|
|
|
29
31
|
|
|
@@ -31,9 +33,11 @@ vpc = TyperAlias(help=__doc__)
|
|
|
31
33
|
vpc_port = TyperAlias(help="Manage network ports.")
|
|
32
34
|
vpc.add_typer(vpc_port, name="port", aliases=["ports"])
|
|
33
35
|
|
|
34
|
-
ALLOWED_SUBNETS = [
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
ALLOWED_SUBNETS = [
|
|
37
|
+
IPv4Network("10.0.0.0/8"),
|
|
38
|
+
IPv4Network("192.168.0.0/16"),
|
|
39
|
+
IPv4Network("172.16.0.0/12"),
|
|
40
|
+
]
|
|
37
41
|
|
|
38
42
|
|
|
39
43
|
# ------------------------------------------------------------- #
|
|
@@ -46,12 +50,13 @@ def print_networks(response: Response, filters: Optional[str] = None):
|
|
|
46
50
|
if filters:
|
|
47
51
|
nets = fmt.filter_list(nets, filters)
|
|
48
52
|
table = fmt.Table()
|
|
49
|
-
table.header(["ID", "REGION", "SUBNET"])
|
|
53
|
+
table.header(["ID", "REGION", "ZONE", "SUBNET"])
|
|
50
54
|
for net in nets:
|
|
51
55
|
table.row(
|
|
52
56
|
[
|
|
53
57
|
net["id"],
|
|
54
58
|
net["location"],
|
|
59
|
+
net["availability_zone"],
|
|
55
60
|
net["subnet_v4"],
|
|
56
61
|
]
|
|
57
62
|
)
|
|
@@ -99,8 +104,6 @@ def validate_network(value):
|
|
|
99
104
|
f"Error: Network {value} is not subnet of: "
|
|
100
105
|
f"{[n.with_prefixlen for n in ALLOWED_SUBNETS]}"
|
|
101
106
|
)
|
|
102
|
-
if network.prefixlen in range(MIN_PREFIXLEN, MAX_PREFIXLEN + 1):
|
|
103
|
-
sys.exit("Error: Minimum network prefix is 32, maximum is 16.")
|
|
104
107
|
return value
|
|
105
108
|
|
|
106
109
|
|
|
@@ -108,7 +111,7 @@ def validate_network(value):
|
|
|
108
111
|
def vpc_create(
|
|
109
112
|
subnet: str = typer.Argument(
|
|
110
113
|
...,
|
|
111
|
-
metavar="
|
|
114
|
+
metavar="NETWORK_SUBNET",
|
|
112
115
|
callback=validate_network,
|
|
113
116
|
help="IPv4 network CIDR.",
|
|
114
117
|
),
|
|
@@ -119,13 +122,23 @@ def vpc_create(
|
|
|
119
122
|
name: str = typer.Option(None, help="Network display name."),
|
|
120
123
|
desc: Optional[str] = typer.Option(None, help="Description."),
|
|
121
124
|
region: Optional[str] = region_option,
|
|
125
|
+
availability_zone: Optional[str] = zone_option,
|
|
122
126
|
):
|
|
123
127
|
"""Create network."""
|
|
124
128
|
client = create_client(config, profile)
|
|
125
129
|
if region not in REGIONS_WITH_LAN:
|
|
126
130
|
sys.exit(
|
|
127
131
|
f"Error: Cannot create network in location '{region}'. "
|
|
128
|
-
f"Available regions
|
|
132
|
+
f"Available regions are: {REGIONS_WITH_LAN}"
|
|
133
|
+
)
|
|
134
|
+
usable_zones = set(ZONES_WITH_LAN).intersection(
|
|
135
|
+
set(ServiceRegion.get_zones(region))
|
|
136
|
+
)
|
|
137
|
+
if availability_zone is not None and availability_zone not in usable_zones:
|
|
138
|
+
sys.exit(
|
|
139
|
+
f"Error: Cannot create network in region '{region}' with "
|
|
140
|
+
f"availability zone '{availability_zone}'. "
|
|
141
|
+
f"Usable zones are: {list(usable_zones)}"
|
|
129
142
|
)
|
|
130
143
|
if not name:
|
|
131
144
|
name = subnet
|
|
@@ -134,6 +147,7 @@ def vpc_create(
|
|
|
134
147
|
description=desc,
|
|
135
148
|
subnet=subnet,
|
|
136
149
|
location=region,
|
|
150
|
+
availability_zone=availability_zone,
|
|
137
151
|
)
|
|
138
152
|
fmt.printer(
|
|
139
153
|
response,
|