Skip to content

Commit 0c74840

Browse files
authored
Merge pull request #179 from SimonThalvorsen/main
Updated behaviour of cfengine run-command
2 parents ef5f6e5 + 5c8f184 commit 0c74840

2 files changed

Lines changed: 157 additions & 4 deletions

File tree

src/cfengine_cli/cfengine_wrapper/cfengine_commands.py

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,135 @@
1+
import os
2+
import logging
3+
14
from cfbs.commands import build_command
25
from cf_remote.commands import deploy as deploy_command
36
from cf_remote.commands import install as install_command
47
from cf_remote.commands import destroy as destroy_command
8+
from cf_remote.remote import run_command, transfer_file
59

10+
from cfengine_cli.utils import UserError
11+
from cfengine_cli.cfengine_wrapper.cfengine_objects import (
12+
Executable,
13+
_ensure_default_agent_flags,
14+
)
615
from cfengine_cli.cfengine_wrapper.cfengine_utils import (
16+
extract_agent_file,
17+
prompt_two_options,
718
require_executable,
819
require_installation,
920
)
1021

22+
_DEFAULT_CFENGINE_INPUTS_DIR = "/var/cfengine/inputs"
23+
24+
25+
# ---------------------------------------------------------------------------
26+
# File-resolution helpers for run()
27+
# ---------------------------------------------------------------------------
28+
29+
30+
def _remote_path_exists(host: str, path: str) -> bool:
31+
return run_command(host, f"test -f {path}", sudo=False) is not None
32+
33+
34+
def _replace_file_token(command: str, old: str, new: str) -> str:
35+
tokens = command.split()
36+
return " ".join(new if t == old else t for t in tokens)
37+
38+
39+
def _resolve_bare_filename_local(command: str, file_arg: str) -> str:
40+
cwd_path = os.path.join(os.getcwd(), file_arg)
41+
inputs_path = os.path.join(_DEFAULT_CFENGINE_INPUTS_DIR, file_arg)
42+
cwd_exists = os.path.isfile(cwd_path)
43+
inputs_exists = os.path.isfile(inputs_path)
44+
45+
if not cwd_exists and not inputs_exists:
46+
raise UserError(
47+
f"Could not find '{file_arg}' in the current directory or in "
48+
f"{_DEFAULT_CFENGINE_INPUTS_DIR}."
49+
)
50+
51+
if cwd_exists and inputs_exists:
52+
choice = prompt_two_options(
53+
f"'{file_arg}' exists both in the current directory and in "
54+
f"{_DEFAULT_CFENGINE_INPUTS_DIR}.",
55+
f"the copy in the current directory ({cwd_path})",
56+
f"the copy already in {_DEFAULT_CFENGINE_INPUTS_DIR} ({inputs_path})",
57+
)
58+
use_cwd = choice != "b"
59+
else:
60+
use_cwd = cwd_exists
61+
62+
return _replace_file_token(command, file_arg, cwd_path) if use_cwd else command
63+
64+
65+
def _remote_home_dir(location: str) -> str:
66+
user = location.split("@", 1)[0]
67+
return "/root" if user == "root" else f"/home/{user}"
68+
69+
70+
def _resolve_file_remote(location: str, command: str, file_arg: str) -> str:
71+
local_exists = os.path.isfile(file_arg)
72+
73+
local_home = os.path.expanduser("~")
74+
remote_home = _remote_home_dir(location)
75+
76+
if file_arg.startswith(local_home + os.sep):
77+
# e.g. "~/update.cf", shell expands to /home/{user}/update.cf
78+
rel = file_arg[len(local_home) + 1 :]
79+
remote_path = f"{remote_home}/{rel}"
80+
elif file_arg.startswith("/"):
81+
remote_path = file_arg
82+
else:
83+
remote_path = f"{_DEFAULT_CFENGINE_INPUTS_DIR}/{file_arg}"
84+
85+
remote_exists = _remote_path_exists(location, remote_path)
86+
87+
if not local_exists and not remote_exists:
88+
raise UserError(
89+
f"Could not find '{file_arg}' locally or on {location} (checked {remote_path})."
90+
)
91+
92+
if local_exists and remote_exists:
93+
choice = prompt_two_options(
94+
f"'{file_arg}' exists both locally and on {location} (at {remote_path}).\nUse the:",
95+
"local copy (uploads it, possibly overwriting the remote copy)",
96+
f"copy already on {location}, unchanged",
97+
)
98+
elif local_exists:
99+
choice = "a"
100+
else:
101+
choice = "b"
102+
103+
if choice == "b":
104+
return _replace_file_token(command, file_arg, remote_path)
105+
106+
uploaded_path = f"{remote_home}/{os.path.basename(file_arg)}"
107+
logging.warning(f"Uploading {file_arg} to {location}:{uploaded_path}")
108+
transfer_file(location, file_arg)
109+
return _replace_file_token(command, file_arg, uploaded_path)
110+
111+
112+
def _resolve_command_for_agent(agent: Executable, command: str) -> str:
113+
if agent.name != "cf-agent":
114+
return command
115+
116+
command = _ensure_default_agent_flags(command)
117+
file_arg = extract_agent_file(command)
118+
if not file_arg:
119+
return command
120+
121+
if agent.is_local:
122+
if "/" in file_arg:
123+
return command # no ambiguity for a local target
124+
return _resolve_bare_filename_local(command, file_arg)
125+
126+
return _resolve_file_remote(agent.location, command, file_arg)
127+
128+
129+
# ---------------------------------------------------------------------------
130+
# Commands
131+
# ---------------------------------------------------------------------------
132+
11133

12134
def report(target: str | None = None) -> int: # TODO? ENT-14122
13135
installation = require_installation(target)
@@ -21,9 +143,10 @@ def report(target: str | None = None) -> int: # TODO? ENT-14122
21143

22144
def run(*args, target: str | None = None) -> int:
23145
agent = require_executable("cf-agent", target)
24-
if args:
25-
return agent.run(*args)
26-
return agent.run("-KIf update.cf", "-KI")
146+
if not args:
147+
return agent.run("-KIf update.cf", "-KI")
148+
resolved = [_resolve_command_for_agent(agent, command) for command in args]
149+
return agent.run(*resolved)
27150

28151

29152
def install() -> int: # TODO ENT-14117

src/cfengine_cli/cfengine_wrapper/cfengine_utils.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,34 @@
1212
from cf_remote.utils import read_json
1313

1414

15+
def prompt_two_options(header: str, option_a: str, option_b: str) -> str:
16+
print(header)
17+
print(f" 1) {option_a}")
18+
print(f" 2) {option_b}")
19+
while True:
20+
choice = input("Select [1-2]: ").strip()
21+
if choice == "1":
22+
return "a"
23+
if choice == "2":
24+
return "b"
25+
print("Invalid selection, try again.")
26+
27+
28+
def extract_agent_file(command: str) -> str | None:
29+
"""
30+
Best-effort extraction of the filename cf-agent would read as policy
31+
input, from an already-flagged command string (e.g. "-KIf update.cf").
32+
Returns None if there's no file argument to check at all (e.g. plain
33+
"-KI"), in which case there's nothing to resolve.
34+
"""
35+
tokens = command.split()
36+
for i, token in enumerate(tokens):
37+
if token.startswith("-") and not token.startswith("--") and "f" in token:
38+
if i + 1 < len(tokens):
39+
return tokens[i + 1]
40+
return None
41+
42+
1543
def _find_local_path(binary_name: str) -> str | None:
1644
candidate = bin(binary_name)
1745
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
@@ -95,7 +123,9 @@ def _find_all_paired() -> list[Installation]:
95123
for host, aliases in _known_hosts(role_filter="hub"):
96124
try:
97125
data = get_info(host)
98-
except Exception:
126+
except (Exception, SystemExit) as e:
127+
# Same reasoning as _find_all()
128+
logging.warning(f"Skipping {host}: {e}")
99129
continue
100130
if not data:
101131
continue

0 commit comments

Comments
 (0)