helper-cli 0.1.1__py3-none-any.whl → 0.1.11__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.
- helper/commands/__init__.py +1 -0
- helper/commands/arch.py +8 -0
- helper/commands/docker.py +609 -0
- helper/commands/internal_ip.py +27 -0
- helper/commands/nixos.py +88 -0
- helper/commands/public_ip.py +8 -0
- helper/main.py +92 -46
- helper/utils.py +12 -0
- helper_cli-0.1.11.dist-info/METADATA +6 -0
- helper_cli-0.1.11.dist-info/RECORD +13 -0
- helper_cli-0.1.1.dist-info/METADATA +0 -4
- helper_cli-0.1.1.dist-info/RECORD +0 -6
- {helper_cli-0.1.1.dist-info → helper_cli-0.1.11.dist-info}/WHEEL +0 -0
- {helper_cli-0.1.1.dist-info → helper_cli-0.1.11.dist-info}/entry_points.txt +0 -0
- {helper_cli-0.1.1.dist-info → helper_cli-0.1.11.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Initialize commands package
|
helper/commands/arch.py
ADDED
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import subprocess
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Dict, List, Tuple, Optional, Any
|
|
8
|
+
|
|
9
|
+
# Configure logging
|
|
10
|
+
logging.basicConfig(
|
|
11
|
+
level=logging.WARNING,
|
|
12
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
13
|
+
stream=sys.stderr
|
|
14
|
+
)
|
|
15
|
+
logger = logging.getLogger('docker-helper')
|
|
16
|
+
|
|
17
|
+
class Verbosity:
|
|
18
|
+
"""Handle verbosity levels for logging."""
|
|
19
|
+
def __init__(self, verbosity: int = 0):
|
|
20
|
+
self.verbosity = verbosity
|
|
21
|
+
self.set_level()
|
|
22
|
+
|
|
23
|
+
def set_level(self):
|
|
24
|
+
"""Set logging level based on verbosity."""
|
|
25
|
+
if self.verbosity >= 3:
|
|
26
|
+
logger.setLevel(logging.DEBUG)
|
|
27
|
+
elif self.verbosity == 2:
|
|
28
|
+
logger.setLevel(logging.INFO)
|
|
29
|
+
elif self.verbosity == 1:
|
|
30
|
+
logger.setLevel(logging.WARNING)
|
|
31
|
+
else:
|
|
32
|
+
logger.setLevel(logging.ERROR)
|
|
33
|
+
|
|
34
|
+
def debug(self, msg: str, *args, **kwargs):
|
|
35
|
+
"""Log debug message if verbosity >= 3."""
|
|
36
|
+
if self.verbosity >= 3:
|
|
37
|
+
logger.debug(msg, *args, **kwargs)
|
|
38
|
+
|
|
39
|
+
def info(self, msg: str, *args, **kwargs):
|
|
40
|
+
"""Log info message if verbosity >= 2."""
|
|
41
|
+
if self.verbosity >= 2:
|
|
42
|
+
logger.info(msg, *args, **kwargs)
|
|
43
|
+
|
|
44
|
+
def warning(self, msg: str, *args, **kwargs):
|
|
45
|
+
"""Log warning message if verbosity >= 1."""
|
|
46
|
+
if self.verbosity >= 1:
|
|
47
|
+
logger.warning(msg, *args, **kwargs)
|
|
48
|
+
|
|
49
|
+
def error(self, msg: str, *args, **kwargs):
|
|
50
|
+
"""Log error message regardless of verbosity."""
|
|
51
|
+
logger.error(msg, *args, **kwargs)
|
|
52
|
+
|
|
53
|
+
def get_container_ports(container_id: str, verbosity: Verbosity) -> List[Dict]:
|
|
54
|
+
"""Get exposed ports and IPs for a container."""
|
|
55
|
+
verbosity.debug(f"Getting ports for container {container_id}")
|
|
56
|
+
try:
|
|
57
|
+
result = subprocess.run(
|
|
58
|
+
['docker', 'inspect', '--format',
|
|
59
|
+
'{{range $p, $conf := .NetworkSettings.Ports}}' # noqa: E501
|
|
60
|
+
'{{range $h, $hosts := $conf}}'
|
|
61
|
+
'{{$p}}|{{$hosts.HostIp}}|{{$hosts.HostPort}};'
|
|
62
|
+
'{{end}}{{end}}',
|
|
63
|
+
container_id],
|
|
64
|
+
capture_output=True,
|
|
65
|
+
text=True
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if result.returncode != 0:
|
|
69
|
+
verbosity.error(f"Failed to get container info: {result.stderr}")
|
|
70
|
+
return []
|
|
71
|
+
|
|
72
|
+
ports = []
|
|
73
|
+
raw_output = result.stdout.strip()
|
|
74
|
+
verbosity.debug(f"Raw port mappings: {raw_output}")
|
|
75
|
+
|
|
76
|
+
if raw_output:
|
|
77
|
+
for mapping in raw_output.split(';'):
|
|
78
|
+
if not mapping:
|
|
79
|
+
continue
|
|
80
|
+
try:
|
|
81
|
+
container_port, host_ip, host_port = mapping.split('|')
|
|
82
|
+
verbosity.debug(f"Processing mapping: container={container_port}, host_ip={host_ip}, host_port={host_port}")
|
|
83
|
+
|
|
84
|
+
if container_port and host_port:
|
|
85
|
+
port_info = {
|
|
86
|
+
'container_port': container_port.split('/')[0], # Remove /tcp or /udp
|
|
87
|
+
'host_ip': host_ip if host_ip not in ('0.0.0.0', '') else 'localhost',
|
|
88
|
+
'host_port': host_port
|
|
89
|
+
}
|
|
90
|
+
verbosity.info(f"Added port mapping: {port_info}")
|
|
91
|
+
ports.append(port_info)
|
|
92
|
+
else:
|
|
93
|
+
verbosity.debug(f"Skipping incomplete mapping: {mapping}")
|
|
94
|
+
except ValueError as e:
|
|
95
|
+
verbosity.warning(f"Failed to parse mapping '{mapping}': {e}")
|
|
96
|
+
continue
|
|
97
|
+
verbosity.debug(f"Final port mappings: {ports}")
|
|
98
|
+
return ports
|
|
99
|
+
except Exception as e:
|
|
100
|
+
verbosity.error(f"Unexpected error in get_container_ports: {str(e)}", exc_info=verbosity.verbosity >= 3)
|
|
101
|
+
return []
|
|
102
|
+
|
|
103
|
+
def check_docker(verbosity: Verbosity) -> bool:
|
|
104
|
+
"""Check if Docker is installed and running."""
|
|
105
|
+
verbosity.info("Checking if Docker is installed and running...")
|
|
106
|
+
try:
|
|
107
|
+
result = subprocess.run(['docker', 'info'],
|
|
108
|
+
capture_output=True,
|
|
109
|
+
text=True)
|
|
110
|
+
|
|
111
|
+
verbosity.debug(f"Docker info command output:\n{result.stdout}")
|
|
112
|
+
|
|
113
|
+
if result.returncode != 0:
|
|
114
|
+
verbosity.error(f"Docker is not running or not accessible. Error: {result.stderr}")
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
verbosity.info("Docker is running and accessible")
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
except FileNotFoundError:
|
|
121
|
+
verbosity.error("Docker command not found. Is Docker installed?")
|
|
122
|
+
return False
|
|
123
|
+
except Exception as e:
|
|
124
|
+
verbosity.error(f"Unexpected error checking Docker: {str(e)}", exc_info=verbosity.verbosity >= 3)
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
def format_output(output, output_format='table'):
|
|
128
|
+
"""Format command output based on the specified format."""
|
|
129
|
+
if output_format == 'json':
|
|
130
|
+
try:
|
|
131
|
+
return json.dumps(json.loads(output), indent=2)
|
|
132
|
+
except json.JSONDecodeError:
|
|
133
|
+
return output
|
|
134
|
+
return output
|
|
135
|
+
|
|
136
|
+
def get_verbosity(ctx: click.Context) -> Verbosity:
|
|
137
|
+
"""Get verbosity level from context."""
|
|
138
|
+
# Count the number of 'v's in the --verbose flag
|
|
139
|
+
verbose = ctx.params.get('verbose', 0)
|
|
140
|
+
verbosity = Verbosity(verbosity=verbose)
|
|
141
|
+
verbosity.info(f"Verbosity level set to {verbose}")
|
|
142
|
+
return verbosity
|
|
143
|
+
|
|
144
|
+
@click.group()
|
|
145
|
+
@click.option('-v', '--verbose', count=True, help='Increase verbosity (can be used multiple times)')
|
|
146
|
+
@click.pass_context
|
|
147
|
+
def docker(ctx, verbose):
|
|
148
|
+
"""Docker management commands."""
|
|
149
|
+
ctx.ensure_object(dict)
|
|
150
|
+
|
|
151
|
+
# Get verbosity from parent context if it exists, otherwise use the flag value
|
|
152
|
+
parent_verbosity = ctx.obj.get('verbosity', 0) if hasattr(ctx, 'obj') else 0
|
|
153
|
+
verbosity_level = max(verbose, parent_verbosity)
|
|
154
|
+
|
|
155
|
+
# Initialize verbosity
|
|
156
|
+
verbosity = Verbosity(verbosity=verbosity_level)
|
|
157
|
+
ctx.obj['verbosity'] = verbosity
|
|
158
|
+
|
|
159
|
+
# Set up logging
|
|
160
|
+
logger = logging.getLogger('docker-helper')
|
|
161
|
+
if verbosity_level >= 3:
|
|
162
|
+
logger.setLevel(logging.DEBUG)
|
|
163
|
+
elif verbosity_level == 2:
|
|
164
|
+
logger.setLevel(logging.INFO)
|
|
165
|
+
elif verbosity_level == 1:
|
|
166
|
+
logger.setLevel(logging.WARNING)
|
|
167
|
+
else:
|
|
168
|
+
logger.setLevel(logging.ERROR)
|
|
169
|
+
|
|
170
|
+
logger.debug(f"Docker command group initialized with verbosity level: {verbosity_level}")
|
|
171
|
+
|
|
172
|
+
verbosity.debug("Initializing Docker command group")
|
|
173
|
+
if not check_docker(verbosity):
|
|
174
|
+
click.echo("Error: Docker is not installed or not running. Please start Docker and try again.", err=True)
|
|
175
|
+
ctx.exit(1)
|
|
176
|
+
|
|
177
|
+
@docker.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
|
|
178
|
+
@click.option('--all', '-a', is_flag=True, help='Show all containers (default shows just running)')
|
|
179
|
+
@click.option('--format', type=click.Choice(['table', 'json'], case_sensitive=False),
|
|
180
|
+
default='table', help='Output format')
|
|
181
|
+
@click.pass_context
|
|
182
|
+
def ps(ctx, all, format):
|
|
183
|
+
"""List containers."""
|
|
184
|
+
verbosity = ctx.obj['verbosity']
|
|
185
|
+
cmd = ['docker', 'ps']
|
|
186
|
+
if all:
|
|
187
|
+
cmd.append('-a')
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
verbosity.debug(f"Running command: {' '.join(cmd)}")
|
|
191
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
192
|
+
|
|
193
|
+
if result.returncode == 0:
|
|
194
|
+
if format == 'json':
|
|
195
|
+
# Try to parse and pretty-print JSON output
|
|
196
|
+
try:
|
|
197
|
+
data = json.loads(result.stdout)
|
|
198
|
+
click.echo(json.dumps(data, indent=2))
|
|
199
|
+
except json.JSONDecodeError:
|
|
200
|
+
# Fall back to raw output if not valid JSON
|
|
201
|
+
click.echo(result.stdout)
|
|
202
|
+
else:
|
|
203
|
+
# For table format, try to align columns
|
|
204
|
+
lines = result.stdout.strip().split('\n')
|
|
205
|
+
if len(lines) > 1:
|
|
206
|
+
# Parse as JSON to handle special characters in values
|
|
207
|
+
try:
|
|
208
|
+
data = [json.loads(line) for line in lines[1:]]
|
|
209
|
+
headers = data[0].keys()
|
|
210
|
+
rows = [[item.get(header, '') for header in headers] for item in data]
|
|
211
|
+
|
|
212
|
+
# Calculate column widths
|
|
213
|
+
col_widths = [max(len(str(header)),
|
|
214
|
+
max((len(str(row[i])) for row in rows), default=0))
|
|
215
|
+
for i, header in enumerate(headers)]
|
|
216
|
+
|
|
217
|
+
# Print header
|
|
218
|
+
header_row = " ".join(header.ljust(width) for header, width in zip(headers, col_widths))
|
|
219
|
+
click.echo(header_row)
|
|
220
|
+
click.echo("-" * len(header_row))
|
|
221
|
+
|
|
222
|
+
# Print rows
|
|
223
|
+
for row in rows:
|
|
224
|
+
click.echo(" ".join(str(cell).ljust(width) for cell, width in zip(row, col_widths)))
|
|
225
|
+
except Exception as e:
|
|
226
|
+
verbosity.debug(f"Error formatting table: {str(e)}")
|
|
227
|
+
# Fall back to raw output if processing fails
|
|
228
|
+
click.echo(result.stdout)
|
|
229
|
+
else:
|
|
230
|
+
click.echo(result.stdout)
|
|
231
|
+
else:
|
|
232
|
+
error_msg = f"Error: {result.stderr}"
|
|
233
|
+
verbosity.error(error_msg)
|
|
234
|
+
click.echo(error_msg, err=True)
|
|
235
|
+
except subprocess.CalledProcessError as e:
|
|
236
|
+
error_msg = f"Command failed: {str(e)}"
|
|
237
|
+
verbosity.error(error_msg)
|
|
238
|
+
click.echo(error_msg, err=True)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
error_msg = f"Unexpected error: {str(e)}"
|
|
241
|
+
verbosity.error(error_msg)
|
|
242
|
+
click.echo(error_msg, err=True)
|
|
243
|
+
|
|
244
|
+
@docker.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
|
|
245
|
+
@click.argument('image', required=False)
|
|
246
|
+
@click.option('--name', help='Assign a name to the container')
|
|
247
|
+
@click.option('--port', '-p', multiple=True, help='Publish a container\'s port(s) to the host')
|
|
248
|
+
@click.option('--detach', '-d', is_flag=True, help='Run container in background and print container ID')
|
|
249
|
+
@click.option('--env', '-e', multiple=True, help='Set environment variables')
|
|
250
|
+
@click.option('--volume', '-v', multiple=True, help='Bind mount a volume')
|
|
251
|
+
@click.pass_context
|
|
252
|
+
def run(ctx, image, name, port, detach, env, volume):
|
|
253
|
+
"""Run a command in a new container."""
|
|
254
|
+
verbosity = ctx.obj['verbosity']
|
|
255
|
+
cmd = ['docker', 'run']
|
|
256
|
+
|
|
257
|
+
if name:
|
|
258
|
+
cmd.extend(['--name', name])
|
|
259
|
+
verbosity.debug(f"Setting container name: {name}")
|
|
260
|
+
|
|
261
|
+
for p in port:
|
|
262
|
+
cmd.extend(['-p', p])
|
|
263
|
+
verbosity.debug(f"Adding port mapping: {p}")
|
|
264
|
+
|
|
265
|
+
if detach:
|
|
266
|
+
cmd.append('-d')
|
|
267
|
+
verbosity.debug("Running container in detached mode")
|
|
268
|
+
|
|
269
|
+
for e in env:
|
|
270
|
+
cmd.extend(['-e', e])
|
|
271
|
+
verbosity.debug(f"Setting environment variable: {e}")
|
|
272
|
+
|
|
273
|
+
for v in volume:
|
|
274
|
+
cmd.extend(['-v', v])
|
|
275
|
+
verbosity.debug(f"Mounting volume: {v}")
|
|
276
|
+
|
|
277
|
+
if image:
|
|
278
|
+
cmd.append(image)
|
|
279
|
+
verbosity.debug(f"Using image: {image}")
|
|
280
|
+
|
|
281
|
+
# Add any remaining arguments
|
|
282
|
+
if hasattr(ctx, 'args') and ctx.args:
|
|
283
|
+
cmd.extend(ctx.args)
|
|
284
|
+
verbosity.debug(f"Additional arguments: {' '.join(ctx.args)}")
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
verbosity.debug(f"Running command: {' '.join(cmd)}")
|
|
288
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
289
|
+
|
|
290
|
+
if result.returncode == 0:
|
|
291
|
+
if result.stdout:
|
|
292
|
+
click.echo(result.stdout.strip())
|
|
293
|
+
verbosity.info("Container started successfully")
|
|
294
|
+
else:
|
|
295
|
+
error_msg = f"Error: {result.stderr.strip() or 'Unknown error'}"
|
|
296
|
+
verbosity.error(error_msg)
|
|
297
|
+
click.echo(error_msg, err=True)
|
|
298
|
+
ctx.exit(1)
|
|
299
|
+
|
|
300
|
+
except Exception as e:
|
|
301
|
+
error_msg = f"Failed to run container: {str(e)}"
|
|
302
|
+
verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
|
|
303
|
+
click.echo(error_msg, err=True)
|
|
304
|
+
ctx.exit(1)
|
|
305
|
+
|
|
306
|
+
@docker.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
|
|
307
|
+
@click.argument('containers', nargs=-1, required=False)
|
|
308
|
+
@click.option('--force', '-f', is_flag=True, help='Force the removal of a running container (uses SIGKILL)')
|
|
309
|
+
@click.option('--volumes', '-v', is_flag=True, help='Remove anonymous volumes associated with the container')
|
|
310
|
+
@click.pass_context
|
|
311
|
+
def rm(ctx, containers, force, volumes):
|
|
312
|
+
"""Remove one or more containers."""
|
|
313
|
+
verbosity = ctx.obj['verbosity']
|
|
314
|
+
cmd = ['docker', 'rm']
|
|
315
|
+
|
|
316
|
+
if force:
|
|
317
|
+
cmd.append('-f')
|
|
318
|
+
verbosity.debug("Force removal enabled")
|
|
319
|
+
if volumes:
|
|
320
|
+
cmd.append('-v')
|
|
321
|
+
verbosity.debug("Volume removal enabled")
|
|
322
|
+
|
|
323
|
+
# Get containers from both the containers argument and any remaining args
|
|
324
|
+
all_containers = list(containers)
|
|
325
|
+
if hasattr(ctx, 'args') and ctx.args:
|
|
326
|
+
all_containers.extend(ctx.args)
|
|
327
|
+
|
|
328
|
+
if not all_containers:
|
|
329
|
+
error_msg = "Error: You must specify at least one container"
|
|
330
|
+
verbosity.error(error_msg)
|
|
331
|
+
click.echo(error_msg, err=True)
|
|
332
|
+
ctx.exit(1)
|
|
333
|
+
|
|
334
|
+
cmd.extend(all_containers)
|
|
335
|
+
verbosity.debug(f"Removing containers: {', '.join(all_containers)}")
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
verbosity.debug(f"Running command: {' '.join(cmd)}")
|
|
339
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
340
|
+
|
|
341
|
+
if result.returncode == 0:
|
|
342
|
+
if result.stdout.strip():
|
|
343
|
+
click.echo(result.stdout.strip())
|
|
344
|
+
verbosity.info(f"Successfully removed {len(all_containers)} container(s)")
|
|
345
|
+
else:
|
|
346
|
+
error_msg = f"Error: {result.stderr.strip() or 'Unknown error'}"
|
|
347
|
+
verbosity.error(error_msg)
|
|
348
|
+
click.echo(error_msg, err=True)
|
|
349
|
+
ctx.exit(1)
|
|
350
|
+
|
|
351
|
+
except Exception as e:
|
|
352
|
+
error_msg = f"Failed to remove containers: {str(e)}"
|
|
353
|
+
verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
|
|
354
|
+
click.echo(error_msg, err=True)
|
|
355
|
+
ctx.exit(1)
|
|
356
|
+
|
|
357
|
+
@docker.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
|
|
358
|
+
@click.option('--show-all', '-a', is_flag=True, help='Show all containers (default shows just running)')
|
|
359
|
+
@click.option('--http-only', is_flag=True, help='Show only containers with HTTP/HTTPS ports')
|
|
360
|
+
@click.pass_context
|
|
361
|
+
def url(ctx, show_all, http_only):
|
|
362
|
+
"""Show containers with their HTTP/HTTPS URLs."""
|
|
363
|
+
verbosity = ctx.obj['verbosity']
|
|
364
|
+
verbosity.info(f"Starting url command with show_all={show_all}, http_only={http_only}")
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
# Get all containers
|
|
368
|
+
cmd = ['docker', 'ps', '--format', '{{.ID}}|{{.Names}}|{{.Status}}|{{.Ports}}']
|
|
369
|
+
if show_all:
|
|
370
|
+
cmd.append('-a')
|
|
371
|
+
|
|
372
|
+
verbosity.debug(f"Running command: {' '.join(cmd)}")
|
|
373
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
374
|
+
|
|
375
|
+
if result.returncode != 0:
|
|
376
|
+
error_msg = f"Error listing containers: {result.stderr}"
|
|
377
|
+
verbosity.error(error_msg)
|
|
378
|
+
click.echo(error_msg, err=True)
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
verbosity.debug(f"Command output: {result.stdout}")
|
|
382
|
+
|
|
383
|
+
running_containers = []
|
|
384
|
+
stopped_containers = []
|
|
385
|
+
|
|
386
|
+
container_lines = result.stdout.strip().split('\n')
|
|
387
|
+
verbosity.info(f"Found {len(container_lines)} container(s)")
|
|
388
|
+
|
|
389
|
+
for line in container_lines:
|
|
390
|
+
if not line.strip():
|
|
391
|
+
verbosity.debug("Skipping empty line")
|
|
392
|
+
continue
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
verbosity.debug(f"Processing container line: {line}")
|
|
396
|
+
container_id, name, status, ports = line.split('|', 3)
|
|
397
|
+
is_running = 'Up' in status
|
|
398
|
+
verbosity.info(f"Container: ID={container_id[:12]}, Name={name}, Status={status}, Running={is_running}")
|
|
399
|
+
|
|
400
|
+
# Get container details
|
|
401
|
+
container_info = {
|
|
402
|
+
'id': container_id[:12], # Short ID
|
|
403
|
+
'name': name,
|
|
404
|
+
'status': status,
|
|
405
|
+
'urls': []
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
# Get exposed ports and their mappings
|
|
409
|
+
port_mappings = get_container_ports(container_id, verbosity)
|
|
410
|
+
verbosity.debug(f"Found {len(port_mappings)} port mappings for {name}")
|
|
411
|
+
|
|
412
|
+
for port in port_mappings:
|
|
413
|
+
if not port.get('host_port') or not port.get('container_port'):
|
|
414
|
+
verbosity.debug(f"Skipping incomplete port mapping: {port}")
|
|
415
|
+
continue
|
|
416
|
+
|
|
417
|
+
verbosity.debug(f"Checking port mapping: {port}")
|
|
418
|
+
verbosity.debug(f"Container name: {name}, Port: {port['container_port']}")
|
|
419
|
+
|
|
420
|
+
# Handle different port string formats (e.g., '8069/tcp', '0.0.0.0:8080->80/tcp')
|
|
421
|
+
port_str = port['container_port']
|
|
422
|
+
|
|
423
|
+
# Extract port number and protocol
|
|
424
|
+
port_num = None
|
|
425
|
+
protocol = 'tcp' # default protocol
|
|
426
|
+
|
|
427
|
+
# Handle format like '8069/tcp' or '80/http'
|
|
428
|
+
if '/' in port_str:
|
|
429
|
+
port_num, protocol = port_str.split('/', 1)
|
|
430
|
+
# Handle format like '0.0.0.0:8080->80/tcp'
|
|
431
|
+
elif '->' in port_str:
|
|
432
|
+
_, port_mapping = port_str.split('->', 1)
|
|
433
|
+
if '/' in port_mapping:
|
|
434
|
+
port_num, protocol = port_mapping.split('/', 1)
|
|
435
|
+
else:
|
|
436
|
+
port_num = port_mapping
|
|
437
|
+
else:
|
|
438
|
+
port_num = port_str
|
|
439
|
+
|
|
440
|
+
# Clean up port number (remove any non-numeric characters)
|
|
441
|
+
port_num = ''.join(c for c in port_num if c.isdigit())
|
|
442
|
+
|
|
443
|
+
# Map all ports to HTTP URLs
|
|
444
|
+
if port_num: # Process all ports regardless of protocol
|
|
445
|
+
scheme = 'http'
|
|
446
|
+
|
|
447
|
+
# Handle IPv6 addresses (add brackets if needed)
|
|
448
|
+
host = port['host_ip']
|
|
449
|
+
if ':' in host and not host.startswith('['):
|
|
450
|
+
host = f'[{host}]'
|
|
451
|
+
|
|
452
|
+
url = f"{scheme}://{host}:{port['host_port']}"
|
|
453
|
+
|
|
454
|
+
container_info['urls'].append({
|
|
455
|
+
'url': url,
|
|
456
|
+
'port': port_num,
|
|
457
|
+
'protocol': protocol
|
|
458
|
+
})
|
|
459
|
+
verbosity.info(f"Added URL for {name}: {url} (port {port_num}/{protocol})")
|
|
460
|
+
verbosity.info(f"Added URL for {name}: {url} (port {port_num})")
|
|
461
|
+
|
|
462
|
+
# If http_only is True and no HTTP URLs, skip this container
|
|
463
|
+
if http_only and not container_info['urls']:
|
|
464
|
+
continue
|
|
465
|
+
|
|
466
|
+
if is_running:
|
|
467
|
+
running_containers.append(container_info)
|
|
468
|
+
else:
|
|
469
|
+
stopped_containers.append(container_info)
|
|
470
|
+
|
|
471
|
+
except Exception as e:
|
|
472
|
+
click.echo(f"Error processing container info: {e}", err=True)
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
# Display running containers
|
|
476
|
+
if running_containers:
|
|
477
|
+
click.secho("\n🚀 Running Containers:", fg='green', bold=True)
|
|
478
|
+
for container in running_containers:
|
|
479
|
+
verbosity.debug(f"Displaying running container: {container['name']}")
|
|
480
|
+
click.echo(f"\n{click.style('●', fg='green')} {click.style(container['name'], bold=True)} ({container['id']})")
|
|
481
|
+
|
|
482
|
+
if container['urls']:
|
|
483
|
+
verbosity.info(f"Found {len(container['urls'])} URLs for {container['name']}")
|
|
484
|
+
for url_info in container['urls']:
|
|
485
|
+
verbosity.debug(f"Displaying URL: {url_info['url']}")
|
|
486
|
+
click.echo(f" {click.style('→', fg='blue')} {click.style(url_info['url'], fg='blue', underline=True)}")
|
|
487
|
+
else:
|
|
488
|
+
verbosity.debug(f"No URLs found for {container['name']}")
|
|
489
|
+
|
|
490
|
+
# Display stopped containers
|
|
491
|
+
if stopped_containers and (show_all or not http_only):
|
|
492
|
+
click.secho("\n⏸️ Stopped Containers:", fg='yellow', bold=True)
|
|
493
|
+
for container in stopped_containers:
|
|
494
|
+
verbosity.debug(f"Displaying stopped container: {container['name']}")
|
|
495
|
+
click.echo(f"\n{click.style('●', fg='yellow')} {click.style(container['name'], dim=True)} ({container['id']})")
|
|
496
|
+
|
|
497
|
+
if container['urls']:
|
|
498
|
+
verbosity.info(f"Found {len(container['urls'])} URLs for stopped container {container['name']}")
|
|
499
|
+
for url_info in container['urls']:
|
|
500
|
+
verbosity.debug(f"Displaying URL for stopped container: {url_info['url']}")
|
|
501
|
+
click.echo(f" {click.style('→', fg='blue')} {click.style(url_info['url'], fg='blue', underline=True, dim=True)}")
|
|
502
|
+
else:
|
|
503
|
+
verbosity.debug(f"No URLs found for stopped container {container['name']}")
|
|
504
|
+
|
|
505
|
+
if not running_containers and not stopped_containers:
|
|
506
|
+
msg = "No containers found."
|
|
507
|
+
verbosity.info(msg)
|
|
508
|
+
click.echo(msg)
|
|
509
|
+
else:
|
|
510
|
+
verbosity.info(f"Displayed {len(running_containers)} running and {len(stopped_containers)} stopped containers")
|
|
511
|
+
|
|
512
|
+
except Exception as e:
|
|
513
|
+
error_msg = f"Error in url command: {str(e)}"
|
|
514
|
+
verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
|
|
515
|
+
click.echo(error_msg, err=True)
|
|
516
|
+
|
|
517
|
+
@docker.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
|
|
518
|
+
@click.argument('image', required=False)
|
|
519
|
+
@click.option('--all-tags', '-a', is_flag=True, help='Remove all versions of the image with the given name')
|
|
520
|
+
@click.option('--force', '-f', is_flag=True, help='Force removal of the image')
|
|
521
|
+
@click.option('--no-prune', is_flag=True, help='Do not delete untagged parents')
|
|
522
|
+
@click.pass_context
|
|
523
|
+
def rmi(ctx, image, all_tags, force, no_prune):
|
|
524
|
+
"""Remove one or more images."""
|
|
525
|
+
verbosity = ctx.obj['verbosity']
|
|
526
|
+
cmd = ['docker', 'rmi']
|
|
527
|
+
|
|
528
|
+
if force:
|
|
529
|
+
cmd.append('-f')
|
|
530
|
+
verbosity.debug("Force removal enabled")
|
|
531
|
+
if no_prune:
|
|
532
|
+
cmd.append('--no-prune')
|
|
533
|
+
verbosity.debug("Pruning of untagged parents disabled")
|
|
534
|
+
|
|
535
|
+
# Get images from both the image argument and any remaining args
|
|
536
|
+
images = []
|
|
537
|
+
if image:
|
|
538
|
+
images.append(image)
|
|
539
|
+
if hasattr(ctx, 'args') and ctx.args:
|
|
540
|
+
images.extend(ctx.args)
|
|
541
|
+
|
|
542
|
+
if not images and not all_tags:
|
|
543
|
+
error_msg = "Error: You must specify at least one image"
|
|
544
|
+
verbosity.error(error_msg)
|
|
545
|
+
click.echo(error_msg, err=True)
|
|
546
|
+
ctx.exit(1)
|
|
547
|
+
|
|
548
|
+
if all_tags:
|
|
549
|
+
if not images:
|
|
550
|
+
error_msg = "Error: You must specify an image name when using --all-tags"
|
|
551
|
+
verbosity.error(error_msg)
|
|
552
|
+
click.echo(error_msg, err=True)
|
|
553
|
+
ctx.exit(1)
|
|
554
|
+
|
|
555
|
+
# Get all tags for the specified images
|
|
556
|
+
all_tags_to_remove = []
|
|
557
|
+
for img in images:
|
|
558
|
+
verbosity.debug(f"Finding all tags for image: {img}")
|
|
559
|
+
try:
|
|
560
|
+
result = subprocess.run(
|
|
561
|
+
['docker', 'images', '--format', '{{.Repository}}:{{.Tag}}', img],
|
|
562
|
+
capture_output=True,
|
|
563
|
+
text=True
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
567
|
+
tags = [line for line in result.stdout.split('\n') if line]
|
|
568
|
+
verbosity.debug(f"Found {len(tags)} tags for {img}")
|
|
569
|
+
all_tags_to_remove.extend(tags)
|
|
570
|
+
else:
|
|
571
|
+
verbosity.warning(f"No images found matching '{img}'")
|
|
572
|
+
|
|
573
|
+
except Exception as e:
|
|
574
|
+
verbosity.error(f"Error finding tags for {img}: {str(e)}", exc_info=verbosity.verbosity >= 3)
|
|
575
|
+
continue
|
|
576
|
+
|
|
577
|
+
if not all_tags_to_remove:
|
|
578
|
+
error_msg = "No matching images found to remove"
|
|
579
|
+
verbosity.error(error_msg)
|
|
580
|
+
click.echo(error_msg, err=True)
|
|
581
|
+
ctx.exit(1)
|
|
582
|
+
|
|
583
|
+
cmd.extend(all_tags_to_remove)
|
|
584
|
+
verbosity.info(f"Removing {len(all_tags_to_remove)} image(s) with all tags")
|
|
585
|
+
|
|
586
|
+
else:
|
|
587
|
+
cmd.extend(images)
|
|
588
|
+
verbosity.info(f"Removing {len(images)} image(s)")
|
|
589
|
+
|
|
590
|
+
try:
|
|
591
|
+
verbosity.debug(f"Running command: {' '.join(cmd)}")
|
|
592
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
593
|
+
|
|
594
|
+
if result.returncode == 0:
|
|
595
|
+
if result.stdout.strip():
|
|
596
|
+
click.echo(result.stdout.strip())
|
|
597
|
+
removed_count = len(cmd) - 2 # Subtract 'docker rmi' from the command
|
|
598
|
+
verbosity.info(f"Successfully removed {removed_count} image(s)")
|
|
599
|
+
else:
|
|
600
|
+
error_msg = f"Error: {result.stderr.strip() or 'Unknown error'}"
|
|
601
|
+
verbosity.error(error_msg)
|
|
602
|
+
click.echo(error_msg, err=True)
|
|
603
|
+
ctx.exit(1)
|
|
604
|
+
|
|
605
|
+
except Exception as e:
|
|
606
|
+
error_msg = f"Failed to remove images: {str(e)}"
|
|
607
|
+
verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
|
|
608
|
+
click.echo(error_msg, err=True)
|
|
609
|
+
ctx.exit(1)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import socket
|
|
3
|
+
import shutil
|
|
4
|
+
from ..utils import run_cmd
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
def get_internal_ip():
|
|
8
|
+
"""Get the internal IP address based on the operating system."""
|
|
9
|
+
system = platform.system()
|
|
10
|
+
if system == "Darwin":
|
|
11
|
+
cmd = "ipconfig getifaddr en0"
|
|
12
|
+
elif system == "Linux":
|
|
13
|
+
if shutil.which("ifconfig"):
|
|
14
|
+
cmd = "ifconfig | grep 'inet ' | grep -v 192.168.1.1 | awk '{print $2}' | head -n1"
|
|
15
|
+
else:
|
|
16
|
+
cmd = "hostname -I | awk '{print $1}'"
|
|
17
|
+
else:
|
|
18
|
+
ip = socket.gethostbyname(socket.gethostname())
|
|
19
|
+
print(f"$ python socket.gethostbyname(socket.gethostname())")
|
|
20
|
+
print(ip)
|
|
21
|
+
return
|
|
22
|
+
return run_cmd(cmd)
|
|
23
|
+
|
|
24
|
+
@click.command()
|
|
25
|
+
def internal_ip():
|
|
26
|
+
"""Show local/internal IP"""
|
|
27
|
+
get_internal_ip()
|
helper/commands/nixos.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import platform
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
def check_nixos():
|
|
6
|
+
"""Check if running on NixOS."""
|
|
7
|
+
try:
|
|
8
|
+
with open('/etc/os-release') as f:
|
|
9
|
+
return 'NixOS' in f.read()
|
|
10
|
+
except FileNotFoundError:
|
|
11
|
+
return False
|
|
12
|
+
except Exception as e:
|
|
13
|
+
click.echo(f"Warning: Could not check if running on NixOS: {e}", err=True)
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
def get_nixos_version():
|
|
17
|
+
"""Get NixOS version information."""
|
|
18
|
+
if not check_nixos():
|
|
19
|
+
return "Not running NixOS"
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
result = subprocess.run(['nixos-version'],
|
|
23
|
+
capture_output=True,
|
|
24
|
+
text=True)
|
|
25
|
+
if result.returncode == 0:
|
|
26
|
+
return result.stdout.strip()
|
|
27
|
+
return "NixOS version could not be determined"
|
|
28
|
+
except Exception as e:
|
|
29
|
+
return f"Error getting NixOS version: {str(e)}"
|
|
30
|
+
|
|
31
|
+
@click.group()
|
|
32
|
+
def nixos():
|
|
33
|
+
"""NixOS related commands."""
|
|
34
|
+
if not check_nixos():
|
|
35
|
+
click.echo("Warning: Not running on NixOS. Some commands may not work as expected.", err=True)
|
|
36
|
+
|
|
37
|
+
@nixos.command()
|
|
38
|
+
def version():
|
|
39
|
+
"""Show NixOS version."""
|
|
40
|
+
click.echo(get_nixos_version())
|
|
41
|
+
|
|
42
|
+
@nixos.command()
|
|
43
|
+
@click.argument('package', required=False)
|
|
44
|
+
def search(package):
|
|
45
|
+
"""Search for Nix packages."""
|
|
46
|
+
if not package:
|
|
47
|
+
click.echo("Please specify a package to search for")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
result = subprocess.run(['nix-env', '-qa', package],
|
|
52
|
+
capture_output=True,
|
|
53
|
+
text=True)
|
|
54
|
+
if result.returncode == 0:
|
|
55
|
+
click.echo(result.stdout)
|
|
56
|
+
else:
|
|
57
|
+
click.echo(f"Error searching for package: {result.stderr}", err=True)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
click.echo(f"Error: {str(e)}", err=True)
|
|
60
|
+
|
|
61
|
+
@nixos.command()
|
|
62
|
+
@click.option('-f', '--force', is_flag=True, help='Force garbage collection and remove all old generations')
|
|
63
|
+
def clean(force):
|
|
64
|
+
"""Clean Nix store and perform garbage collection."""
|
|
65
|
+
if not check_nixos():
|
|
66
|
+
click.echo("Error: This command can only be run on NixOS", err=True)
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
click.echo("Running Nix garbage collection...")
|
|
71
|
+
if force:
|
|
72
|
+
click.echo("Forcing garbage collection and removing all old generations...")
|
|
73
|
+
# Remove all old generations of all profiles
|
|
74
|
+
subprocess.run(['nix-collect-garbage', '-d'], check=True)
|
|
75
|
+
click.echo("✓ Removed all old generations and ran garbage collection")
|
|
76
|
+
else:
|
|
77
|
+
# Regular garbage collection (safe, only removes unreachable paths)
|
|
78
|
+
subprocess.run(['nix-collect-garbage'], check=True)
|
|
79
|
+
click.echo("✓ Garbage collection completed")
|
|
80
|
+
|
|
81
|
+
# Show disk space usage after cleanup
|
|
82
|
+
click.echo("\nDisk space usage after cleanup:")
|
|
83
|
+
subprocess.run(['nix-store', '--query', '--disk-usage', '/nix/store'])
|
|
84
|
+
|
|
85
|
+
except subprocess.CalledProcessError as e:
|
|
86
|
+
click.echo(f"Error during cleanup: {e}", err=True)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
click.echo(f"Unexpected error: {str(e)}", err=True)
|
helper/main.py
CHANGED
|
@@ -1,63 +1,109 @@
|
|
|
1
1
|
import click
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import shutil
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
from .commands import internal_ip, public_ip, arch, nixos, docker
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
class VerbosityCommand(click.Command):
|
|
7
|
+
def parse_args(self, ctx, args):
|
|
8
|
+
# Initialize verbosity from context if it exists
|
|
9
|
+
ctx.ensure_object(dict)
|
|
10
|
+
verbose = ctx.obj.get('verbosity', 0)
|
|
11
|
+
|
|
12
|
+
# Process args for verbosity flags
|
|
13
|
+
new_args = []
|
|
14
|
+
i = 0
|
|
15
|
+
while i < len(args):
|
|
16
|
+
arg = args[i]
|
|
17
|
+
if arg == '--verbose':
|
|
18
|
+
verbose += 1
|
|
19
|
+
elif arg.startswith('-v'):
|
|
20
|
+
verbose += arg.count('v')
|
|
21
|
+
else:
|
|
22
|
+
new_args.append(arg)
|
|
23
|
+
i += 1
|
|
24
|
+
|
|
25
|
+
# Update verbosity in context
|
|
26
|
+
ctx.obj['verbosity'] = verbose
|
|
27
|
+
|
|
28
|
+
# Set up logging
|
|
29
|
+
self._setup_logging(verbose)
|
|
30
|
+
|
|
31
|
+
# Continue with normal argument parsing
|
|
32
|
+
return super().parse_args(ctx, new_args)
|
|
33
|
+
|
|
34
|
+
def _setup_logging(self, verbose):
|
|
35
|
+
logger = logging.getLogger('docker-helper')
|
|
36
|
+
if verbose >= 3:
|
|
37
|
+
logger.setLevel(logging.DEBUG)
|
|
38
|
+
elif verbose == 2:
|
|
39
|
+
logger.setLevel(logging.INFO)
|
|
40
|
+
elif verbose == 1:
|
|
41
|
+
logger.setLevel(logging.WARNING)
|
|
42
|
+
else:
|
|
43
|
+
logger.setLevel(logging.ERROR)
|
|
19
44
|
|
|
20
|
-
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
45
|
+
class VerbosityGroup(click.Group):
|
|
46
|
+
def make_context(self, info_name, args, parent=None, **extra):
|
|
47
|
+
# Pre-process args to find verbosity flags
|
|
48
|
+
verbose = 0
|
|
49
|
+
processed_args = []
|
|
50
|
+
|
|
51
|
+
for arg in args:
|
|
52
|
+
if arg == '--verbose':
|
|
53
|
+
verbose += 1
|
|
54
|
+
elif arg.startswith('-v'):
|
|
55
|
+
verbose += arg.count('v')
|
|
56
|
+
else:
|
|
57
|
+
processed_args.append(arg)
|
|
58
|
+
|
|
59
|
+
# Create context with processed args
|
|
60
|
+
ctx = super().make_context(info_name, processed_args, parent=parent, **extra)
|
|
61
|
+
|
|
62
|
+
# Set verbosity in context
|
|
63
|
+
ctx.ensure_object(dict)
|
|
64
|
+
ctx.obj['verbosity'] = verbose
|
|
65
|
+
|
|
66
|
+
# Set up logging
|
|
67
|
+
logger = logging.getLogger('docker-helper')
|
|
68
|
+
if verbose >= 3:
|
|
69
|
+
logger.setLevel(logging.DEBUG)
|
|
70
|
+
elif verbose == 2:
|
|
71
|
+
logger.setLevel(logging.INFO)
|
|
72
|
+
elif verbose == 1:
|
|
73
|
+
logger.setLevel(logging.WARNING)
|
|
30
74
|
else:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
print(f"$ python socket.gethostbyname(socket.gethostname())")
|
|
35
|
-
print(ip)
|
|
36
|
-
return
|
|
37
|
-
run_cmd(cmd)
|
|
75
|
+
logger.setLevel(logging.ERROR)
|
|
76
|
+
|
|
77
|
+
return ctx
|
|
38
78
|
|
|
39
|
-
@
|
|
40
|
-
def
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
|
|
79
|
+
@click.group(cls=VerbosityGroup)
|
|
80
|
+
def cli():
|
|
81
|
+
"""Helper CLI - quick system info"""
|
|
82
|
+
# Set up basic logging
|
|
83
|
+
logging.basicConfig(
|
|
84
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
85
|
+
level=logging.ERROR
|
|
86
|
+
)
|
|
44
87
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
88
|
+
# Register all commands
|
|
89
|
+
cli.add_command(internal_ip.internal_ip)
|
|
90
|
+
cli.add_command(public_ip.public_ip)
|
|
91
|
+
cli.add_command(arch.arch)
|
|
92
|
+
cli.add_command(nixos.nixos, name="nixos")
|
|
93
|
+
cli.add_command(docker.docker, name="docker")
|
|
50
94
|
|
|
51
95
|
@cli.command()
|
|
52
96
|
@click.pass_context
|
|
53
97
|
def all(ctx):
|
|
54
98
|
"""Show all info"""
|
|
55
99
|
click.echo("=== Internal IP ===")
|
|
56
|
-
ctx.invoke(internal_ip)
|
|
100
|
+
ctx.invoke(internal_ip.internal_ip)
|
|
57
101
|
click.echo("\n=== Public IP ===")
|
|
58
|
-
ctx.invoke(public_ip)
|
|
102
|
+
ctx.invoke(public_ip.public_ip)
|
|
59
103
|
click.echo("\n=== Architecture ===")
|
|
60
|
-
ctx.invoke(arch)
|
|
104
|
+
ctx.invoke(arch.arch)
|
|
105
|
+
click.echo("\n=== NixOS ===")
|
|
106
|
+
ctx.invoke(nixos.nixos, 'version')
|
|
61
107
|
|
|
62
108
|
if __name__ == "__main__":
|
|
63
109
|
cli()
|
helper/utils.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
def run_cmd(cmd):
|
|
4
|
+
"""Run a shell command and print the output."""
|
|
5
|
+
print(f"$ {cmd}")
|
|
6
|
+
try:
|
|
7
|
+
result = subprocess.check_output(cmd, shell=True, text=True).strip()
|
|
8
|
+
print(result)
|
|
9
|
+
return result
|
|
10
|
+
except subprocess.CalledProcessError as e:
|
|
11
|
+
print(f"Error: {e}")
|
|
12
|
+
return None
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
helper/main.py,sha256=39UV2OxIOAgak7ALwGRdK2oeCaaMXQqvXgnrWU77kB4,3247
|
|
2
|
+
helper/utils.py,sha256=JynYacwFA9kgkDuWpGPCj3VxYl_yFJL-FRCKlW1g6Mo,337
|
|
3
|
+
helper/commands/__init__.py,sha256=HFk0MU1U4VqGJUkb9SvCoslqpYSah8KePP9tP8Xzzs4,30
|
|
4
|
+
helper/commands/arch.py,sha256=AREsblUki99P_wVpi44maQd7KA8IlUCJ6ilzJAQODDU,141
|
|
5
|
+
helper/commands/docker.py,sha256=ZmEk3B2xor0p3AQJPkVQLz0biFa1MSsoF-dRxOHDvsI,25349
|
|
6
|
+
helper/commands/internal_ip.py,sha256=ZN7c1HRgB5f-wRk9IwfGQeI3YuZqmPW-UflIpGEImt0,786
|
|
7
|
+
helper/commands/nixos.py,sha256=--Uz2lA-xw6-spp1WBjzzfu4-imFtcziyZuUHZk3Pxs,3113
|
|
8
|
+
helper/commands/public_ip.py,sha256=HS99RDYCaKDZ-AxMQhUwazgR-tT6IGlBb5Qn0f5ITPg,150
|
|
9
|
+
helper_cli-0.1.11.dist-info/METADATA,sha256=6AR44RTeCCTlJvglidOgz754evOnjEhUeWb7yUqvocY,131
|
|
10
|
+
helper_cli-0.1.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
+
helper_cli-0.1.11.dist-info/entry_points.txt,sha256=4EoFQ0yRogLTvStcbjlpkTqayWmiRqbMwkcDBcgQbK8,43
|
|
12
|
+
helper_cli-0.1.11.dist-info/top_level.txt,sha256=VM8lkErPJijbKhnfEGA_hE_YDXde4iizgqWKloZIxW8,7
|
|
13
|
+
helper_cli-0.1.11.dist-info/RECORD,,
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
helper/main.py,sha256=TTB6NuLydg2EcMlV2NxyYwKLag6tGuQhxbwNBBiOanU,1520
|
|
2
|
-
helper_cli-0.1.1.dist-info/METADATA,sha256=PBrA7-WIggiEiRpXGrCJNbCCXsKkWKQAyzoJlt1lxBw,75
|
|
3
|
-
helper_cli-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
4
|
-
helper_cli-0.1.1.dist-info/entry_points.txt,sha256=4EoFQ0yRogLTvStcbjlpkTqayWmiRqbMwkcDBcgQbK8,43
|
|
5
|
-
helper_cli-0.1.1.dist-info/top_level.txt,sha256=VM8lkErPJijbKhnfEGA_hE_YDXde4iizgqWKloZIxW8,7
|
|
6
|
-
helper_cli-0.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|