utils_devops 0.1.130__tar.gz → 0.1.136__tar.gz

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.
Files changed (24) hide show
  1. {utils_devops-0.1.130 → utils_devops-0.1.136}/PKG-INFO +1 -1
  2. {utils_devops-0.1.130 → utils_devops-0.1.136}/pyproject.toml +1 -1
  3. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/extras/ssh_ops.py +137 -9
  4. {utils_devops-0.1.130 → utils_devops-0.1.136}/README.md +0 -0
  5. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/__init__.py +0 -0
  6. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/core/__init__.py +0 -0
  7. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/core/datetimes.py +0 -0
  8. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/core/envs.py +0 -0
  9. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/core/files.py +0 -0
  10. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/core/logs.py +0 -0
  11. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/core/script_helpers.py +0 -0
  12. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/core/strings.py +0 -0
  13. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/core/systems.py +0 -0
  14. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/extras/__init__.py +0 -0
  15. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/extras/aws_ops.py +0 -0
  16. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/extras/docker_ops.py +0 -0
  17. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/extras/git_ops.py +0 -0
  18. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/extras/interaction_ops.py +0 -0
  19. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/extras/metrics_ops.py +0 -0
  20. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/extras/network_ops.py +0 -0
  21. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/extras/nginx_ops.py +0 -0
  22. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/extras/notification_ops.py +0 -0
  23. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/extras/performance_ops.py +0 -0
  24. {utils_devops-0.1.130 → utils_devops-0.1.136}/src/utils_devops/extras/vault_ops.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: utils_devops
3
- Version: 0.1.130
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
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "utils_devops"
3
- version = "0.1.130" # Bumped for new string features + diffing
3
+ version = "0.1.136" # Bumped for new string features + diffing
4
4
  description = "Lightweight DevOps utilities for automation scripts: config editing (YAML/JSON/INI/.env), templating, diffing, and CLI tools"
5
5
  authors = ["Hamed Sheikhan <sh.sheikhan.m@gmail.com>"]
6
6
  license = "MIT"
@@ -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
- pass
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
- key_path = Path(machine.key_file).expanduser()
1470
- if not key_path.exists():
1471
- errors.append(f"SSH key file not found: {key_path}")
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
 
File without changes