utils_devops 0.1.130__py3-none-any.whl → 0.1.136__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.
- utils_devops/extras/ssh_ops.py +137 -9
- {utils_devops-0.1.130.dist-info → utils_devops-0.1.136.dist-info}/METADATA +1 -1
- {utils_devops-0.1.130.dist-info → utils_devops-0.1.136.dist-info}/RECORD +5 -5
- {utils_devops-0.1.130.dist-info → utils_devops-0.1.136.dist-info}/WHEEL +0 -0
- {utils_devops-0.1.130.dist-info → utils_devops-0.1.136.dist-info}/entry_points.txt +0 -0
utils_devops/extras/ssh_ops.py
CHANGED
|
@@ -340,18 +340,35 @@ def ssh_connect(
|
|
|
340
340
|
key_path, key_content = _resolve_ssh_key(key_file)
|
|
341
341
|
|
|
342
342
|
if key_content:
|
|
343
|
+
# **FIXED HERE**: Ensure proper line endings in the key content
|
|
344
|
+
# SSH keys MUST have newlines at specific positions
|
|
345
|
+
key_content = _normalize_ssh_key_content(key_content)
|
|
346
|
+
|
|
343
347
|
# Create temporary file for key content
|
|
344
348
|
import tempfile
|
|
345
349
|
temp_key_file = tempfile.NamedTemporaryFile(
|
|
346
|
-
mode='w',
|
|
350
|
+
mode='w', # Text mode for proper newline handling
|
|
347
351
|
suffix='_ssh_key',
|
|
348
|
-
delete=False
|
|
352
|
+
delete=False,
|
|
353
|
+
newline='\n' # Explicitly use LF newlines
|
|
349
354
|
)
|
|
355
|
+
|
|
356
|
+
# **CRITICAL FIX**: Write with proper line endings
|
|
350
357
|
temp_key_file.write(key_content)
|
|
358
|
+
temp_key_file.flush() # Ensure data is written
|
|
359
|
+
os.fsync(temp_key_file.fileno()) # Force sync to disk
|
|
351
360
|
temp_key_file.close()
|
|
352
361
|
|
|
353
|
-
# Set permissions
|
|
362
|
+
# Set strict permissions for SSH keys
|
|
354
363
|
os.chmod(temp_key_file.name, 0o600)
|
|
364
|
+
|
|
365
|
+
# **DEBUG**: Verify what was written
|
|
366
|
+
with open(temp_key_file.name, 'r') as f:
|
|
367
|
+
written_content = f.read()
|
|
368
|
+
_logger.debug(f"Written key length: {len(written_content)}")
|
|
369
|
+
_logger.debug(f"Key first 100 chars: {written_content[:100]}")
|
|
370
|
+
_logger.debug(f"Key lines: {written_content.count(chr(10))}")
|
|
371
|
+
|
|
355
372
|
connect_kwargs['key_filename'] = temp_key_file.name
|
|
356
373
|
_logger.debug(f"Using temporary SSH key file: {temp_key_file.name}")
|
|
357
374
|
|
|
@@ -390,8 +407,8 @@ def ssh_connect(
|
|
|
390
407
|
try:
|
|
391
408
|
os.unlink(temp_key_file.name)
|
|
392
409
|
_logger.debug(f"Cleaned up temporary key file: {temp_key_file.name}")
|
|
393
|
-
except:
|
|
394
|
-
|
|
410
|
+
except Exception as e:
|
|
411
|
+
_logger.warning(f"Failed to cleanup temp key file: {e}")
|
|
395
412
|
|
|
396
413
|
if client:
|
|
397
414
|
try:
|
|
@@ -400,6 +417,117 @@ def ssh_connect(
|
|
|
400
417
|
except:
|
|
401
418
|
pass
|
|
402
419
|
|
|
420
|
+
|
|
421
|
+
def _normalize_ssh_key_content(key_content: str) -> str:
|
|
422
|
+
"""
|
|
423
|
+
Normalize SSH key content to ensure proper formatting.
|
|
424
|
+
|
|
425
|
+
SSH keys require specific line breaks:
|
|
426
|
+
- Must start with -----BEGIN ...-----
|
|
427
|
+
- Base64 content should be on separate lines (64 chars max per line)
|
|
428
|
+
- Must end with -----END ...-----
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
key_content: Raw key content (may be single line or malformed)
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Properly formatted SSH key
|
|
435
|
+
"""
|
|
436
|
+
# If already looks properly formatted, return as-is
|
|
437
|
+
if "-----BEGIN" in key_content and "-----END" in key_content and '\n' in key_content:
|
|
438
|
+
# Verify line endings
|
|
439
|
+
lines = key_content.splitlines()
|
|
440
|
+
if len(lines) > 1:
|
|
441
|
+
return key_content
|
|
442
|
+
|
|
443
|
+
# Parse and reformat
|
|
444
|
+
lines = []
|
|
445
|
+
current_line = []
|
|
446
|
+
|
|
447
|
+
# Remove all existing whitespace and split
|
|
448
|
+
clean_content = key_content.strip()
|
|
449
|
+
|
|
450
|
+
if clean_content.startswith("-----BEGIN"):
|
|
451
|
+
# Extract header
|
|
452
|
+
header_end = clean_content.find("-----", 10) + 5
|
|
453
|
+
header = clean_content[:header_end]
|
|
454
|
+
lines.append(header)
|
|
455
|
+
|
|
456
|
+
# Get base64 content between headers
|
|
457
|
+
base64_start = header_end
|
|
458
|
+
base64_end = clean_content.find("-----END")
|
|
459
|
+
base64_content = clean_content[base64_start:base64_end].strip()
|
|
460
|
+
|
|
461
|
+
# Remove all whitespace from base64
|
|
462
|
+
base64_clean = ''.join(base64_content.split())
|
|
463
|
+
|
|
464
|
+
# Split into 64 character lines (standard for SSH keys)
|
|
465
|
+
for i in range(0, len(base64_clean), 64):
|
|
466
|
+
lines.append(base64_clean[i:i+64])
|
|
467
|
+
|
|
468
|
+
# Add footer
|
|
469
|
+
footer_start = base64_end
|
|
470
|
+
lines.append(clean_content[footer_start:].strip())
|
|
471
|
+
else:
|
|
472
|
+
# Assume it's just base64, wrap in OpenSSH header/footer
|
|
473
|
+
lines.append("-----BEGIN OPENSSH PRIVATE KEY-----")
|
|
474
|
+
clean_base64 = ''.join(clean_content.split())
|
|
475
|
+
for i in range(0, len(clean_base64), 64):
|
|
476
|
+
lines.append(clean_base64[i:i+64])
|
|
477
|
+
lines.append("-----END OPENSSH PRIVATE KEY-----")
|
|
478
|
+
|
|
479
|
+
return '\n'.join(lines) + '\n'
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
# Also update your _resolve_ssh_key function to preserve newlines:
|
|
483
|
+
def _resolve_ssh_key(key_input: Optional[Union[str, Path]]) -> Tuple[Optional[Path], Optional[str]]:
|
|
484
|
+
"""
|
|
485
|
+
Resolve SSH key input to either a file path or key content.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
key_input: Could be:
|
|
489
|
+
- Path to key file
|
|
490
|
+
- Environment variable name ($VAR)
|
|
491
|
+
- Raw key content string
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
Tuple of (file_path, key_content)
|
|
495
|
+
"""
|
|
496
|
+
if not key_input:
|
|
497
|
+
return None, None
|
|
498
|
+
|
|
499
|
+
# If it's a Path object
|
|
500
|
+
if isinstance(key_input, Path):
|
|
501
|
+
if key_input.exists():
|
|
502
|
+
return key_input, None
|
|
503
|
+
else:
|
|
504
|
+
raise FileNotFoundError(f"SSH key file not found: {key_input}")
|
|
505
|
+
|
|
506
|
+
key_input_str = str(key_input)
|
|
507
|
+
|
|
508
|
+
# Check if it's an environment variable reference
|
|
509
|
+
if key_input_str.startswith('$'):
|
|
510
|
+
env_var = key_input_str[1:]
|
|
511
|
+
key_content = os.environ.get(env_var)
|
|
512
|
+
if not key_content:
|
|
513
|
+
raise ValueError(f"Environment variable not set: {env_var}")
|
|
514
|
+
|
|
515
|
+
# **CRITICAL**: Don't strip newlines from env var content!
|
|
516
|
+
# SSH keys in env vars often have escaped newlines (\n)
|
|
517
|
+
# Replace literal \n with actual newlines
|
|
518
|
+
if '\\n' in key_content:
|
|
519
|
+
key_content = key_content.replace('\\n', '\n')
|
|
520
|
+
|
|
521
|
+
return None, key_content
|
|
522
|
+
|
|
523
|
+
# Check if it's a file path
|
|
524
|
+
if os.path.exists(key_input_str):
|
|
525
|
+
return Path(key_input_str), None
|
|
526
|
+
|
|
527
|
+
# Assume it's raw key content
|
|
528
|
+
# **IMPORTANT**: Preserve any newlines in the content
|
|
529
|
+
return None, key_input_str
|
|
530
|
+
|
|
403
531
|
def ssh_execute_command(
|
|
404
532
|
host: str,
|
|
405
533
|
command: Union[str, List[str]],
|
|
@@ -1465,10 +1593,10 @@ def validate_machine_config(machine: MachineConfig) -> List[str]:
|
|
|
1465
1593
|
errors.append(f"Invalid deploy_dir format: {e}")
|
|
1466
1594
|
|
|
1467
1595
|
# Validate key file if specified
|
|
1468
|
-
if machine.key_file:
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1596
|
+
# if machine.key_file:
|
|
1597
|
+
# key_path = Path(machine.key_file).expanduser()
|
|
1598
|
+
# if not key_path.exists():
|
|
1599
|
+
# errors.append(f"SSH key file not found: {key_path}")
|
|
1472
1600
|
|
|
1473
1601
|
return errors
|
|
1474
1602
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: utils_devops
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.136
|
|
4
4
|
Summary: Lightweight DevOps utilities for automation scripts: config editing (YAML/JSON/INI/.env), templating, diffing, and CLI tools
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: devops,automation,nginx,cli,jinja2,yaml,config,diff,templating,logging,docker,compose,file-ops
|
|
@@ -17,9 +17,9 @@ utils_devops/extras/network_ops.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
|
|
|
17
17
|
utils_devops/extras/nginx_ops.py,sha256=FcfwGSuvcG5OX7UXPJpSTEMEBGYeT2g_I4kbTf-xY0w,100648
|
|
18
18
|
utils_devops/extras/notification_ops.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
19
|
utils_devops/extras/performance_ops.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
utils_devops/extras/ssh_ops.py,sha256=
|
|
20
|
+
utils_devops/extras/ssh_ops.py,sha256=8I_AF0q76CJOK2qp68w1oro2SVOZ_v7b8OvgDYcE4tg,73741
|
|
21
21
|
utils_devops/extras/vault_ops.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
-
utils_devops-0.1.
|
|
23
|
-
utils_devops-0.1.
|
|
24
|
-
utils_devops-0.1.
|
|
25
|
-
utils_devops-0.1.
|
|
22
|
+
utils_devops-0.1.136.dist-info/METADATA,sha256=BVHNdxH50ZdVlV5w2uN21e_klGqdvwg168g7fWVnh90,1903
|
|
23
|
+
utils_devops-0.1.136.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
24
|
+
utils_devops-0.1.136.dist-info/entry_points.txt,sha256=ei3B6ZL5yu6dOq-U1r8wsBdkXeg63RAyV7m8_ADaE6k,53
|
|
25
|
+
utils_devops-0.1.136.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|