Skip to content

Commit bb4221a

Browse files
committed
Add all options to initial config
This adds all available options with a description to the config file (if recreated) instead of a minimalistic one. Signed-off-by: TobiPeterG <github.threefold020@passmail.net>
1 parent 3fcc00c commit bb4221a

3 files changed

Lines changed: 293 additions & 29 deletions

File tree

osc/commandline.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ def post_parse_args(self, args):
146146
# HACK: never ask for credentials when displaying help
147147
return
148148

149+
if args.command in ("initconfig", "ic"):
150+
# HACK: let command handle config creation
151+
self.args = args
152+
return
153+
149154
overrides = {}
150155
for i in args.setopt:
151156
key, value = i.split("=")
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import os
2+
import sys
3+
4+
import osc.commandline
5+
from osc import conf as osc_conf
6+
from osc import conf
7+
from osc.util.helper import raw_input
8+
9+
10+
def _confirm_overwrite(conffile: str) -> bool:
11+
path = os.path.expanduser(conffile)
12+
if os.path.exists(path):
13+
print(f"Config file '{conffile}' already exists.", file=sys.stderr)
14+
answer = raw_input("Overwrite it? [y/N]: ").strip().lower()
15+
if answer not in ("y", "yes"):
16+
print("Aborted: not overwriting existing config.", file=sys.stderr)
17+
return False
18+
return True
19+
20+
21+
class InitConfigFullCommand(osc.commandline.OscCommand):
22+
"""
23+
Generate a new configuration file.
24+
"""
25+
26+
name = "initconfig"
27+
aliases = ["ic"]
28+
29+
def init_arguments(self):
30+
self.add_argument(
31+
"--file",
32+
help="Write the config to this file instead of the default location",
33+
)
34+
self.add_argument(
35+
"--apiurl",
36+
help="API URL to embed in the config instead of the default one",
37+
)
38+
39+
def run(self, args):
40+
apiurl = args.apiurl or osc_conf.Options().apiurl
41+
conffile = args.file or osc_conf.identify_conf()
42+
43+
if not _confirm_overwrite(conffile):
44+
return # user chose not to overwrite
45+
46+
conf.interactive_config_setup(conffile, apiurl, True)

osc/conf.py

Lines changed: 242 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1502,23 +1502,157 @@ def __setitem__(self, name, value):
15021502

15031503
DEFAULTS = Defaults()
15041504

1505+
def _ini_default_from_field(field):
1506+
"""
1507+
Compute a reasonable default-as-string for inclusion in comments.
1508+
"""
1509+
from .util.models import FromParent
15051510

1506-
new_conf_template = """
1507-
# see oscrc(5) man page for the full list of available options
1511+
if field.default is None:
1512+
return None
15081513

1509-
[general]
1514+
if getattr(field, "default_is_lazy", False):
1515+
return None
15101516

1511-
# Default URL to the API server.
1512-
# Credentials and other `apiurl` specific settings must be configured in a `[$apiurl]` config section.
1513-
apiurl=%(apiurl)s
1517+
ini_type = field.extra.get("ini_type", None)
1518+
if ini_type:
1519+
return None
15141520

1515-
[%(apiurl)s]
1516-
# aliases=
1517-
# user=
1518-
# pass=
1519-
# credentials_mgr_class=osc.credentials...
1520-
"""
1521+
if isinstance(field.default, FromParent):
1522+
return None
1523+
1524+
origin_type = field.origin_type
1525+
1526+
if origin_type == bool:
1527+
return "1" if field.default else "0"
1528+
1529+
if origin_type == int:
1530+
return str(field.default)
1531+
1532+
if origin_type == list:
1533+
if not field.default:
1534+
return None
1535+
return " ".join(str(v) for v in field.default)
1536+
1537+
if origin_type == str:
1538+
return field.default
1539+
1540+
return None
1541+
1542+
1543+
def _emit_option_block(
1544+
lines,
1545+
ini_key: str,
1546+
desc: str,
1547+
default_str: str | None,
1548+
apiurl_placeholder: str | None = None,
1549+
):
1550+
"""
1551+
Append a fully formatted option block to `lines`.
1552+
1553+
If `apiurl_placeholder` is not None and ini_key == "apiurl",
1554+
emit an active `apiurl=...` line instead of a commented placeholder.
1555+
"""
1556+
lines.append("#")
1557+
1558+
if desc:
1559+
for line in desc.splitlines():
1560+
if line.strip():
1561+
lines.append("#--# " + line)
1562+
else:
1563+
lines.append("#--#")
1564+
lines.append("#--#")
1565+
1566+
if apiurl_placeholder is not None and ini_key == "apiurl":
1567+
lines.append(f"apiurl={apiurl_placeholder}")
1568+
return
1569+
1570+
if default_str is not None:
1571+
lines.append(f"#--# Default: {ini_key} = {default_str}")
1572+
lines.append("#")
1573+
lines.append(f"{ini_key} = {default_str}")
1574+
else:
1575+
lines.append("#")
1576+
lines.append(f"# {ini_key} =")
1577+
1578+
1579+
def generate_conf_template(apiurl_placeholder: str = "%(apiurl)s") -> str:
1580+
"""
1581+
Generate a full oscrc template.
1582+
"""
1583+
line_length = 30
1584+
lines: list[str] = []
1585+
lines.append("# see oscrc(5) man page for the full list of available options")
1586+
lines.append("#")
1587+
lines.append("[general]")
1588+
1589+
for name, field in Options.__fields__.items():
1590+
extra = field.extra
1591+
1592+
if extra.get("section", False):
1593+
lines.append("#")
1594+
lines.append("#" + "-" * line_length)
1595+
lines.append(f"#-- {field.default} --")
1596+
lines.append("#" + "-" * line_length)
1597+
continue
1598+
1599+
if extra.get("ini_exclude", False) or field.exclude:
1600+
continue
1601+
1602+
if field.description is None and not extra.get("ini_description", None):
1603+
continue
1604+
1605+
ini_key = extra.get("ini_key", name)
1606+
desc = extra.get("ini_description", None) or field.description or ""
1607+
default_str = _ini_default_from_field(field)
1608+
1609+
_emit_option_block(
1610+
lines,
1611+
ini_key=ini_key,
1612+
desc=desc,
1613+
default_str=default_str,
1614+
apiurl_placeholder=apiurl_placeholder,
1615+
)
1616+
1617+
lines.append("#")
1618+
lines.append(f"[{apiurl_placeholder}]")
1619+
lines.append("# Per-API server configuration")
1620+
lines.append("# (credentials and API specific options)")
1621+
lines.append("#")
1622+
1623+
for name, field in HostOptions.__fields__.items():
1624+
extra = field.extra
1625+
1626+
if extra.get("section", False):
1627+
continue
15211628

1629+
if extra.get("ini_exclude", False) or field.exclude:
1630+
continue
1631+
1632+
if field.description is None and not extra.get("ini_description", None):
1633+
continue
1634+
1635+
ini_key = extra.get("ini_key", name)
1636+
1637+
# apiurl itself is expressed by the section header; do not emit as key
1638+
if ini_key == "apiurl":
1639+
continue
1640+
1641+
desc = extra.get("ini_description", None) or field.description or ""
1642+
default_str = _ini_default_from_field(field)
1643+
1644+
_emit_option_block(
1645+
lines,
1646+
ini_key=ini_key,
1647+
desc=desc,
1648+
default_str=default_str,
1649+
apiurl_placeholder=None,
1650+
)
1651+
1652+
return "\n".join(lines) + "\n"
1653+
1654+
1655+
new_conf_template = generate_conf_template(apiurl_placeholder="%(apiurl)s")
15221656

15231657
account_not_configured_text = """
15241658
Your user account / password are not configured yet.
@@ -1766,25 +1900,66 @@ def _extract_user_compat(cp, section, creds_mgr):
17661900
user = creds_mgr.get_user(section)
17671901
return user
17681902

1903+
def _replace_placeholder_in_section(text: str, section: str, key: str, value: str) -> str:
1904+
"""
1905+
Replace the first commented placeholder '# key = ...' in the given
1906+
[section] with an active 'key=value' line.
1907+
"""
1908+
lines = text.splitlines()
1909+
section_header = f"[{section}]"
1910+
inside = False
1911+
1912+
pattern = re.compile(rf"^#\s*{re.escape(key)}\s*=\s*.*$")
1913+
1914+
for i, line in enumerate(lines):
1915+
stripped = line.strip()
1916+
1917+
if stripped.startswith("[") and stripped.endswith("]"):
1918+
inside = stripped == section_header
1919+
continue
1920+
1921+
if not inside:
1922+
continue
1923+
1924+
if pattern.match(stripped):
1925+
lines[i] = f"{key}={value}"
1926+
break
1927+
1928+
return "\n".join(lines) + "\n"
1929+
17691930

1770-
def write_initial_config(conffile, entries, custom_template='', creds_mgr_descriptor=None):
1931+
def write_initial_config(conffile, entries, custom_template="", creds_mgr_descriptor=None):
17711932
"""
1772-
write osc's intial configuration file. entries is a dict which contains values
1933+
write osc's initial configuration file. entries is a dict which contains values
17731934
for the config file (e.g. { 'user' : 'username', 'pass' : 'password' } ).
17741935
custom_template is an optional configuration template.
17751936
"""
17761937
conf_template = custom_template or new_conf_template
1777-
config = globals()["config"].dict()
1778-
config.update(entries)
1779-
sio = StringIO(conf_template.strip() % config)
1938+
1939+
base_cfg = globals()["config"]
1940+
apiurl = entries.get("apiurl") or base_cfg.apiurl
1941+
user = entries["user"]
1942+
passwd = entries["pass"]
1943+
1944+
text = conf_template.strip().replace("%(apiurl)s", apiurl)
1945+
1946+
text = _replace_placeholder_in_section(text, apiurl, "user", user)
1947+
if passwd:
1948+
text = _replace_placeholder_in_section(text, apiurl, "pass", passwd)
1949+
1950+
# Will be set to actual value by credential manager; remove the comment here
1951+
text = _replace_placeholder_in_section(text, apiurl, "credentials_mgr_class", "")
1952+
1953+
sio = StringIO(text)
17801954
cp = OscConfigParser.OscConfigParser()
17811955
cp.read_file(sio)
1782-
cp.set(config['apiurl'], 'user', config['user'])
1956+
17831957
if creds_mgr_descriptor:
17841958
creds_mgr = creds_mgr_descriptor.create(cp)
17851959
else:
1786-
creds_mgr = _get_credentials_manager(config['apiurl'], cp)
1787-
creds_mgr.set_password(config['apiurl'], config['user'], config['pass'])
1960+
creds_mgr = _get_credentials_manager(apiurl, cp)
1961+
creds_mgr.set_password(apiurl, user, passwd)
1962+
17881963
write_config(conffile, cp)
17891964

17901965

@@ -1811,10 +1986,18 @@ def add_section(filename, url, user, passwd, creds_mgr_descriptor=None, allow_ht
18111986

18121987

18131988
def _get_credentials_manager(url, cp):
1814-
if cp.has_option(url, credentials.AbstractCredentialsManager.config_entry):
1989+
try:
1990+
entry = cp.get(url, credentials.AbstractCredentialsManager.config_entry, raw=True)
1991+
except OscConfigParser.configparser.NoOptionError:
1992+
entry = None
1993+
1994+
if entry is not None:
1995+
entry = entry.strip()
1996+
1997+
if entry:
18151998
creds_mgr = credentials.create_credentials_manager(url, cp)
18161999
if creds_mgr is None:
1817-
msg = f'Unable to instantiate creds mgr (section: {url})'
2000+
msg = f"Unable to instantiate creds mgr (section: {url})"
18182001
conffile = get_configParser.conffile
18192002
raise oscerr.ConfigMissingCredentialsError(msg, conffile, url)
18202003
return creds_mgr
@@ -2157,19 +2340,49 @@ def interactive_config_setup(conffile, apiurl, initial=True):
21572340

21582341
apiurl_no_scheme = urlsplit(apiurl)[1] or apiurl
21592342
user_prompt = f"Username [{apiurl_no_scheme}]: "
2160-
user = raw_input(user_prompt)
2343+
user = ""
2344+
max_attempts = 3
2345+
for attempt in range(max_attempts):
2346+
user = raw_input(user_prompt).strip()
2347+
if user:
2348+
break
2349+
remaining = max_attempts - attempt - 1
2350+
if remaining > 0:
2351+
print("Username must not be empty, please try again.", file=sys.stderr)
2352+
else:
2353+
print("No username entered. Aborting.", file=sys.stderr)
2354+
raise oscerr.UserAbort()
2355+
21612356
pass_prompt = f"Password [{user}@{apiurl_no_scheme}]: "
21622357
passwd = getpass.getpass(pass_prompt)
2163-
creds_mgr_descr = select_credentials_manager_descr()
2358+
if not passwd:
2359+
print("Password is empty. To use SSH, enter no password again.")
2360+
pass_prompt = f"Password [{user}@{apiurl_no_scheme}]: "
2361+
passwd = getpass.getpass(pass_prompt)
2362+
if not passwd:
2363+
print("Password is empty. Use SSH or run 'osc initconfig' to re-create the config file.")
2364+
21642365
if initial:
2165-
config = {'user': user, 'pass': passwd}
2366+
config = {"user": user, "pass": passwd}
21662367
if apiurl:
2167-
config['apiurl'] = apiurl
2368+
config["apiurl"] = apiurl
21682369
if http:
2169-
config['allow_http'] = 1
2170-
write_initial_config(conffile, config, creds_mgr_descriptor=creds_mgr_descr)
2370+
config["allow_http"] = 1
2371+
2372+
write_initial_config(
2373+
conffile,
2374+
config,
2375+
creds_mgr_descriptor=select_credentials_manager_descr(),
2376+
)
21712377
else:
2172-
add_section(conffile, apiurl, user, passwd, creds_mgr_descriptor=creds_mgr_descr, allow_http=http)
2378+
add_section(
2379+
conffile,
2380+
apiurl,
2381+
user,
2382+
passwd,
2383+
creds_mgr_descriptor=select_credentials_manager_descr(),
2384+
allow_http=http,
2385+
)
21732386

21742387

21752388
def select_credentials_manager_descr():

0 commit comments

Comments
 (0)