ominfra 0.0.0.dev154__py3-none-any.whl → 0.0.0.dev155__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.
Files changed (41) hide show
  1. ominfra/manage/bootstrap.py +4 -0
  2. ominfra/manage/bootstrap_.py +5 -0
  3. ominfra/manage/commands/inject.py +8 -11
  4. ominfra/manage/commands/{execution.py → local.py} +1 -5
  5. ominfra/manage/commands/ping.py +23 -0
  6. ominfra/manage/commands/types.py +8 -0
  7. ominfra/manage/deploy/apps.py +72 -0
  8. ominfra/manage/deploy/config.py +8 -0
  9. ominfra/manage/deploy/git.py +136 -0
  10. ominfra/manage/deploy/inject.py +21 -0
  11. ominfra/manage/deploy/paths.py +81 -28
  12. ominfra/manage/deploy/types.py +13 -0
  13. ominfra/manage/deploy/venvs.py +66 -0
  14. ominfra/manage/inject.py +20 -4
  15. ominfra/manage/main.py +15 -27
  16. ominfra/manage/remote/_main.py +1 -1
  17. ominfra/manage/remote/config.py +0 -2
  18. ominfra/manage/remote/connection.py +7 -24
  19. ominfra/manage/remote/execution.py +1 -1
  20. ominfra/manage/remote/inject.py +3 -14
  21. ominfra/manage/system/commands.py +22 -2
  22. ominfra/manage/system/config.py +3 -1
  23. ominfra/manage/system/inject.py +16 -6
  24. ominfra/manage/system/packages.py +33 -7
  25. ominfra/manage/system/platforms.py +72 -0
  26. ominfra/manage/targets/__init__.py +0 -0
  27. ominfra/manage/targets/connection.py +150 -0
  28. ominfra/manage/targets/inject.py +42 -0
  29. ominfra/manage/targets/targets.py +87 -0
  30. ominfra/scripts/journald2aws.py +24 -7
  31. ominfra/scripts/manage.py +1880 -438
  32. ominfra/scripts/supervisor.py +187 -25
  33. ominfra/supervisor/configs.py +163 -18
  34. {ominfra-0.0.0.dev154.dist-info → ominfra-0.0.0.dev155.dist-info}/METADATA +3 -3
  35. {ominfra-0.0.0.dev154.dist-info → ominfra-0.0.0.dev155.dist-info}/RECORD +40 -29
  36. ominfra/manage/system/types.py +0 -5
  37. /ominfra/manage/{commands → deploy}/interp.py +0 -0
  38. {ominfra-0.0.0.dev154.dist-info → ominfra-0.0.0.dev155.dist-info}/LICENSE +0 -0
  39. {ominfra-0.0.0.dev154.dist-info → ominfra-0.0.0.dev155.dist-info}/WHEEL +0 -0
  40. {ominfra-0.0.0.dev154.dist-info → ominfra-0.0.0.dev155.dist-info}/entry_points.txt +0 -0
  41. {ominfra-0.0.0.dev154.dist-info → ominfra-0.0.0.dev155.dist-info}/top_level.txt +0 -0
ominfra/scripts/manage.py CHANGED
@@ -92,9 +92,16 @@ CallableVersionOperator = ta.Callable[['Version', str], bool]
92
92
  CommandT = ta.TypeVar('CommandT', bound='Command')
93
93
  CommandOutputT = ta.TypeVar('CommandOutputT', bound='Command.Output')
94
94
 
95
+ # deploy/paths.py
96
+ DeployPathKind = ta.Literal['dir', 'file'] # ta.TypeAlias
97
+ DeployPathSpec = ta.Literal['app', 'tag'] # ta.TypeAlias
98
+
95
99
  # ../../omlish/argparse/cli.py
96
100
  ArgparseCommandFn = ta.Callable[[], ta.Optional[int]] # ta.TypeAlias
97
101
 
102
+ # ../../omlish/lite/contextmanagers.py
103
+ ExitStackedT = ta.TypeVar('ExitStackedT', bound='ExitStacked')
104
+
98
105
  # ../../omlish/lite/inject.py
99
106
  U = ta.TypeVar('U')
100
107
  InjectorKeyCls = ta.Union[type, ta.NewType]
@@ -528,19 +535,28 @@ class MainConfig:
528
535
 
529
536
 
530
537
  ########################################
531
- # ../system/config.py
538
+ # ../deploy/config.py
532
539
 
533
540
 
534
541
  @dc.dataclass(frozen=True)
535
- class SystemConfig:
536
- platform: ta.Optional[str] = None
542
+ class DeployConfig:
543
+ deploy_home: ta.Optional[str] = None
537
544
 
538
545
 
539
546
  ########################################
540
- # ../system/types.py
547
+ # ../deploy/types.py
541
548
 
542
549
 
543
- SystemPlatform = ta.NewType('SystemPlatform', str)
550
+ DeployHome = ta.NewType('DeployHome', str)
551
+
552
+ DeployApp = ta.NewType('DeployApp', str)
553
+ DeployTag = ta.NewType('DeployTag', str)
554
+ DeployRev = ta.NewType('DeployRev', str)
555
+
556
+
557
+ class DeployAppTag(ta.NamedTuple):
558
+ app: DeployApp
559
+ tag: DeployTag
544
560
 
545
561
 
546
562
  ########################################
@@ -1221,8 +1237,6 @@ def async_cached_nullary(fn): # ta.Callable[..., T]) -> ta.Callable[..., T]:
1221
1237
  """
1222
1238
  TODO:
1223
1239
  - def maybe(v: lang.Maybe[T])
1224
- - patch / override lite.check ?
1225
- - checker interface?
1226
1240
  """
1227
1241
 
1228
1242
 
@@ -1937,6 +1951,489 @@ def set_process_deathsig(sig: int) -> bool:
1937
1951
  return False
1938
1952
 
1939
1953
 
1954
+ ########################################
1955
+ # ../../../omlish/os/linux.py
1956
+ """
1957
+ ➜ ~ cat /etc/os-release
1958
+ NAME="Amazon Linux"
1959
+ VERSION="2"
1960
+ ID="amzn"
1961
+ ID_LIKE="centos rhel fedora"
1962
+ VERSION_ID="2"
1963
+ PRETTY_NAME="Amazon Linux 2"
1964
+
1965
+ ➜ ~ cat /etc/os-release
1966
+ PRETTY_NAME="Ubuntu 22.04.5 LTS"
1967
+ NAME="Ubuntu"
1968
+ VERSION_ID="22.04"
1969
+ VERSION="22.04.5 LTS (Jammy Jellyfish)"
1970
+ VERSION_CODENAME=jammy
1971
+ ID=ubuntu
1972
+ ID_LIKE=debian
1973
+ UBUNTU_CODENAME=jammy
1974
+
1975
+ ➜ omlish git:(master) docker run -i python:3.12 cat /etc/os-release
1976
+ PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
1977
+ NAME="Debian GNU/Linux"
1978
+ VERSION_ID="12"
1979
+ VERSION="12 (bookworm)"
1980
+ VERSION_CODENAME=bookworm
1981
+ ID=debian
1982
+ """
1983
+
1984
+
1985
+ @dc.dataclass(frozen=True)
1986
+ class LinuxOsRelease:
1987
+ """
1988
+ https://man7.org/linux/man-pages/man5/os-release.5.html
1989
+ """
1990
+
1991
+ raw: ta.Mapping[str, str]
1992
+
1993
+ # General information identifying the operating system
1994
+
1995
+ @property
1996
+ def name(self) -> str:
1997
+ """
1998
+ A string identifying the operating system, without a version component, and suitable for presentation to the
1999
+ user. If not set, a default of "NAME=Linux" may be used.
2000
+
2001
+ Examples: "NAME=Fedora", "NAME="Debian GNU/Linux"".
2002
+ """
2003
+
2004
+ return self.raw['NAME']
2005
+
2006
+ @property
2007
+ def id(self) -> str:
2008
+ """
2009
+ A lower-case string (no spaces or other characters outside of 0-9, a-z, ".", "_" and "-") identifying the
2010
+ operating system, excluding any version information and suitable for processing by scripts or usage in generated
2011
+ filenames. If not set, a default of "ID=linux" may be used. Note that even though this string may not include
2012
+ characters that require shell quoting, quoting may nevertheless be used.
2013
+
2014
+ Examples: "ID=fedora", "ID=debian".
2015
+ """
2016
+
2017
+ return self.raw['ID']
2018
+
2019
+ @property
2020
+ def id_like(self) -> str:
2021
+ """
2022
+ A space-separated list of operating system identifiers in the same syntax as the ID= setting. It should list
2023
+ identifiers of operating systems that are closely related to the local operating system in regards to packaging
2024
+ and programming interfaces, for example listing one or more OS identifiers the local OS is a derivative from. An
2025
+ OS should generally only list other OS identifiers it itself is a derivative of, and not any OSes that are
2026
+ derived from it, though symmetric relationships are possible. Build scripts and similar should check this
2027
+ variable if they need to identify the local operating system and the value of ID= is not recognized. Operating
2028
+ systems should be listed in order of how closely the local operating system relates to the listed ones, starting
2029
+ with the closest. This field is optional.
2030
+
2031
+ Examples: for an operating system with "ID=centos", an assignment of "ID_LIKE="rhel fedora"" would be
2032
+ appropriate. For an operating system with "ID=ubuntu", an assignment of "ID_LIKE=debian" is appropriate.
2033
+ """
2034
+
2035
+ return self.raw['ID_LIKE']
2036
+
2037
+ @property
2038
+ def pretty_name(self) -> str:
2039
+ """
2040
+ A pretty operating system name in a format suitable for presentation to the user. May or may not contain a
2041
+ release code name or OS version of some kind, as suitable. If not set, a default of "PRETTY_NAME="Linux"" may be
2042
+ used
2043
+
2044
+ Example: "PRETTY_NAME="Fedora 17 (Beefy Miracle)"".
2045
+ """
2046
+
2047
+ return self.raw['PRETTY_NAME']
2048
+
2049
+ @property
2050
+ def cpe_name(self) -> str:
2051
+ """
2052
+ A CPE name for the operating system, in URI binding syntax, following the Common Platform Enumeration
2053
+ Specification[4] as proposed by the NIST. This field is optional.
2054
+
2055
+ Example: "CPE_NAME="cpe:/o:fedoraproject:fedora:17""
2056
+ """
2057
+
2058
+ return self.raw['CPE_NAME']
2059
+
2060
+ @property
2061
+ def variant(self) -> str:
2062
+ """
2063
+ A string identifying a specific variant or edition of the operating system suitable for presentation to the
2064
+ user. This field may be used to inform the user that the configuration of this system is subject to a specific
2065
+ divergent set of rules or default configuration settings. This field is optional and may not be implemented on
2066
+ all systems.
2067
+
2068
+ Examples: "VARIANT="Server Edition"", "VARIANT="Smart Refrigerator Edition"".
2069
+
2070
+ Note: this field is for display purposes only. The VARIANT_ID field should be used for making programmatic
2071
+ decisions.
2072
+
2073
+ Added in version 220.
2074
+ """
2075
+
2076
+ return self.raw['VARIANT']
2077
+
2078
+ @property
2079
+ def variant_id(self) -> str:
2080
+ """
2081
+ A lower-case string (no spaces or other characters outside of 0-9, a-z, ".", "_" and "-"), identifying a
2082
+ specific variant or edition of the operating system. This may be interpreted by other packages in order to
2083
+ determine a divergent default configuration. This field is optional and may not be implemented on all systems.
2084
+
2085
+ Examples: "VARIANT_ID=server", "VARIANT_ID=embedded".
2086
+
2087
+ Added in version 220.
2088
+ """
2089
+
2090
+ return self.raw['variant_id']
2091
+
2092
+ # Information about the version of the operating system
2093
+
2094
+ @property
2095
+ def version(self) -> str:
2096
+ """
2097
+ A string identifying the operating system version, excluding any OS name information, possibly including a
2098
+ release code name, and suitable for presentation to the user. This field is optional.
2099
+
2100
+ Examples: "VERSION=17", "VERSION="17 (Beefy Miracle)"".
2101
+ """
2102
+
2103
+ return self.raw['VERSION']
2104
+
2105
+ @property
2106
+ def version_id(self) -> str:
2107
+ """
2108
+ A lower-case string (mostly numeric, no spaces or other characters outside of 0-9, a-z, ".", "_" and "-")
2109
+ identifying the operating system version, excluding any OS name information or release code name, and suitable
2110
+ for processing by scripts or usage in generated filenames. This field is optional.
2111
+
2112
+ Examples: "VERSION_ID=17", "VERSION_ID=11.04".
2113
+ """
2114
+
2115
+ return self.raw['VERSION_ID']
2116
+
2117
+ @property
2118
+ def version_codename(self) -> str:
2119
+ """
2120
+ A lower-case string (no spaces or other characters outside of 0-9, a-z, ".", "_" and "-") identifying the
2121
+ operating system release code name, excluding any OS name information or release version, and suitable for
2122
+ processing by scripts or usage in generated filenames. This field is optional and may not be implemented on all
2123
+ systems.
2124
+
2125
+ Examples: "VERSION_CODENAME=buster", "VERSION_CODENAME=xenial".
2126
+
2127
+ Added in version 231.
2128
+ """
2129
+
2130
+ return self.raw['VERSION_CODENAME']
2131
+
2132
+ @property
2133
+ def build_id(self) -> str:
2134
+ """
2135
+ A string uniquely identifying the system image originally used as the installation base. In most cases,
2136
+ VERSION_ID or IMAGE_ID+IMAGE_VERSION are updated when the entire system image is replaced during an update.
2137
+ BUILD_ID may be used in distributions where the original installation image version is important: VERSION_ID
2138
+ would change during incremental system updates, but BUILD_ID would not. This field is optional.
2139
+
2140
+ Examples: "BUILD_ID="2013-03-20.3"", "BUILD_ID=201303203".
2141
+
2142
+ Added in version 200.
2143
+ """
2144
+
2145
+ return self.raw['BUILD_ID']
2146
+
2147
+ @property
2148
+ def image_id(self) -> str:
2149
+ """
2150
+ A lower-case string (no spaces or other characters outside of 0-9, a-z, ".", "_" and "-"), identifying a
2151
+ specific image of the operating system. This is supposed to be used for environments where OS images are
2152
+ prepared, built, shipped and updated as comprehensive, consistent OS images. This field is optional and may not
2153
+ be implemented on all systems, in particularly not on those that are not managed via images but put together and
2154
+ updated from individual packages and on the local system.
2155
+
2156
+ Examples: "IMAGE_ID=vendorx-cashier-system", "IMAGE_ID=netbook-image".
2157
+
2158
+ Added in version 249.
2159
+ """
2160
+
2161
+ return self.raw['IMAGE_ID']
2162
+
2163
+ @property
2164
+ def image_version(self) -> str:
2165
+ """
2166
+ A lower-case string (mostly numeric, no spaces or other characters outside of 0-9, a-z, ".", "_" and "-")
2167
+ identifying the OS image version. This is supposed to be used together with IMAGE_ID described above, to discern
2168
+ different versions of the same image.
2169
+
2170
+ Examples: "IMAGE_VERSION=33", "IMAGE_VERSION=47.1rc1".
2171
+
2172
+ Added in version 249.
2173
+ """
2174
+
2175
+ return self.raw['IMAGE_VERSION']
2176
+
2177
+ # To summarize: if the image updates are built and shipped as comprehensive units, IMAGE_ID+IMAGE_VERSION is the
2178
+ # best fit. Otherwise, if updates eventually completely replace previously installed contents, as in a typical
2179
+ # binary distribution, VERSION_ID should be used to identify major releases of the operating system. BUILD_ID may
2180
+ # be used instead or in addition to VERSION_ID when the original system image version is important.
2181
+
2182
+ #
2183
+
2184
+ # Presentation information and links
2185
+
2186
+ # Links to resources on the Internet related to the operating system. HOME_URL= should refer to the homepage of the
2187
+ # operating system, or alternatively some homepage of the specific version of the operating system.
2188
+ # DOCUMENTATION_URL= should refer to the main documentation page for this operating system. SUPPORT_URL= should
2189
+ # refer to the main support page for the operating system, if there is any. This is primarily intended for operating
2190
+ # systems which vendors provide support for. BUG_REPORT_URL= should refer to the main bug reporting page for the
2191
+ # operating system, if there is any. This is primarily intended for operating systems that rely on community QA.
2192
+ # PRIVACY_POLICY_URL= should refer to the main privacy policy page for the operating system, if there is any. These
2193
+ # settings are optional, and providing only some of these settings is common. These URLs are intended to be exposed
2194
+ # in "About this system" UIs behind links with captions such as "About this Operating System", "Obtain Support",
2195
+ # "Report a Bug", or "Privacy Policy". The values should be in RFC3986 format[5], and should be "http:" or "https:"
2196
+ # URLs, and possibly "mailto:" or "tel:". Only one URL shall be listed in each setting. If multiple resources need
2197
+ # to be referenced, it is recommended to provide an online landing page linking all available resources.
2198
+
2199
+ # Examples: "HOME_URL="https://fedoraproject.org/"", "BUG_REPORT_URL="https://bugzilla.redhat.com/"".
2200
+
2201
+ @property
2202
+ def home_url(self) -> str:
2203
+ return self.raw['HOME_URL']
2204
+
2205
+ @property
2206
+ def documentation_url(self) -> str:
2207
+ return self.raw['DOCUMENTATION_URL']
2208
+
2209
+ @property
2210
+ def support_url(self) -> str:
2211
+ return self.raw['SUPPORT_URL']
2212
+
2213
+ @property
2214
+ def bug_report_url(self) -> str:
2215
+ return self.raw['BUG_REPORT_URL']
2216
+
2217
+ @property
2218
+ def privacy_policy_url(self) -> str:
2219
+ return self.raw['PRIVACY_POLICY_URL']
2220
+
2221
+ @property
2222
+ def support_end(self) -> str:
2223
+ """
2224
+ The date at which support for this version of the OS ends. (What exactly "lack of support" means varies between
2225
+ vendors, but generally users should assume that updates, including security fixes, will not be provided.) The
2226
+ value is a date in the ISO 8601 format "YYYY-MM-DD", and specifies the first day on which support is not
2227
+ provided.
2228
+
2229
+ For example, "SUPPORT_END=2001-01-01" means that the system was supported until the end of the last day of the
2230
+ previous millennium.
2231
+
2232
+ Added in version 252.
2233
+ """
2234
+
2235
+ return self.raw['SUPPORT_END']
2236
+
2237
+ @property
2238
+ def logo(self) -> str:
2239
+ """
2240
+ A string, specifying the name of an icon as defined by freedesktop.org Icon Theme Specification[6]. This can be
2241
+ used by graphical applications to display an operating system's or distributor's logo. This field is optional
2242
+ and may not necessarily be implemented on all systems.
2243
+
2244
+ Examples: "LOGO=fedora-logo", "LOGO=distributor-logo-opensuse"
2245
+
2246
+ Added in version 240.
2247
+ """
2248
+
2249
+ return self.raw['LOGO']
2250
+
2251
+ @property
2252
+ def ansi_color(self) -> str:
2253
+ """
2254
+ A suggested presentation color when showing the OS name on the console. This should be specified as string
2255
+ suitable for inclusion in the ESC [ m ANSI/ECMA-48 escape code for setting graphical rendition. This field is
2256
+ optional.
2257
+
2258
+ Examples: "ANSI_COLOR="0;31"" for red, "ANSI_COLOR="1;34"" for light blue, or "ANSI_COLOR="0;38;2;60;110;180""
2259
+ for Fedora blue.
2260
+ """
2261
+
2262
+ return self.raw['ANSI_COLOR']
2263
+
2264
+ @property
2265
+ def vendor_name(self) -> str:
2266
+ """
2267
+ The name of the OS vendor. This is the name of the organization or company which produces the OS. This field is
2268
+ optional.
2269
+
2270
+ This name is intended to be exposed in "About this system" UIs or software update UIs when needed to distinguish
2271
+ the OS vendor from the OS itself. It is intended to be human readable.
2272
+
2273
+ Examples: "VENDOR_NAME="Fedora Project"" for Fedora Linux, "VENDOR_NAME="Canonical"" for Ubuntu.
2274
+
2275
+ Added in version 254.
2276
+ """
2277
+
2278
+ return self.raw['VENDOR_NAME']
2279
+
2280
+ @property
2281
+ def vendor_url(self) -> str:
2282
+ """
2283
+ The homepage of the OS vendor. This field is optional. The VENDOR_NAME= field should be set if this one is,
2284
+ although clients must be robust against either field not being set.
2285
+
2286
+ The value should be in RFC3986 format[5], and should be "http:" or "https:" URLs. Only one URL shall be listed
2287
+ in the setting.
2288
+
2289
+ Examples: "VENDOR_URL="https://fedoraproject.org/"", "VENDOR_URL="https://canonical.com/"".
2290
+
2291
+ Added in version 254.
2292
+ """
2293
+
2294
+ return self.raw['VENDOR_URL']
2295
+
2296
+ # Distribution-level defaults and metadata
2297
+
2298
+ @property
2299
+ def default_hostname(self) -> str:
2300
+ """
2301
+ A string specifying the hostname if hostname(5) is not present and no other configuration source specifies the
2302
+ hostname. Must be either a single DNS label (a string composed of 7-bit ASCII lower-case characters and no
2303
+ spaces or dots, limited to the format allowed for DNS domain name labels), or a sequence of such labels
2304
+ separated by single dots that forms a valid DNS FQDN. The hostname must be at most 64 characters, which is a
2305
+ Linux limitation (DNS allows longer names).
2306
+
2307
+ See org.freedesktop.hostname1(5) for a description of how systemd-hostnamed.service(8) determines the fallback
2308
+ hostname.
2309
+
2310
+ Added in version 248.
2311
+ """
2312
+
2313
+ return self.raw['DEFAULT_HOSTNAME']
2314
+
2315
+ @property
2316
+ def architecture(self) -> str:
2317
+ """
2318
+ A string that specifies which CPU architecture the userspace binaries require. The architecture identifiers are
2319
+ the same as for ConditionArchitecture= described in systemd.unit(5). The field is optional and should only be
2320
+ used when just single architecture is supported. It may provide redundant information when used in a GPT
2321
+ partition with a GUID type that already encodes the architecture. If this is not the case, the architecture
2322
+ should be specified in e.g., an extension image, to prevent an incompatible host from loading it.
2323
+
2324
+ Added in version 252.
2325
+ """
2326
+
2327
+ return self.raw['ARCHITECTURE']
2328
+
2329
+ @property
2330
+ def sysext_level(self) -> str:
2331
+ """
2332
+ A lower-case string (mostly numeric, no spaces or other characters outside of 0-9, a-z, ".", "_" and "-")
2333
+ identifying the operating system extensions support level, to indicate which extension images are supported. See
2334
+ /usr/lib/extension-release.d/extension-release.IMAGE, initrd[2] and systemd-sysext(8)) for more information.
2335
+
2336
+ Examples: "SYSEXT_LEVEL=2", "SYSEXT_LEVEL=15.14".
2337
+
2338
+ Added in version 248.
2339
+ """
2340
+
2341
+ return self.raw['SYSEXT_LEVEL']
2342
+
2343
+ @property
2344
+ def confext_level(self) -> str:
2345
+ """
2346
+ Semantically the same as SYSEXT_LEVEL= but for confext images. See
2347
+ /etc/extension-release.d/extension-release.IMAGE for more information.
2348
+
2349
+ Examples: "CONFEXT_LEVEL=2", "CONFEXT_LEVEL=15.14".
2350
+
2351
+ Added in version 254.
2352
+ """
2353
+
2354
+ return self.raw['CONFEXT_LEVEL']
2355
+
2356
+ @property
2357
+ def sysext_scope(self) -> str:
2358
+ """
2359
+ Takes a space-separated list of one or more of the strings "system", "initrd" and "portable". This field is only
2360
+ supported in extension-release.d/ files and indicates what environments the system extension is applicable to:
2361
+ i.e. to regular systems, to initrds, or to portable service images. If unspecified, "SYSEXT_SCOPE=system
2362
+ portable" is implied, i.e. any system extension without this field is applicable to regular systems and to
2363
+ portable service environments, but not to initrd environments.
2364
+
2365
+ Added in version 250.
2366
+ """
2367
+
2368
+ return self.raw['SYSEXT_SCOPE']
2369
+
2370
+ @property
2371
+ def confext_scope(self) -> str:
2372
+ """
2373
+ Semantically the same as SYSEXT_SCOPE= but for confext images.
2374
+
2375
+ Added in version 254.
2376
+ """
2377
+
2378
+ return self.raw['CONFEXT_SCOPE']
2379
+
2380
+ @property
2381
+ def portable_prefixes(self) -> str:
2382
+ """
2383
+ Takes a space-separated list of one or more valid prefix match strings for the Portable Services[3] logic. This
2384
+ field serves two purposes: it is informational, identifying portable service images as such (and thus allowing
2385
+ them to be distinguished from other OS images, such as bootable system images). It is also used when a portable
2386
+ service image is attached: the specified or implied portable service prefix is checked against the list
2387
+ specified here, to enforce restrictions how images may be attached to a system.
2388
+
2389
+ Added in version 250.
2390
+ """
2391
+
2392
+ return self.raw['PORTABLE_PREFIXES']
2393
+
2394
+ #
2395
+
2396
+ DEFAULT_PATHS: ta.ClassVar[ta.Sequence[str]] = [
2397
+ '/etc/os-release',
2398
+ '/usr/lib/os-release',
2399
+ ]
2400
+
2401
+ @classmethod
2402
+ def read(cls, *paths: str) -> ta.Optional['LinuxOsRelease']:
2403
+ for fp in (paths or cls.DEFAULT_PATHS):
2404
+ if not os.path.isfile(fp):
2405
+ continue
2406
+ with open(fp) as f:
2407
+ src = f.read()
2408
+ break
2409
+ else:
2410
+ return None
2411
+
2412
+ raw = cls._parse_os_release(src)
2413
+
2414
+ return cls(raw)
2415
+
2416
+ @classmethod
2417
+ def _parse_os_release(cls, src: str) -> ta.Mapping[str, str]:
2418
+ dct: ta.Dict[str, str] = {}
2419
+
2420
+ for l in src.splitlines():
2421
+ if not (l := l.strip()):
2422
+ continue
2423
+ if l.startswith('#') or '=' not in l:
2424
+ continue
2425
+
2426
+ k, _, v = l.partition('=')
2427
+ if k.startswith('"'):
2428
+ k = k[1:-1]
2429
+ if v.startswith('"'):
2430
+ v = v[1:-1]
2431
+
2432
+ dct[k] = v
2433
+
2434
+ return dct
2435
+
2436
+
1940
2437
  ########################################
1941
2438
  # ../../../omdev/packaging/specifiers.py
1942
2439
  # Copyright (c) Donald Stufft and individual contributors.
@@ -2610,43 +3107,264 @@ def build_command_name_map(crs: CommandRegistrations) -> CommandNameMap:
2610
3107
 
2611
3108
 
2612
3109
  ########################################
2613
- # ../remote/config.py
3110
+ # ../deploy/paths.py
3111
+ """
3112
+ ~deploy
3113
+ deploy.pid (flock)
3114
+ /app
3115
+ /<appspec> - shallow clone
3116
+ /conf
3117
+ /env
3118
+ <appspec>.env
3119
+ /nginx
3120
+ <appspec>.conf
3121
+ /supervisor
3122
+ <appspec>.conf
3123
+ /venv
3124
+ /<appspec>
3125
+
3126
+ ?
3127
+ /logs
3128
+ /wrmsr--omlish--<spec>
3129
+
3130
+ spec = <name>--<rev>--<when>
3131
+
3132
+ ==
3133
+
3134
+ for dn in [
3135
+ 'app',
3136
+ 'conf',
3137
+ 'conf/env',
3138
+ 'conf/nginx',
3139
+ 'conf/supervisor',
3140
+ 'venv',
3141
+ ]:
3142
+
3143
+ ==
2614
3144
 
3145
+ """
2615
3146
 
2616
- @dc.dataclass(frozen=True)
2617
- class RemoteConfig:
2618
- payload_file: ta.Optional[str] = None
2619
3147
 
2620
- set_pgid: bool = True
3148
+ ##
2621
3149
 
2622
- deathsig: ta.Optional[str] = 'KILL'
2623
3150
 
2624
- pycharm_remote_debug: ta.Optional[PycharmRemoteDebug] = None
3151
+ DEPLOY_PATH_SPEC_PLACEHOLDER = '@'
3152
+ DEPLOY_PATH_SPEC_SEPARATORS = '-.'
2625
3153
 
2626
- forward_logging: bool = True
3154
+ DEPLOY_PATH_SPECS: ta.FrozenSet[str] = frozenset([
3155
+ 'app',
3156
+ 'tag', # <rev>-<dt>
3157
+ ])
2627
3158
 
2628
- timebomb_delay_s: ta.Optional[float] = 60 * 60.
2629
3159
 
2630
- heartbeat_interval_s: float = 3.
3160
+ class DeployPathError(Exception):
3161
+ pass
2631
3162
 
2632
- use_in_process_remote_executor: bool = False
2633
3163
 
3164
+ @dc.dataclass(frozen=True)
3165
+ class DeployPathPart(abc.ABC): # noqa
3166
+ @property
3167
+ @abc.abstractmethod
3168
+ def kind(self) -> DeployPathKind:
3169
+ raise NotImplementedError
2634
3170
 
2635
- ########################################
2636
- # ../remote/payload.py
3171
+ @abc.abstractmethod
3172
+ def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
3173
+ raise NotImplementedError
2637
3174
 
2638
3175
 
2639
- RemoteExecutionPayloadFile = ta.NewType('RemoteExecutionPayloadFile', str)
3176
+ #
2640
3177
 
2641
3178
 
2642
- @cached_nullary
2643
- def _get_self_src() -> str:
2644
- return inspect.getsource(sys.modules[__name__])
3179
+ class DirDeployPathPart(DeployPathPart, abc.ABC):
3180
+ @property
3181
+ def kind(self) -> DeployPathKind:
3182
+ return 'dir'
2645
3183
 
3184
+ @classmethod
3185
+ def parse(cls, s: str) -> 'DirDeployPathPart':
3186
+ if DEPLOY_PATH_SPEC_PLACEHOLDER in s:
3187
+ check.equal(s[0], DEPLOY_PATH_SPEC_PLACEHOLDER)
3188
+ return SpecDirDeployPathPart(s[1:])
3189
+ else:
3190
+ return ConstDirDeployPathPart(s)
2646
3191
 
2647
- def _is_src_amalg(src: str) -> bool:
2648
- for l in src.splitlines(): # noqa
2649
- if l.startswith('# @omlish-amalg-output '):
3192
+
3193
+ class FileDeployPathPart(DeployPathPart, abc.ABC):
3194
+ @property
3195
+ def kind(self) -> DeployPathKind:
3196
+ return 'file'
3197
+
3198
+ @classmethod
3199
+ def parse(cls, s: str) -> 'FileDeployPathPart':
3200
+ if DEPLOY_PATH_SPEC_PLACEHOLDER in s:
3201
+ check.equal(s[0], DEPLOY_PATH_SPEC_PLACEHOLDER)
3202
+ if not any(c in s for c in DEPLOY_PATH_SPEC_SEPARATORS):
3203
+ return SpecFileDeployPathPart(s[1:], '')
3204
+ else:
3205
+ p = min(f for c in DEPLOY_PATH_SPEC_SEPARATORS if (f := s.find(c)) > 0)
3206
+ return SpecFileDeployPathPart(s[1:p], s[p:])
3207
+ else:
3208
+ return ConstFileDeployPathPart(s)
3209
+
3210
+
3211
+ #
3212
+
3213
+
3214
+ @dc.dataclass(frozen=True)
3215
+ class ConstDeployPathPart(DeployPathPart, abc.ABC):
3216
+ name: str
3217
+
3218
+ def __post_init__(self) -> None:
3219
+ check.non_empty_str(self.name)
3220
+ check.not_in('/', self.name)
3221
+ check.not_in(DEPLOY_PATH_SPEC_PLACEHOLDER, self.name)
3222
+
3223
+ def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
3224
+ return self.name
3225
+
3226
+
3227
+ class ConstDirDeployPathPart(ConstDeployPathPart, DirDeployPathPart):
3228
+ pass
3229
+
3230
+
3231
+ class ConstFileDeployPathPart(ConstDeployPathPart, FileDeployPathPart):
3232
+ pass
3233
+
3234
+
3235
+ #
3236
+
3237
+
3238
+ @dc.dataclass(frozen=True)
3239
+ class SpecDeployPathPart(DeployPathPart, abc.ABC):
3240
+ spec: str # DeployPathSpec
3241
+
3242
+ def __post_init__(self) -> None:
3243
+ check.non_empty_str(self.spec)
3244
+ for c in [*DEPLOY_PATH_SPEC_SEPARATORS, DEPLOY_PATH_SPEC_PLACEHOLDER, '/']:
3245
+ check.not_in(c, self.spec)
3246
+ check.in_(self.spec, DEPLOY_PATH_SPECS)
3247
+
3248
+ def _render_spec(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
3249
+ if specs is not None:
3250
+ return specs[self.spec] # type: ignore
3251
+ else:
3252
+ return DEPLOY_PATH_SPEC_PLACEHOLDER + self.spec
3253
+
3254
+
3255
+ @dc.dataclass(frozen=True)
3256
+ class SpecDirDeployPathPart(SpecDeployPathPart, DirDeployPathPart):
3257
+ def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
3258
+ return self._render_spec(specs)
3259
+
3260
+
3261
+ @dc.dataclass(frozen=True)
3262
+ class SpecFileDeployPathPart(SpecDeployPathPart, FileDeployPathPart):
3263
+ suffix: str
3264
+
3265
+ def __post_init__(self) -> None:
3266
+ super().__post_init__()
3267
+ if self.suffix:
3268
+ for c in [DEPLOY_PATH_SPEC_PLACEHOLDER, '/']:
3269
+ check.not_in(c, self.suffix)
3270
+
3271
+ def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
3272
+ return self._render_spec(specs) + self.suffix
3273
+
3274
+
3275
+ ##
3276
+
3277
+
3278
+ @dc.dataclass(frozen=True)
3279
+ class DeployPath:
3280
+ parts: ta.Sequence[DeployPathPart]
3281
+
3282
+ def __post_init__(self) -> None:
3283
+ check.not_empty(self.parts)
3284
+ for p in self.parts[:-1]:
3285
+ check.equal(p.kind, 'dir')
3286
+
3287
+ pd = {}
3288
+ for i, p in enumerate(self.parts):
3289
+ if isinstance(p, SpecDeployPathPart):
3290
+ if p.spec in pd:
3291
+ raise DeployPathError('Duplicate specs in path', self)
3292
+ pd[p.spec] = i
3293
+
3294
+ if 'tag' in pd:
3295
+ if 'app' not in pd or pd['app'] >= pd['tag']:
3296
+ raise DeployPathError('Tag spec in path without preceding app', self)
3297
+
3298
+ @property
3299
+ def kind(self) -> ta.Literal['file', 'dir']:
3300
+ return self.parts[-1].kind
3301
+
3302
+ def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
3303
+ return os.path.join( # noqa
3304
+ *[p.render(specs) for p in self.parts],
3305
+ *([''] if self.kind == 'dir' else []),
3306
+ )
3307
+
3308
+ @classmethod
3309
+ def parse(cls, s: str) -> 'DeployPath':
3310
+ tail_parse: ta.Callable[[str], DeployPathPart]
3311
+ if s.endswith('/'):
3312
+ tail_parse = DirDeployPathPart.parse
3313
+ s = s[:-1]
3314
+ else:
3315
+ tail_parse = FileDeployPathPart.parse
3316
+ ps = check.non_empty_str(s).split('/')
3317
+ return cls([
3318
+ *([DirDeployPathPart.parse(p) for p in ps[:-1]] if len(ps) > 1 else []),
3319
+ tail_parse(ps[-1]),
3320
+ ])
3321
+
3322
+
3323
+ ##
3324
+
3325
+
3326
+ class DeployPathOwner(abc.ABC):
3327
+ @abc.abstractmethod
3328
+ def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
3329
+ raise NotImplementedError
3330
+
3331
+
3332
+ ########################################
3333
+ # ../remote/config.py
3334
+
3335
+
3336
+ @dc.dataclass(frozen=True)
3337
+ class RemoteConfig:
3338
+ payload_file: ta.Optional[str] = None
3339
+
3340
+ set_pgid: bool = True
3341
+
3342
+ deathsig: ta.Optional[str] = 'KILL'
3343
+
3344
+ pycharm_remote_debug: ta.Optional[PycharmRemoteDebug] = None
3345
+
3346
+ forward_logging: bool = True
3347
+
3348
+ timebomb_delay_s: ta.Optional[float] = 60 * 60.
3349
+
3350
+ heartbeat_interval_s: float = 3.
3351
+
3352
+
3353
+ ########################################
3354
+ # ../remote/payload.py
3355
+
3356
+
3357
+ RemoteExecutionPayloadFile = ta.NewType('RemoteExecutionPayloadFile', str)
3358
+
3359
+
3360
+ @cached_nullary
3361
+ def _get_self_src() -> str:
3362
+ return inspect.getsource(sys.modules[__name__])
3363
+
3364
+
3365
+ def _is_src_amalg(src: str) -> bool:
3366
+ for l in src.splitlines(): # noqa
3367
+ if l.startswith('# @omlish-amalg-output '):
2650
3368
  return True
2651
3369
  return False
2652
3370
 
@@ -2671,6 +3389,90 @@ def get_remote_payload_src(
2671
3389
  return importlib.resources.files(__package__.split('.')[0] + '.scripts').joinpath('manage.py').read_text()
2672
3390
 
2673
3391
 
3392
+ ########################################
3393
+ # ../targets/targets.py
3394
+ """
3395
+ It's desugaring. Subprocess and locals are only leafs. Retain an origin?
3396
+ ** TWO LAYERS ** - ManageTarget is user-facing, ConnectorTarget is bound, internal
3397
+ """
3398
+
3399
+
3400
+ ##
3401
+
3402
+
3403
+ class ManageTarget(abc.ABC): # noqa
3404
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
3405
+ super().__init_subclass__(**kwargs)
3406
+
3407
+ check.state(cls.__name__.endswith('ManageTarget'))
3408
+
3409
+
3410
+ #
3411
+
3412
+
3413
+ @dc.dataclass(frozen=True)
3414
+ class PythonRemoteManageTarget:
3415
+ DEFAULT_PYTHON: ta.ClassVar[str] = 'python3'
3416
+ python: str = DEFAULT_PYTHON
3417
+
3418
+
3419
+ #
3420
+
3421
+
3422
+ class RemoteManageTarget(ManageTarget, abc.ABC):
3423
+ pass
3424
+
3425
+
3426
+ class PhysicallyRemoteManageTarget(RemoteManageTarget, abc.ABC):
3427
+ pass
3428
+
3429
+
3430
+ class LocalManageTarget(ManageTarget, abc.ABC):
3431
+ pass
3432
+
3433
+
3434
+ ##
3435
+
3436
+
3437
+ @dc.dataclass(frozen=True)
3438
+ class SshManageTarget(PhysicallyRemoteManageTarget, PythonRemoteManageTarget):
3439
+ host: ta.Optional[str] = None
3440
+ username: ta.Optional[str] = None
3441
+ key_file: ta.Optional[str] = None
3442
+
3443
+ def __post_init__(self) -> None:
3444
+ check.non_empty_str(self.host)
3445
+
3446
+
3447
+ ##
3448
+
3449
+
3450
+ @dc.dataclass(frozen=True)
3451
+ class DockerManageTarget(RemoteManageTarget, PythonRemoteManageTarget): # noqa
3452
+ image: ta.Optional[str] = None
3453
+ container_id: ta.Optional[str] = None
3454
+
3455
+ def __post_init__(self) -> None:
3456
+ check.arg(bool(self.image) ^ bool(self.container_id))
3457
+
3458
+
3459
+ ##
3460
+
3461
+
3462
+ @dc.dataclass(frozen=True)
3463
+ class InProcessManageTarget(LocalManageTarget):
3464
+ class Mode(enum.Enum):
3465
+ DIRECT = enum.auto()
3466
+ FAKE_REMOTE = enum.auto()
3467
+
3468
+ mode: Mode = Mode.DIRECT
3469
+
3470
+
3471
+ @dc.dataclass(frozen=True)
3472
+ class SubprocessManageTarget(LocalManageTarget, PythonRemoteManageTarget):
3473
+ pass
3474
+
3475
+
2674
3476
  ########################################
2675
3477
  # ../../../omlish/argparse/cli.py
2676
3478
  """
@@ -2943,6 +3745,78 @@ class ArgparseCli:
2943
3745
  return await fn()
2944
3746
 
2945
3747
 
3748
+ ########################################
3749
+ # ../../../omlish/lite/contextmanagers.py
3750
+
3751
+
3752
+ ##
3753
+
3754
+
3755
+ class ExitStacked:
3756
+ _exit_stack: ta.Optional[contextlib.ExitStack] = None
3757
+
3758
+ def __enter__(self: ExitStackedT) -> ExitStackedT:
3759
+ check.state(self._exit_stack is None)
3760
+ es = self._exit_stack = contextlib.ExitStack()
3761
+ es.__enter__()
3762
+ return self
3763
+
3764
+ def __exit__(self, exc_type, exc_val, exc_tb):
3765
+ if (es := self._exit_stack) is None:
3766
+ return None
3767
+ self._exit_contexts()
3768
+ return es.__exit__(exc_type, exc_val, exc_tb)
3769
+
3770
+ def _exit_contexts(self) -> None:
3771
+ pass
3772
+
3773
+ def _enter_context(self, cm: ta.ContextManager[T]) -> T:
3774
+ es = check.not_none(self._exit_stack)
3775
+ return es.enter_context(cm)
3776
+
3777
+
3778
+ ##
3779
+
3780
+
3781
+ @contextlib.contextmanager
3782
+ def defer(fn: ta.Callable) -> ta.Generator[ta.Callable, None, None]:
3783
+ try:
3784
+ yield fn
3785
+ finally:
3786
+ fn()
3787
+
3788
+
3789
+ @contextlib.contextmanager
3790
+ def attr_setting(obj, attr, val, *, default=None): # noqa
3791
+ not_set = object()
3792
+ orig = getattr(obj, attr, not_set)
3793
+ try:
3794
+ setattr(obj, attr, val)
3795
+ if orig is not not_set:
3796
+ yield orig
3797
+ else:
3798
+ yield default
3799
+ finally:
3800
+ if orig is not_set:
3801
+ delattr(obj, attr)
3802
+ else:
3803
+ setattr(obj, attr, orig)
3804
+
3805
+
3806
+ ##
3807
+
3808
+
3809
+ class aclosing(contextlib.AbstractAsyncContextManager): # noqa
3810
+ def __init__(self, thing):
3811
+ self.thing = thing
3812
+
3813
+ async def __aenter__(self):
3814
+ return self.thing
3815
+
3816
+ async def __aexit__(self, *exc_info):
3817
+ await self.thing.aclose()
3818
+
3819
+
2946
3820
  ########################################
2947
3821
  # ../../../omlish/lite/inject.py
2948
3822
 
@@ -4111,7 +4985,8 @@ def configure_standard_logging(
4111
4985
  """
4112
4986
  TODO:
4113
4987
  - pickle stdlib objs? have to pin to 3.8 pickle protocol, will be cross-version
4114
- - nonstrict toggle
4988
+ - namedtuple
4989
+ - literals
4115
4990
  """
4116
4991
 
4117
4992
 
@@ -4401,14 +5276,18 @@ class ObjMarshalerManager:
4401
5276
  ) -> ObjMarshaler:
4402
5277
  if isinstance(ty, type):
4403
5278
  if abc.ABC in ty.__bases__:
4404
- return PolymorphicObjMarshaler.of([ # type: ignore
5279
+ impls = [ity for ity in deep_subclasses(ty) if abc.ABC not in ity.__bases__] # type: ignore
5280
+ if all(ity.__qualname__.endswith(ty.__name__) for ity in impls):
5281
+ ins = {ity: snake_case(ity.__qualname__[:-len(ty.__name__)]) for ity in impls}
5282
+ else:
5283
+ ins = {ity: ity.__qualname__ for ity in impls}
5284
+ return PolymorphicObjMarshaler.of([
4405
5285
  PolymorphicObjMarshaler.Impl(
4406
5286
  ity,
4407
- ity.__qualname__,
5287
+ itn,
4408
5288
  rec(ity),
4409
5289
  )
4410
- for ity in deep_subclasses(ty)
4411
- if abc.ABC not in ity.__bases__
5290
+ for ity, itn in ins.items()
4412
5291
  ])
4413
5292
 
4414
5293
  if issubclass(ty, enum.Enum):
@@ -4655,41 +5534,6 @@ class Interp:
4655
5534
  version: InterpVersion
4656
5535
 
4657
5536
 
4658
- ########################################
4659
- # ../bootstrap.py
4660
-
4661
-
4662
- @dc.dataclass(frozen=True)
4663
- class MainBootstrap:
4664
- main_config: MainConfig = MainConfig()
4665
-
4666
- remote_config: RemoteConfig = RemoteConfig()
4667
-
4668
- system_config: SystemConfig = SystemConfig()
4669
-
4670
-
4671
- ########################################
4672
- # ../commands/execution.py
4673
-
4674
-
4675
- CommandExecutorMap = ta.NewType('CommandExecutorMap', ta.Mapping[ta.Type[Command], CommandExecutor])
4676
-
4677
-
4678
- class LocalCommandExecutor(CommandExecutor):
4679
- def __init__(
4680
- self,
4681
- *,
4682
- command_executors: CommandExecutorMap,
4683
- ) -> None:
4684
- super().__init__()
4685
-
4686
- self._command_executors = command_executors
4687
-
4688
- async def execute(self, cmd: Command) -> Command.Output:
4689
- ce: CommandExecutor = self._command_executors[type(cmd)]
4690
- return await ce.execute(cmd)
4691
-
4692
-
4693
5537
  ########################################
4694
5538
  # ../commands/marshal.py
4695
5539
 
@@ -4715,6 +5559,34 @@ def install_command_marshaling(
4715
5559
  )
4716
5560
 
4717
5561
 
5562
+ ########################################
5563
+ # ../commands/ping.py
5564
+
5565
+
5566
+ ##
5567
+
5568
+
5569
+ @dc.dataclass(frozen=True)
5570
+ class PingCommand(Command['PingCommand.Output']):
5571
+ time: float = dc.field(default_factory=time.time)
5572
+
5573
+ @dc.dataclass(frozen=True)
5574
+ class Output(Command.Output):
5575
+ time: float
5576
+
5577
+
5578
+ class PingCommandExecutor(CommandExecutor[PingCommand, PingCommand.Output]):
5579
+ async def execute(self, cmd: PingCommand) -> PingCommand.Output:
5580
+ return PingCommand.Output(cmd.time)
5581
+
5582
+
5583
+ ########################################
5584
+ # ../commands/types.py
5585
+
5586
+
5587
+ CommandExecutorMap = ta.NewType('CommandExecutorMap', ta.Mapping[ta.Type[Command], CommandExecutor])
5588
+
5589
+
4718
5590
  ########################################
4719
5591
  # ../deploy/commands.py
4720
5592
 
@@ -4828,24 +5700,72 @@ class RemoteChannelImpl(RemoteChannel):
4828
5700
 
4829
5701
 
4830
5702
  ########################################
4831
- # ../system/commands.py
5703
+ # ../system/platforms.py
4832
5704
 
4833
5705
 
4834
5706
  ##
4835
5707
 
4836
5708
 
4837
5709
  @dc.dataclass(frozen=True)
4838
- class CheckSystemPackageCommand(Command['CheckSystemPackageCommand.Output']):
4839
- @dc.dataclass(frozen=True)
4840
- class Output(Command.Output):
4841
- pass
5710
+ class Platform(abc.ABC): # noqa
5711
+ pass
4842
5712
 
4843
5713
 
4844
- class CheckSystemPackageCommandExecutor(CommandExecutor[CheckSystemPackageCommand, CheckSystemPackageCommand.Output]):
4845
- async def execute(self, cmd: CheckSystemPackageCommand) -> CheckSystemPackageCommand.Output:
4846
- log.info('Checking system package!')
5714
+ class LinuxPlatform(Platform, abc.ABC):
5715
+ pass
5716
+
5717
+
5718
+ class UbuntuPlatform(LinuxPlatform):
5719
+ pass
5720
+
5721
+
5722
+ class AmazonLinuxPlatform(LinuxPlatform):
5723
+ pass
5724
+
5725
+
5726
+ class GenericLinuxPlatform(LinuxPlatform):
5727
+ pass
5728
+
5729
+
5730
+ class DarwinPlatform(Platform):
5731
+ pass
5732
+
5733
+
5734
+ class UnknownPlatform(Platform):
5735
+ pass
5736
+
5737
+
5738
+ ##
5739
+
5740
+
5741
+ def _detect_system_platform() -> Platform:
5742
+ plat = sys.platform
5743
+
5744
+ if plat == 'linux':
5745
+ if (osr := LinuxOsRelease.read()) is None:
5746
+ return GenericLinuxPlatform()
5747
+
5748
+ if osr.id == 'amzn':
5749
+ return AmazonLinuxPlatform()
5750
+
5751
+ elif osr.id == 'ubuntu':
5752
+ return UbuntuPlatform()
5753
+
5754
+ else:
5755
+ return GenericLinuxPlatform()
4847
5756
 
4848
- return CheckSystemPackageCommand.Output()
5757
+ elif plat == 'darwin':
5758
+ return DarwinPlatform()
5759
+
5760
+ else:
5761
+ return UnknownPlatform()
5762
+
5763
+
5764
+ @cached_nullary
5765
+ def detect_system_platform() -> Platform:
5766
+ platform = _detect_system_platform()
5767
+ log.info('Detected platform: %r', platform)
5768
+ return platform
4849
5769
 
4850
5770
 
4851
5771
  ########################################
@@ -5032,6 +5952,25 @@ def subprocess_close(
5032
5952
  proc.wait(timeout)
5033
5953
 
5034
5954
 
5955
+ ########################################
5956
+ # ../commands/local.py
5957
+
5958
+
5959
+ class LocalCommandExecutor(CommandExecutor):
5960
+ def __init__(
5961
+ self,
5962
+ *,
5963
+ command_executors: CommandExecutorMap,
5964
+ ) -> None:
5965
+ super().__init__()
5966
+
5967
+ self._command_executors = command_executors
5968
+
5969
+ async def execute(self, cmd: Command) -> Command.Output:
5970
+ ce: CommandExecutor = self._command_executors[type(cmd)]
5971
+ return await ce.execute(cmd)
5972
+
5973
+
5035
5974
  ########################################
5036
5975
  # ../remote/execution.py
5037
5976
  """
@@ -5409,7 +6348,7 @@ class RemoteCommandExecutor(CommandExecutor):
5409
6348
  self,
5410
6349
  cmd: Command,
5411
6350
  *,
5412
- log: ta.Optional[logging.Logger] = None,
6351
+ log: ta.Optional[logging.Logger] = None, # noqa
5413
6352
  omit_exc_object: bool = False,
5414
6353
  ) -> CommandOutputOrException:
5415
6354
  try:
@@ -5429,6 +6368,15 @@ class RemoteCommandExecutor(CommandExecutor):
5429
6368
  return r
5430
6369
 
5431
6370
 
6371
+ ########################################
6372
+ # ../system/config.py
6373
+
6374
+
6375
+ @dc.dataclass(frozen=True)
6376
+ class SystemConfig:
6377
+ platform: ta.Optional[Platform] = None
6378
+
6379
+
5432
6380
  ########################################
5433
6381
  # ../../../omlish/lite/asyncio/subprocesses.py
5434
6382
 
@@ -5591,6 +6539,13 @@ async def asyncio_subprocess_communicate(
5591
6539
  return await AsyncioProcessCommunicator(proc).communicate(input, timeout) # noqa
5592
6540
 
5593
6541
 
6542
+ @dc.dataclass(frozen=True)
6543
+ class AsyncioSubprocessOutput:
6544
+ proc: asyncio.subprocess.Process
6545
+ stdout: ta.Optional[bytes]
6546
+ stderr: ta.Optional[bytes]
6547
+
6548
+
5594
6549
  async def asyncio_subprocess_run(
5595
6550
  *args: str,
5596
6551
  input: ta.Any = None, # noqa
@@ -5598,7 +6553,7 @@ async def asyncio_subprocess_run(
5598
6553
  check: bool = False, # noqa
5599
6554
  capture_output: ta.Optional[bool] = None,
5600
6555
  **kwargs: ta.Any,
5601
- ) -> ta.Tuple[ta.Optional[bytes], ta.Optional[bytes]]:
6556
+ ) -> AsyncioSubprocessOutput:
5602
6557
  if capture_output:
5603
6558
  kwargs.setdefault('stdout', subprocess.PIPE)
5604
6559
  kwargs.setdefault('stderr', subprocess.PIPE)
@@ -5617,7 +6572,11 @@ async def asyncio_subprocess_run(
5617
6572
  stderr=stderr,
5618
6573
  )
5619
6574
 
5620
- return stdout, stderr
6575
+ return AsyncioSubprocessOutput(
6576
+ proc,
6577
+ stdout,
6578
+ stderr,
6579
+ )
5621
6580
 
5622
6581
 
5623
6582
  ##
@@ -5630,7 +6589,7 @@ async def asyncio_subprocess_check_call(
5630
6589
  timeout: ta.Optional[float] = None,
5631
6590
  **kwargs: ta.Any,
5632
6591
  ) -> None:
5633
- _, _ = await asyncio_subprocess_run(
6592
+ await asyncio_subprocess_run(
5634
6593
  *args,
5635
6594
  stdout=stdout,
5636
6595
  input=input,
@@ -5646,7 +6605,7 @@ async def asyncio_subprocess_check_output(
5646
6605
  timeout: ta.Optional[float] = None,
5647
6606
  **kwargs: ta.Any,
5648
6607
  ) -> bytes:
5649
- stdout, stderr = await asyncio_subprocess_run(
6608
+ out = await asyncio_subprocess_run(
5650
6609
  *args,
5651
6610
  stdout=asyncio.subprocess.PIPE,
5652
6611
  input=input,
@@ -5655,7 +6614,7 @@ async def asyncio_subprocess_check_output(
5655
6614
  **kwargs,
5656
6615
  )
5657
6616
 
5658
- return check.not_none(stdout)
6617
+ return check.not_none(out.stdout)
5659
6618
 
5660
6619
 
5661
6620
  async def asyncio_subprocess_check_output_str(*args: str, **kwargs: ta.Any) -> str:
@@ -5811,6 +6770,21 @@ class InterpInspector:
5811
6770
  INTERP_INSPECTOR = InterpInspector()
5812
6771
 
5813
6772
 
6773
+ ########################################
6774
+ # ../bootstrap.py
6775
+
6776
+
6777
+ @dc.dataclass(frozen=True)
6778
+ class MainBootstrap:
6779
+ main_config: MainConfig = MainConfig()
6780
+
6781
+ deploy_config: DeployConfig = DeployConfig()
6782
+
6783
+ remote_config: RemoteConfig = RemoteConfig()
6784
+
6785
+ system_config: SystemConfig = SystemConfig()
6786
+
6787
+
5814
6788
  ########################################
5815
6789
  # ../commands/subprocess.py
5816
6790
 
@@ -5882,146 +6856,190 @@ class SubprocessCommandExecutor(CommandExecutor[SubprocessCommand, SubprocessCom
5882
6856
 
5883
6857
 
5884
6858
  ########################################
5885
- # ../remote/_main.py
6859
+ # ../deploy/git.py
6860
+ """
6861
+ TODO:
6862
+ - 'repos'?
6863
+
6864
+ git/github.com/wrmsr/omlish <- bootstrap repo
6865
+ - shallow clone off bootstrap into /apps
6866
+
6867
+ github.com/wrmsr/omlish@rev
6868
+ """
5886
6869
 
5887
6870
 
5888
6871
  ##
5889
6872
 
5890
6873
 
5891
- class _RemoteExecutionLogHandler(logging.Handler):
5892
- def __init__(self, fn: ta.Callable[[str], None]) -> None:
5893
- super().__init__()
5894
- self._fn = fn
6874
+ @dc.dataclass(frozen=True)
6875
+ class DeployGitRepo:
6876
+ host: ta.Optional[str] = None
6877
+ username: ta.Optional[str] = None
6878
+ path: ta.Optional[str] = None
5895
6879
 
5896
- def emit(self, record):
5897
- msg = self.format(record)
5898
- self._fn(msg)
6880
+ def __post_init__(self) -> None:
6881
+ check.not_in('..', check.non_empty_str(self.host))
6882
+ check.not_in('.', check.non_empty_str(self.path))
6883
+
6884
+
6885
+ @dc.dataclass(frozen=True)
6886
+ class DeployGitSpec:
6887
+ repo: DeployGitRepo
6888
+ rev: DeployRev
5899
6889
 
5900
6890
 
5901
6891
  ##
5902
6892
 
5903
6893
 
5904
- class _RemoteExecutionMain:
6894
+ class DeployGitManager(DeployPathOwner):
5905
6895
  def __init__(
5906
6896
  self,
5907
- chan: RemoteChannel,
6897
+ *,
6898
+ deploy_home: DeployHome,
5908
6899
  ) -> None:
5909
6900
  super().__init__()
5910
6901
 
5911
- self._chan = chan
5912
-
5913
- self.__bootstrap: ta.Optional[MainBootstrap] = None
5914
- self.__injector: ta.Optional[Injector] = None
5915
-
5916
- @property
5917
- def _bootstrap(self) -> MainBootstrap:
5918
- return check.not_none(self.__bootstrap)
5919
-
5920
- @property
5921
- def _injector(self) -> Injector:
5922
- return check.not_none(self.__injector)
6902
+ self._deploy_home = deploy_home
6903
+ self._dir = os.path.join(deploy_home, 'git')
5923
6904
 
5924
- #
6905
+ self._repo_dirs: ta.Dict[DeployGitRepo, DeployGitManager.RepoDir] = {}
5925
6906
 
5926
- def _timebomb_main(
5927
- self,
5928
- delay_s: float,
5929
- *,
5930
- sig: int = signal.SIGINT,
5931
- code: int = 1,
5932
- ) -> None:
5933
- time.sleep(delay_s)
6907
+ def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
6908
+ return {
6909
+ DeployPath.parse('git'),
6910
+ }
5934
6911
 
5935
- if (pgid := os.getpgid(0)) == os.getpid():
5936
- os.killpg(pgid, sig)
6912
+ class RepoDir:
6913
+ def __init__(
6914
+ self,
6915
+ git: 'DeployGitManager',
6916
+ repo: DeployGitRepo,
6917
+ ) -> None:
6918
+ super().__init__()
5937
6919
 
5938
- os._exit(code) # noqa
6920
+ self._git = git
6921
+ self._repo = repo
6922
+ self._dir = os.path.join(
6923
+ self._git._dir, # noqa
6924
+ check.non_empty_str(repo.host),
6925
+ check.non_empty_str(repo.path),
6926
+ )
5939
6927
 
5940
- @cached_nullary
5941
- def _timebomb_thread(self) -> ta.Optional[threading.Thread]:
5942
- if (tbd := self._bootstrap.remote_config.timebomb_delay_s) is None:
5943
- return None
6928
+ @property
6929
+ def repo(self) -> DeployGitRepo:
6930
+ return self._repo
5944
6931
 
5945
- thr = threading.Thread(
5946
- target=functools.partial(self._timebomb_main, tbd),
5947
- name=f'{self.__class__.__name__}.timebomb',
5948
- daemon=True,
5949
- )
6932
+ @property
6933
+ def url(self) -> str:
6934
+ if self._repo.username is not None:
6935
+ return f'{self._repo.username}@{self._repo.host}:{self._repo.path}'
6936
+ else:
6937
+ return f'https://{self._repo.host}/{self._repo.path}'
5950
6938
 
5951
- thr.start()
6939
+ async def _call(self, *cmd: str) -> None:
6940
+ await asyncio_subprocess_check_call(
6941
+ *cmd,
6942
+ cwd=self._dir,
6943
+ )
5952
6944
 
5953
- log.debug('Started timebomb thread: %r', thr)
6945
+ @async_cached_nullary
6946
+ async def init(self) -> None:
6947
+ os.makedirs(self._dir, exist_ok=True)
6948
+ if os.path.exists(os.path.join(self._dir, '.git')):
6949
+ return
5954
6950
 
5955
- return thr
6951
+ await self._call('git', 'init')
6952
+ await self._call('git', 'remote', 'add', 'origin', self.url)
5956
6953
 
5957
- #
6954
+ async def fetch(self, rev: DeployRev) -> None:
6955
+ await self.init()
6956
+ await self._call('git', 'fetch', '--depth=1', 'origin', rev)
5958
6957
 
5959
- @cached_nullary
5960
- def _log_handler(self) -> _RemoteLogHandler:
5961
- return _RemoteLogHandler(self._chan)
6958
+ async def checkout(self, rev: DeployRev, dst_dir: str) -> None:
6959
+ check.state(not os.path.exists(dst_dir))
5962
6960
 
5963
- #
6961
+ await self.fetch(rev)
5964
6962
 
5965
- async def _setup(self) -> None:
5966
- check.none(self.__bootstrap)
5967
- check.none(self.__injector)
6963
+ # FIXME: temp dir swap
6964
+ os.makedirs(dst_dir)
5968
6965
 
5969
- # Bootstrap
6966
+ dst_call = functools.partial(asyncio_subprocess_check_call, cwd=dst_dir)
6967
+ await dst_call('git', 'init')
5970
6968
 
5971
- self.__bootstrap = check.not_none(await self._chan.recv_obj(MainBootstrap))
6969
+ await dst_call('git', 'remote', 'add', 'local', self._dir)
6970
+ await dst_call('git', 'fetch', '--depth=1', 'local', rev)
6971
+ await dst_call('git', 'checkout', rev)
5972
6972
 
5973
- if (prd := self._bootstrap.remote_config.pycharm_remote_debug) is not None:
5974
- pycharm_debug_connect(prd)
6973
+ def get_repo_dir(self, repo: DeployGitRepo) -> RepoDir:
6974
+ try:
6975
+ return self._repo_dirs[repo]
6976
+ except KeyError:
6977
+ repo_dir = self._repo_dirs[repo] = DeployGitManager.RepoDir(self, repo)
6978
+ return repo_dir
5975
6979
 
5976
- self.__injector = main_bootstrap(self._bootstrap)
6980
+ async def checkout(self, spec: DeployGitSpec, dst_dir: str) -> None:
6981
+ await self.get_repo_dir(spec.repo).checkout(spec.rev, dst_dir)
5977
6982
 
5978
- self._chan.set_marshaler(self._injector[ObjMarshalerManager])
5979
6983
 
5980
- # Post-bootstrap
6984
+ ########################################
6985
+ # ../deploy/venvs.py
6986
+ """
6987
+ TODO:
6988
+ - interp
6989
+ - share more code with pyproject?
6990
+ """
5981
6991
 
5982
- if self._bootstrap.remote_config.set_pgid:
5983
- if os.getpgid(0) != os.getpid():
5984
- log.debug('Setting pgid')
5985
- os.setpgid(0, 0)
5986
6992
 
5987
- if (ds := self._bootstrap.remote_config.deathsig) is not None:
5988
- log.debug('Setting deathsig: %s', ds)
5989
- set_process_deathsig(int(signal.Signals[f'SIG{ds.upper()}']))
6993
+ class DeployVenvManager(DeployPathOwner):
6994
+ def __init__(
6995
+ self,
6996
+ *,
6997
+ deploy_home: DeployHome,
6998
+ ) -> None:
6999
+ super().__init__()
5990
7000
 
5991
- self._timebomb_thread()
7001
+ self._deploy_home = deploy_home
7002
+ self._dir = os.path.join(deploy_home, 'venvs')
5992
7003
 
5993
- if self._bootstrap.remote_config.forward_logging:
5994
- log.debug('Installing log forwarder')
5995
- logging.root.addHandler(self._log_handler())
7004
+ def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
7005
+ return {
7006
+ DeployPath.parse('venvs/@app/@tag/'),
7007
+ }
5996
7008
 
5997
- #
7009
+ async def setup_venv(
7010
+ self,
7011
+ app_dir: str,
7012
+ venv_dir: str,
7013
+ *,
7014
+ use_uv: bool = True,
7015
+ ) -> None:
7016
+ sys_exe = 'python3'
5998
7017
 
5999
- async def run(self) -> None:
6000
- await self._setup()
7018
+ await asyncio_subprocess_check_call(sys_exe, '-m', 'venv', venv_dir)
6001
7019
 
6002
- executor = self._injector[LocalCommandExecutor]
7020
+ #
6003
7021
 
6004
- handler = _RemoteCommandHandler(self._chan, executor)
7022
+ venv_exe = os.path.join(venv_dir, 'bin', 'python3')
6005
7023
 
6006
- await handler.run()
7024
+ #
6007
7025
 
7026
+ reqs_txt = os.path.join(app_dir, 'requirements.txt')
6008
7027
 
6009
- def _remote_execution_main() -> None:
6010
- rt = pyremote_bootstrap_finalize() # noqa
7028
+ if os.path.isfile(reqs_txt):
7029
+ if use_uv:
7030
+ await asyncio_subprocess_check_call(venv_exe, '-m', 'pip', 'install', 'uv')
7031
+ pip_cmd = ['-m', 'uv', 'pip']
7032
+ else:
7033
+ pip_cmd = ['-m', 'pip']
6011
7034
 
6012
- async def inner() -> None:
6013
- input = await asyncio_open_stream_reader(rt.input) # noqa
6014
- output = await asyncio_open_stream_writer(rt.output)
7035
+ await asyncio_subprocess_check_call(venv_exe, *pip_cmd,'install', '-r', reqs_txt)
6015
7036
 
6016
- chan = RemoteChannelImpl(
6017
- input,
6018
- output,
7037
+ async def setup_app_venv(self, app_tag: DeployAppTag) -> None:
7038
+ await self.setup_venv(
7039
+ os.path.join(self._deploy_home, 'apps', app_tag.app, app_tag.tag),
7040
+ os.path.join(self._deploy_home, 'venvs', app_tag.app, app_tag.tag),
6019
7041
  )
6020
7042
 
6021
- await _RemoteExecutionMain(chan).run()
6022
-
6023
- asyncio.run(inner())
6024
-
6025
7043
 
6026
7044
  ########################################
6027
7045
  # ../remote/spawning.py
@@ -6196,25 +7214,24 @@ class AptSystemPackageManager(SystemPackageManager):
6196
7214
  }
6197
7215
 
6198
7216
  async def update(self) -> None:
6199
- await asyncio_subprocess_check_call('apt', 'update', env={**os.environ, **self._APT_ENV})
7217
+ await asyncio_subprocess_check_call('sudo', 'apt', 'update', env={**os.environ, **self._APT_ENV})
6200
7218
 
6201
7219
  async def upgrade(self) -> None:
6202
- await asyncio_subprocess_check_call('apt', 'upgrade', '-y', env={**os.environ, **self._APT_ENV})
7220
+ await asyncio_subprocess_check_call('sudo', 'apt', 'upgrade', '-y', env={**os.environ, **self._APT_ENV})
6203
7221
 
6204
7222
  async def install(self, *packages: SystemPackageOrStr) -> None:
6205
7223
  pns = [p.name if isinstance(p, SystemPackage) else p for p in packages] # FIXME: versions
6206
- await asyncio_subprocess_check_call('apt', 'install', '-y', *pns, env={**os.environ, **self._APT_ENV})
7224
+ await asyncio_subprocess_check_call('sudo', 'apt', 'install', '-y', *pns, env={**os.environ, **self._APT_ENV})
6207
7225
 
6208
7226
  async def query(self, *packages: SystemPackageOrStr) -> ta.Mapping[str, SystemPackage]:
6209
7227
  pns = [p.name if isinstance(p, SystemPackage) else p for p in packages]
6210
- cmd = ['dpkg-query', '-W', '-f=${Package}=${Version}\n', *pns]
6211
- stdout, stderr = await asyncio_subprocess_run(
6212
- *cmd,
7228
+ out = await asyncio_subprocess_run(
7229
+ 'dpkg-query', '-W', '-f=${Package}=${Version}\n', *pns,
6213
7230
  capture_output=True,
6214
7231
  check=False,
6215
7232
  )
6216
7233
  d: ta.Dict[str, SystemPackage] = {}
6217
- for l in check.not_none(stdout).decode('utf-8').strip().splitlines():
7234
+ for l in check.not_none(out.stdout).decode('utf-8').strip().splitlines():
6218
7235
  n, v = l.split('=', 1)
6219
7236
  d[n] = SystemPackage(
6220
7237
  name=n,
@@ -6223,6 +7240,33 @@ class AptSystemPackageManager(SystemPackageManager):
6223
7240
  return d
6224
7241
 
6225
7242
 
7243
+ class YumSystemPackageManager(SystemPackageManager):
7244
+ async def update(self) -> None:
7245
+ await asyncio_subprocess_check_call('sudo', 'yum', 'check-update')
7246
+
7247
+ async def upgrade(self) -> None:
7248
+ await asyncio_subprocess_check_call('sudo', 'yum', 'update')
7249
+
7250
+ async def install(self, *packages: SystemPackageOrStr) -> None:
7251
+ pns = [p.name if isinstance(p, SystemPackage) else p for p in packages] # FIXME: versions
7252
+ await asyncio_subprocess_check_call('sudo', 'yum', 'install', *pns)
7253
+
7254
+ async def query(self, *packages: SystemPackageOrStr) -> ta.Mapping[str, SystemPackage]:
7255
+ pns = [p.name if isinstance(p, SystemPackage) else p for p in packages]
7256
+ d: ta.Dict[str, SystemPackage] = {}
7257
+ for pn in pns:
7258
+ out = await asyncio_subprocess_run(
7259
+ 'rpm', '-q', pn,
7260
+ capture_output=True,
7261
+ )
7262
+ if not out.proc.returncode:
7263
+ d[pn] = SystemPackage(
7264
+ pn,
7265
+ check.not_none(out.stdout).decode().strip(),
7266
+ )
7267
+ return d
7268
+
7269
+
6226
7270
  ########################################
6227
7271
  # ../../../omdev/interp/providers.py
6228
7272
  """
@@ -6285,139 +7329,342 @@ class RunningInterpProvider(InterpProvider):
6285
7329
 
6286
7330
 
6287
7331
  ########################################
6288
- # ../remote/connection.py
7332
+ # ../commands/inject.py
6289
7333
 
6290
7334
 
6291
7335
  ##
6292
7336
 
6293
7337
 
6294
- class RemoteExecutionConnector(abc.ABC):
6295
- @abc.abstractmethod
6296
- def connect(
6297
- self,
6298
- tgt: RemoteSpawning.Target,
6299
- bs: MainBootstrap,
6300
- ) -> ta.AsyncContextManager[RemoteCommandExecutor]:
6301
- raise NotImplementedError
7338
+ def bind_command(
7339
+ command_cls: ta.Type[Command],
7340
+ executor_cls: ta.Optional[ta.Type[CommandExecutor]],
7341
+ ) -> InjectorBindings:
7342
+ lst: ta.List[InjectorBindingOrBindings] = [
7343
+ inj.bind(CommandRegistration(command_cls), array=True),
7344
+ ]
7345
+
7346
+ if executor_cls is not None:
7347
+ lst.extend([
7348
+ inj.bind(executor_cls, singleton=True),
7349
+ inj.bind(CommandExecutorRegistration(command_cls, executor_cls), array=True),
7350
+ ])
7351
+
7352
+ return inj.as_bindings(*lst)
6302
7353
 
6303
7354
 
6304
7355
  ##
6305
7356
 
6306
7357
 
6307
- class PyremoteRemoteExecutionConnector(RemoteExecutionConnector):
6308
- def __init__(
6309
- self,
6310
- *,
6311
- spawning: RemoteSpawning,
6312
- msh: ObjMarshalerManager,
6313
- payload_file: ta.Optional[RemoteExecutionPayloadFile] = None,
6314
- ) -> None:
6315
- super().__init__()
7358
+ @dc.dataclass(frozen=True)
7359
+ class _FactoryCommandExecutor(CommandExecutor):
7360
+ factory: ta.Callable[[], CommandExecutor]
6316
7361
 
6317
- self._spawning = spawning
6318
- self._msh = msh
6319
- self._payload_file = payload_file
7362
+ def execute(self, i: Command) -> ta.Awaitable[Command.Output]:
7363
+ return self.factory().execute(i)
7364
+
7365
+
7366
+ ##
7367
+
7368
+
7369
+ def bind_commands(
7370
+ *,
7371
+ main_config: MainConfig,
7372
+ ) -> InjectorBindings:
7373
+ lst: ta.List[InjectorBindingOrBindings] = [
7374
+ inj.bind_array(CommandRegistration),
7375
+ inj.bind_array_type(CommandRegistration, CommandRegistrations),
7376
+
7377
+ inj.bind_array(CommandExecutorRegistration),
7378
+ inj.bind_array_type(CommandExecutorRegistration, CommandExecutorRegistrations),
7379
+
7380
+ inj.bind(build_command_name_map, singleton=True),
7381
+ ]
6320
7382
 
6321
7383
  #
6322
7384
 
6323
- @cached_nullary
6324
- def _payload_src(self) -> str:
6325
- return get_remote_payload_src(file=self._payload_file)
7385
+ def provide_obj_marshaler_installer(cmds: CommandNameMap) -> ObjMarshalerInstaller:
7386
+ return ObjMarshalerInstaller(functools.partial(install_command_marshaling, cmds))
6326
7387
 
6327
- @cached_nullary
6328
- def _remote_src(self) -> ta.Sequence[str]:
6329
- return [
6330
- self._payload_src(),
6331
- '_remote_execution_main()',
6332
- ]
7388
+ lst.append(inj.bind(provide_obj_marshaler_installer, array=True))
6333
7389
 
6334
- @cached_nullary
6335
- def _spawn_src(self) -> str:
6336
- return pyremote_build_bootstrap_cmd(__package__ or 'manage')
7390
+ #
7391
+
7392
+ def provide_command_executor_map(
7393
+ injector: Injector,
7394
+ crs: CommandExecutorRegistrations,
7395
+ ) -> CommandExecutorMap:
7396
+ dct: ta.Dict[ta.Type[Command], CommandExecutor] = {}
7397
+
7398
+ cr: CommandExecutorRegistration
7399
+ for cr in crs:
7400
+ if cr.command_cls in dct:
7401
+ raise KeyError(cr.command_cls)
7402
+
7403
+ factory = functools.partial(injector.provide, cr.executor_cls)
7404
+ if main_config.debug:
7405
+ ce = factory()
7406
+ else:
7407
+ ce = _FactoryCommandExecutor(factory)
7408
+
7409
+ dct[cr.command_cls] = ce
7410
+
7411
+ return CommandExecutorMap(dct)
7412
+
7413
+ lst.extend([
7414
+ inj.bind(provide_command_executor_map, singleton=True),
7415
+
7416
+ inj.bind(LocalCommandExecutor, singleton=True, eager=main_config.debug),
7417
+ ])
6337
7418
 
6338
7419
  #
6339
7420
 
6340
- @contextlib.asynccontextmanager
6341
- async def connect(
7421
+ lst.extend([
7422
+ bind_command(PingCommand, PingCommandExecutor),
7423
+ bind_command(SubprocessCommand, SubprocessCommandExecutor),
7424
+ ])
7425
+
7426
+ #
7427
+
7428
+ return inj.as_bindings(*lst)
7429
+
7430
+
7431
+ ########################################
7432
+ # ../deploy/apps.py
7433
+
7434
+
7435
+ def make_deploy_tag(
7436
+ rev: DeployRev,
7437
+ now: ta.Optional[datetime.datetime] = None,
7438
+ ) -> DeployTag:
7439
+ if now is None:
7440
+ now = datetime.datetime.utcnow() # noqa
7441
+ now_fmt = '%Y%m%dT%H%M%S'
7442
+ now_str = now.strftime(now_fmt)
7443
+ return DeployTag('-'.join([rev, now_str]))
7444
+
7445
+
7446
+ class DeployAppManager(DeployPathOwner):
7447
+ def __init__(
6342
7448
  self,
6343
- tgt: RemoteSpawning.Target,
6344
- bs: MainBootstrap,
6345
- ) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
6346
- spawn_src = self._spawn_src()
6347
- remote_src = self._remote_src()
7449
+ *,
7450
+ deploy_home: DeployHome,
7451
+ git: DeployGitManager,
7452
+ venvs: DeployVenvManager,
7453
+ ) -> None:
7454
+ super().__init__()
6348
7455
 
6349
- async with self._spawning.spawn(
6350
- tgt,
6351
- spawn_src,
6352
- debug=bs.main_config.debug,
6353
- ) as proc:
6354
- res = await PyremoteBootstrapDriver( # noqa
6355
- remote_src,
6356
- PyremoteBootstrapOptions(
6357
- debug=bs.main_config.debug,
6358
- ),
6359
- ).async_run(
6360
- proc.stdout,
6361
- proc.stdin,
6362
- )
7456
+ self._deploy_home = deploy_home
7457
+ self._git = git
7458
+ self._venvs = venvs
6363
7459
 
6364
- chan = RemoteChannelImpl(
6365
- proc.stdout,
6366
- proc.stdin,
6367
- msh=self._msh,
6368
- )
7460
+ self._dir = os.path.join(deploy_home, 'apps')
6369
7461
 
6370
- await chan.send_obj(bs)
7462
+ def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
7463
+ return {
7464
+ DeployPath.parse('apps/@app/@tag'),
7465
+ }
6371
7466
 
6372
- rce: RemoteCommandExecutor
6373
- async with contextlib.aclosing(RemoteCommandExecutor(chan)) as rce:
6374
- await rce.start()
7467
+ async def prepare_app(
7468
+ self,
7469
+ app: DeployApp,
7470
+ rev: DeployRev,
7471
+ repo: DeployGitRepo,
7472
+ ):
7473
+ app_tag = DeployAppTag(app, make_deploy_tag(rev))
7474
+ app_dir = os.path.join(self._dir, app, app_tag.tag)
7475
+
7476
+ #
7477
+
7478
+ await self._git.checkout(
7479
+ DeployGitSpec(
7480
+ repo=repo,
7481
+ rev=rev,
7482
+ ),
7483
+ app_dir,
7484
+ )
7485
+
7486
+ #
7487
+
7488
+ await self._venvs.setup_app_venv(app_tag)
6375
7489
 
6376
- yield rce
7490
+
7491
+ ########################################
7492
+ # ../remote/_main.py
7493
+
7494
+
7495
+ ##
7496
+
7497
+
7498
+ class _RemoteExecutionLogHandler(logging.Handler):
7499
+ def __init__(self, fn: ta.Callable[[str], None]) -> None:
7500
+ super().__init__()
7501
+ self._fn = fn
7502
+
7503
+ def emit(self, record):
7504
+ msg = self.format(record)
7505
+ self._fn(msg)
6377
7506
 
6378
7507
 
6379
7508
  ##
6380
7509
 
6381
7510
 
6382
- class InProcessRemoteExecutionConnector(RemoteExecutionConnector):
7511
+ class _RemoteExecutionMain:
6383
7512
  def __init__(
6384
7513
  self,
6385
- *,
6386
- msh: ObjMarshalerManager,
6387
- local_executor: LocalCommandExecutor,
7514
+ chan: RemoteChannel,
6388
7515
  ) -> None:
6389
7516
  super().__init__()
6390
7517
 
6391
- self._msh = msh
6392
- self._local_executor = local_executor
7518
+ self._chan = chan
6393
7519
 
6394
- @contextlib.asynccontextmanager
6395
- async def connect(
7520
+ self.__bootstrap: ta.Optional[MainBootstrap] = None
7521
+ self.__injector: ta.Optional[Injector] = None
7522
+
7523
+ @property
7524
+ def _bootstrap(self) -> MainBootstrap:
7525
+ return check.not_none(self.__bootstrap)
7526
+
7527
+ @property
7528
+ def _injector(self) -> Injector:
7529
+ return check.not_none(self.__injector)
7530
+
7531
+ #
7532
+
7533
+ def _timebomb_main(
6396
7534
  self,
6397
- tgt: RemoteSpawning.Target,
6398
- bs: MainBootstrap,
6399
- ) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
6400
- r0, w0 = asyncio_create_bytes_channel()
6401
- r1, w1 = asyncio_create_bytes_channel()
7535
+ delay_s: float,
7536
+ *,
7537
+ sig: int = signal.SIGINT,
7538
+ code: int = 1,
7539
+ ) -> None:
7540
+ time.sleep(delay_s)
6402
7541
 
6403
- remote_chan = RemoteChannelImpl(r0, w1, msh=self._msh)
6404
- local_chan = RemoteChannelImpl(r1, w0, msh=self._msh)
7542
+ if (pgid := os.getpgid(0)) == os.getpid():
7543
+ os.killpg(pgid, sig)
6405
7544
 
6406
- rch = _RemoteCommandHandler(
6407
- remote_chan,
6408
- self._local_executor,
7545
+ os._exit(code) # noqa
7546
+
7547
+ @cached_nullary
7548
+ def _timebomb_thread(self) -> ta.Optional[threading.Thread]:
7549
+ if (tbd := self._bootstrap.remote_config.timebomb_delay_s) is None:
7550
+ return None
7551
+
7552
+ thr = threading.Thread(
7553
+ target=functools.partial(self._timebomb_main, tbd),
7554
+ name=f'{self.__class__.__name__}.timebomb',
7555
+ daemon=True,
6409
7556
  )
6410
- rch_task = asyncio.create_task(rch.run()) # noqa
6411
- try:
6412
- rce: RemoteCommandExecutor
6413
- async with contextlib.aclosing(RemoteCommandExecutor(local_chan)) as rce:
6414
- await rce.start()
6415
7557
 
6416
- yield rce
7558
+ thr.start()
6417
7559
 
6418
- finally:
6419
- rch.stop()
6420
- await rch_task
7560
+ log.debug('Started timebomb thread: %r', thr)
7561
+
7562
+ return thr
7563
+
7564
+ #
7565
+
7566
+ @cached_nullary
7567
+ def _log_handler(self) -> _RemoteLogHandler:
7568
+ return _RemoteLogHandler(self._chan)
7569
+
7570
+ #
7571
+
7572
+ async def _setup(self) -> None:
7573
+ check.none(self.__bootstrap)
7574
+ check.none(self.__injector)
7575
+
7576
+ # Bootstrap
7577
+
7578
+ self.__bootstrap = check.not_none(await self._chan.recv_obj(MainBootstrap))
7579
+
7580
+ if (prd := self._bootstrap.remote_config.pycharm_remote_debug) is not None:
7581
+ pycharm_debug_connect(prd)
7582
+
7583
+ self.__injector = main_bootstrap(self._bootstrap)
7584
+
7585
+ self._chan.set_marshaler(self._injector[ObjMarshalerManager])
7586
+
7587
+ # Post-bootstrap
7588
+
7589
+ if self._bootstrap.remote_config.set_pgid:
7590
+ if os.getpgid(0) != os.getpid():
7591
+ log.debug('Setting pgid')
7592
+ os.setpgid(0, 0)
7593
+
7594
+ if (ds := self._bootstrap.remote_config.deathsig) is not None:
7595
+ log.debug('Setting deathsig: %s', ds)
7596
+ set_process_deathsig(int(signal.Signals[f'SIG{ds.upper()}']))
7597
+
7598
+ self._timebomb_thread()
7599
+
7600
+ if self._bootstrap.remote_config.forward_logging:
7601
+ log.debug('Installing log forwarder')
7602
+ logging.root.addHandler(self._log_handler())
7603
+
7604
+ #
7605
+
7606
+ async def run(self) -> None:
7607
+ await self._setup()
7608
+
7609
+ executor = self._injector[LocalCommandExecutor]
7610
+
7611
+ handler = _RemoteCommandHandler(self._chan, executor)
7612
+
7613
+ await handler.run()
7614
+
7615
+
7616
+ def _remote_execution_main() -> None:
7617
+ rt = pyremote_bootstrap_finalize() # noqa
7618
+
7619
+ async def inner() -> None:
7620
+ input = await asyncio_open_stream_reader(rt.input) # noqa
7621
+ output = await asyncio_open_stream_writer(rt.output)
7622
+
7623
+ chan = RemoteChannelImpl(
7624
+ input,
7625
+ output,
7626
+ )
7627
+
7628
+ await _RemoteExecutionMain(chan).run()
7629
+
7630
+ asyncio.run(inner())
7631
+
7632
+
7633
+ ########################################
7634
+ # ../system/commands.py
7635
+
7636
+
7637
+ ##
7638
+
7639
+
7640
+ @dc.dataclass(frozen=True)
7641
+ class CheckSystemPackageCommand(Command['CheckSystemPackageCommand.Output']):
7642
+ pkgs: ta.Sequence[str] = ()
7643
+
7644
+ def __post_init__(self) -> None:
7645
+ check.not_isinstance(self.pkgs, str)
7646
+
7647
+ @dc.dataclass(frozen=True)
7648
+ class Output(Command.Output):
7649
+ pkgs: ta.Sequence[SystemPackage]
7650
+
7651
+
7652
+ class CheckSystemPackageCommandExecutor(CommandExecutor[CheckSystemPackageCommand, CheckSystemPackageCommand.Output]):
7653
+ def __init__(
7654
+ self,
7655
+ *,
7656
+ mgr: SystemPackageManager,
7657
+ ) -> None:
7658
+ super().__init__()
7659
+
7660
+ self._mgr = mgr
7661
+
7662
+ async def execute(self, cmd: CheckSystemPackageCommand) -> CheckSystemPackageCommand.Output:
7663
+ log.info('Checking system package!')
7664
+
7665
+ ret = await self._mgr.query(*cmd.pkgs)
7666
+
7667
+ return CheckSystemPackageCommand.Output(list(ret.values()))
6421
7668
 
6422
7669
 
6423
7670
  ########################################
@@ -6961,54 +8208,183 @@ class SystemInterpProvider(InterpProvider):
6961
8208
  lst.append((e, ev))
6962
8209
  return lst
6963
8210
 
6964
- #
8211
+ #
8212
+
8213
+ async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
8214
+ return [ev for e, ev in await self.exe_versions()]
8215
+
8216
+ async def get_installed_version(self, version: InterpVersion) -> Interp:
8217
+ for e, ev in await self.exe_versions():
8218
+ if ev != version:
8219
+ continue
8220
+ return Interp(
8221
+ exe=e,
8222
+ version=ev,
8223
+ )
8224
+ raise KeyError(version)
8225
+
8226
+
8227
+ ########################################
8228
+ # ../remote/connection.py
8229
+
8230
+
8231
+ ##
8232
+
8233
+
8234
+ class PyremoteRemoteExecutionConnector:
8235
+ def __init__(
8236
+ self,
8237
+ *,
8238
+ spawning: RemoteSpawning,
8239
+ msh: ObjMarshalerManager,
8240
+ payload_file: ta.Optional[RemoteExecutionPayloadFile] = None,
8241
+ ) -> None:
8242
+ super().__init__()
8243
+
8244
+ self._spawning = spawning
8245
+ self._msh = msh
8246
+ self._payload_file = payload_file
8247
+
8248
+ #
8249
+
8250
+ @cached_nullary
8251
+ def _payload_src(self) -> str:
8252
+ return get_remote_payload_src(file=self._payload_file)
8253
+
8254
+ @cached_nullary
8255
+ def _remote_src(self) -> ta.Sequence[str]:
8256
+ return [
8257
+ self._payload_src(),
8258
+ '_remote_execution_main()',
8259
+ ]
8260
+
8261
+ @cached_nullary
8262
+ def _spawn_src(self) -> str:
8263
+ return pyremote_build_bootstrap_cmd(__package__ or 'manage')
8264
+
8265
+ #
8266
+
8267
+ @contextlib.asynccontextmanager
8268
+ async def connect(
8269
+ self,
8270
+ tgt: RemoteSpawning.Target,
8271
+ bs: MainBootstrap,
8272
+ ) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
8273
+ spawn_src = self._spawn_src()
8274
+ remote_src = self._remote_src()
8275
+
8276
+ async with self._spawning.spawn(
8277
+ tgt,
8278
+ spawn_src,
8279
+ debug=bs.main_config.debug,
8280
+ ) as proc:
8281
+ res = await PyremoteBootstrapDriver( # noqa
8282
+ remote_src,
8283
+ PyremoteBootstrapOptions(
8284
+ debug=bs.main_config.debug,
8285
+ ),
8286
+ ).async_run(
8287
+ proc.stdout,
8288
+ proc.stdin,
8289
+ )
8290
+
8291
+ chan = RemoteChannelImpl(
8292
+ proc.stdout,
8293
+ proc.stdin,
8294
+ msh=self._msh,
8295
+ )
8296
+
8297
+ await chan.send_obj(bs)
8298
+
8299
+ rce: RemoteCommandExecutor
8300
+ async with aclosing(RemoteCommandExecutor(chan)) as rce:
8301
+ await rce.start()
8302
+
8303
+ yield rce
8304
+
8305
+
8306
+ ##
8307
+
8308
+
8309
+ class InProcessRemoteExecutionConnector:
8310
+ def __init__(
8311
+ self,
8312
+ *,
8313
+ msh: ObjMarshalerManager,
8314
+ local_executor: LocalCommandExecutor,
8315
+ ) -> None:
8316
+ super().__init__()
8317
+
8318
+ self._msh = msh
8319
+ self._local_executor = local_executor
8320
+
8321
+ @contextlib.asynccontextmanager
8322
+ async def connect(self) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
8323
+ r0, w0 = asyncio_create_bytes_channel()
8324
+ r1, w1 = asyncio_create_bytes_channel()
8325
+
8326
+ remote_chan = RemoteChannelImpl(r0, w1, msh=self._msh)
8327
+ local_chan = RemoteChannelImpl(r1, w0, msh=self._msh)
6965
8328
 
6966
- async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
6967
- return [ev for e, ev in await self.exe_versions()]
8329
+ rch = _RemoteCommandHandler(
8330
+ remote_chan,
8331
+ self._local_executor,
8332
+ )
8333
+ rch_task = asyncio.create_task(rch.run()) # noqa
8334
+ try:
8335
+ rce: RemoteCommandExecutor
8336
+ async with aclosing(RemoteCommandExecutor(local_chan)) as rce:
8337
+ await rce.start()
6968
8338
 
6969
- async def get_installed_version(self, version: InterpVersion) -> Interp:
6970
- for e, ev in await self.exe_versions():
6971
- if ev != version:
6972
- continue
6973
- return Interp(
6974
- exe=e,
6975
- version=ev,
6976
- )
6977
- raise KeyError(version)
8339
+ yield rce
8340
+
8341
+ finally:
8342
+ rch.stop()
8343
+ await rch_task
6978
8344
 
6979
8345
 
6980
8346
  ########################################
6981
- # ../remote/inject.py
8347
+ # ../system/inject.py
6982
8348
 
6983
8349
 
6984
- def bind_remote(
8350
+ def bind_system(
6985
8351
  *,
6986
- remote_config: RemoteConfig,
8352
+ system_config: SystemConfig,
6987
8353
  ) -> InjectorBindings:
6988
8354
  lst: ta.List[InjectorBindingOrBindings] = [
6989
- inj.bind(remote_config),
6990
-
6991
- inj.bind(SubprocessRemoteSpawning, singleton=True),
6992
- inj.bind(RemoteSpawning, to_key=SubprocessRemoteSpawning),
8355
+ inj.bind(system_config),
6993
8356
  ]
6994
8357
 
6995
8358
  #
6996
8359
 
6997
- if remote_config.use_in_process_remote_executor:
8360
+ platform = system_config.platform or detect_system_platform()
8361
+ lst.append(inj.bind(platform, key=Platform))
8362
+
8363
+ #
8364
+
8365
+ if isinstance(platform, AmazonLinuxPlatform):
6998
8366
  lst.extend([
6999
- inj.bind(InProcessRemoteExecutionConnector, singleton=True),
7000
- inj.bind(RemoteExecutionConnector, to_key=InProcessRemoteExecutionConnector),
8367
+ inj.bind(YumSystemPackageManager, singleton=True),
8368
+ inj.bind(SystemPackageManager, to_key=YumSystemPackageManager),
7001
8369
  ])
7002
- else:
8370
+
8371
+ elif isinstance(platform, LinuxPlatform):
8372
+ lst.extend([
8373
+ inj.bind(AptSystemPackageManager, singleton=True),
8374
+ inj.bind(SystemPackageManager, to_key=AptSystemPackageManager),
8375
+ ])
8376
+
8377
+ elif isinstance(platform, DarwinPlatform):
7003
8378
  lst.extend([
7004
- inj.bind(PyremoteRemoteExecutionConnector, singleton=True),
7005
- inj.bind(RemoteExecutionConnector, to_key=PyremoteRemoteExecutionConnector),
8379
+ inj.bind(BrewSystemPackageManager, singleton=True),
8380
+ inj.bind(SystemPackageManager, to_key=BrewSystemPackageManager),
7006
8381
  ])
7007
8382
 
7008
8383
  #
7009
8384
 
7010
- if (pf := remote_config.payload_file) is not None:
7011
- lst.append(inj.bind(pf, key=RemoteExecutionPayloadFile))
8385
+ lst.extend([
8386
+ bind_command(CheckSystemPackageCommand, CheckSystemPackageCommandExecutor),
8387
+ ])
7012
8388
 
7013
8389
  #
7014
8390
 
@@ -7114,189 +8490,248 @@ DEFAULT_INTERP_RESOLVER = InterpResolver([(p.name, p) for p in [
7114
8490
 
7115
8491
 
7116
8492
  ########################################
7117
- # ../commands/interp.py
8493
+ # ../remote/inject.py
7118
8494
 
7119
8495
 
7120
- ##
8496
+ def bind_remote(
8497
+ *,
8498
+ remote_config: RemoteConfig,
8499
+ ) -> InjectorBindings:
8500
+ lst: ta.List[InjectorBindingOrBindings] = [
8501
+ inj.bind(remote_config),
7121
8502
 
8503
+ inj.bind(SubprocessRemoteSpawning, singleton=True),
8504
+ inj.bind(RemoteSpawning, to_key=SubprocessRemoteSpawning),
7122
8505
 
7123
- @dc.dataclass(frozen=True)
7124
- class InterpCommand(Command['InterpCommand.Output']):
7125
- spec: str
7126
- install: bool = False
8506
+ inj.bind(PyremoteRemoteExecutionConnector, singleton=True),
8507
+ inj.bind(InProcessRemoteExecutionConnector, singleton=True),
8508
+ ]
7127
8509
 
7128
- @dc.dataclass(frozen=True)
7129
- class Output(Command.Output):
7130
- exe: str
7131
- version: str
7132
- opts: InterpOpts
8510
+ #
8511
+
8512
+ if (pf := remote_config.payload_file) is not None:
8513
+ lst.append(inj.bind(pf, key=RemoteExecutionPayloadFile))
7133
8514
 
8515
+ #
7134
8516
 
7135
- class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
7136
- async def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
7137
- i = InterpSpecifier.parse(check.not_none(cmd.spec))
7138
- o = check.not_none(await DEFAULT_INTERP_RESOLVER.resolve(i, install=cmd.install))
7139
- return InterpCommand.Output(
7140
- exe=o.exe,
7141
- version=str(o.version.version),
7142
- opts=o.version.opts,
7143
- )
8517
+ return inj.as_bindings(*lst)
7144
8518
 
7145
8519
 
7146
8520
  ########################################
7147
- # ../commands/inject.py
8521
+ # ../targets/connection.py
7148
8522
 
7149
8523
 
7150
8524
  ##
7151
8525
 
7152
8526
 
7153
- def bind_command(
7154
- command_cls: ta.Type[Command],
7155
- executor_cls: ta.Optional[ta.Type[CommandExecutor]],
7156
- ) -> InjectorBindings:
7157
- lst: ta.List[InjectorBindingOrBindings] = [
7158
- inj.bind(CommandRegistration(command_cls), array=True),
7159
- ]
8527
+ class ManageTargetConnector(abc.ABC):
8528
+ @abc.abstractmethod
8529
+ def connect(self, tgt: ManageTarget) -> ta.AsyncContextManager[CommandExecutor]:
8530
+ raise NotImplementedError
7160
8531
 
7161
- if executor_cls is not None:
7162
- lst.extend([
7163
- inj.bind(executor_cls, singleton=True),
7164
- inj.bind(CommandExecutorRegistration(command_cls, executor_cls), array=True),
7165
- ])
7166
8532
 
7167
- return inj.as_bindings(*lst)
8533
+ ##
7168
8534
 
7169
8535
 
7170
- ##
8536
+ ManageTargetConnectorMap = ta.NewType('ManageTargetConnectorMap', ta.Mapping[ta.Type[ManageTarget], ManageTargetConnector]) # noqa
7171
8537
 
7172
8538
 
7173
8539
  @dc.dataclass(frozen=True)
7174
- class _FactoryCommandExecutor(CommandExecutor):
7175
- factory: ta.Callable[[], CommandExecutor]
8540
+ class TypeSwitchedManageTargetConnector(ManageTargetConnector):
8541
+ connectors: ManageTargetConnectorMap
7176
8542
 
7177
- def execute(self, i: Command) -> ta.Awaitable[Command.Output]:
7178
- return self.factory().execute(i)
8543
+ def get_connector(self, ty: ta.Type[ManageTarget]) -> ManageTargetConnector:
8544
+ for k, v in self.connectors.items():
8545
+ if issubclass(ty, k):
8546
+ return v
8547
+ raise KeyError(ty)
8548
+
8549
+ def connect(self, tgt: ManageTarget) -> ta.AsyncContextManager[CommandExecutor]:
8550
+ return self.get_connector(type(tgt)).connect(tgt)
7179
8551
 
7180
8552
 
7181
8553
  ##
7182
8554
 
7183
8555
 
7184
- def bind_commands(
7185
- *,
7186
- main_config: MainConfig,
7187
- ) -> InjectorBindings:
7188
- lst: ta.List[InjectorBindingOrBindings] = [
7189
- inj.bind_array(CommandRegistration),
7190
- inj.bind_array_type(CommandRegistration, CommandRegistrations),
8556
+ @dc.dataclass(frozen=True)
8557
+ class LocalManageTargetConnector(ManageTargetConnector):
8558
+ _local_executor: LocalCommandExecutor
8559
+ _in_process_connector: InProcessRemoteExecutionConnector
8560
+ _pyremote_connector: PyremoteRemoteExecutionConnector
8561
+ _bootstrap: MainBootstrap
7191
8562
 
7192
- inj.bind_array(CommandExecutorRegistration),
7193
- inj.bind_array_type(CommandExecutorRegistration, CommandExecutorRegistrations),
8563
+ @contextlib.asynccontextmanager
8564
+ async def connect(self, tgt: ManageTarget) -> ta.AsyncGenerator[CommandExecutor, None]:
8565
+ lmt = check.isinstance(tgt, LocalManageTarget)
7194
8566
 
7195
- inj.bind(build_command_name_map, singleton=True),
7196
- ]
8567
+ if isinstance(lmt, InProcessManageTarget):
8568
+ imt = check.isinstance(lmt, InProcessManageTarget)
7197
8569
 
7198
- #
8570
+ if imt.mode == InProcessManageTarget.Mode.DIRECT:
8571
+ yield self._local_executor
7199
8572
 
7200
- def provide_obj_marshaler_installer(cmds: CommandNameMap) -> ObjMarshalerInstaller:
7201
- return ObjMarshalerInstaller(functools.partial(install_command_marshaling, cmds))
8573
+ elif imt.mode == InProcessManageTarget.Mode.FAKE_REMOTE:
8574
+ async with self._in_process_connector.connect() as rce:
8575
+ yield rce
7202
8576
 
7203
- lst.append(inj.bind(provide_obj_marshaler_installer, array=True))
8577
+ else:
8578
+ raise TypeError(imt.mode)
8579
+
8580
+ elif isinstance(lmt, SubprocessManageTarget):
8581
+ async with self._pyremote_connector.connect(
8582
+ RemoteSpawning.Target(
8583
+ python=lmt.python,
8584
+ ),
8585
+ self._bootstrap,
8586
+ ) as rce:
8587
+ yield rce
7204
8588
 
7205
- #
8589
+ else:
8590
+ raise TypeError(lmt)
7206
8591
 
7207
- def provide_command_executor_map(
7208
- injector: Injector,
7209
- crs: CommandExecutorRegistrations,
7210
- ) -> CommandExecutorMap:
7211
- dct: ta.Dict[ta.Type[Command], CommandExecutor] = {}
7212
8592
 
7213
- cr: CommandExecutorRegistration
7214
- for cr in crs:
7215
- if cr.command_cls in dct:
7216
- raise KeyError(cr.command_cls)
8593
+ ##
7217
8594
 
7218
- factory = functools.partial(injector.provide, cr.executor_cls)
7219
- if main_config.debug:
7220
- ce = factory()
7221
- else:
7222
- ce = _FactoryCommandExecutor(factory)
7223
8595
 
7224
- dct[cr.command_cls] = ce
8596
+ @dc.dataclass(frozen=True)
8597
+ class DockerManageTargetConnector(ManageTargetConnector):
8598
+ _pyremote_connector: PyremoteRemoteExecutionConnector
8599
+ _bootstrap: MainBootstrap
7225
8600
 
7226
- return CommandExecutorMap(dct)
8601
+ @contextlib.asynccontextmanager
8602
+ async def connect(self, tgt: ManageTarget) -> ta.AsyncGenerator[CommandExecutor, None]:
8603
+ dmt = check.isinstance(tgt, DockerManageTarget)
8604
+
8605
+ sh_parts: ta.List[str] = ['docker']
8606
+ if dmt.image is not None:
8607
+ sh_parts.extend(['run', '-i', dmt.image])
8608
+ elif dmt.container_id is not None:
8609
+ sh_parts.extend(['exec', dmt.container_id])
8610
+ else:
8611
+ raise ValueError(dmt)
7227
8612
 
7228
- lst.extend([
7229
- inj.bind(provide_command_executor_map, singleton=True),
8613
+ async with self._pyremote_connector.connect(
8614
+ RemoteSpawning.Target(
8615
+ shell=' '.join(sh_parts),
8616
+ python=dmt.python,
8617
+ ),
8618
+ self._bootstrap,
8619
+ ) as rce:
8620
+ yield rce
7230
8621
 
7231
- inj.bind(LocalCommandExecutor, singleton=True, eager=main_config.debug),
7232
- ])
7233
8622
 
7234
- #
8623
+ ##
7235
8624
 
7236
- command_cls: ta.Any
7237
- executor_cls: ta.Any
7238
- for command_cls, executor_cls in [
7239
- (SubprocessCommand, SubprocessCommandExecutor),
7240
- (InterpCommand, InterpCommandExecutor),
7241
- ]:
7242
- lst.append(bind_command(command_cls, executor_cls))
7243
8625
 
7244
- #
8626
+ @dc.dataclass(frozen=True)
8627
+ class SshManageTargetConnector(ManageTargetConnector):
8628
+ _pyremote_connector: PyremoteRemoteExecutionConnector
8629
+ _bootstrap: MainBootstrap
7245
8630
 
7246
- return inj.as_bindings(*lst)
8631
+ @contextlib.asynccontextmanager
8632
+ async def connect(self, tgt: ManageTarget) -> ta.AsyncGenerator[CommandExecutor, None]:
8633
+ smt = check.isinstance(tgt, SshManageTarget)
8634
+
8635
+ sh_parts: ta.List[str] = ['ssh']
8636
+ if smt.key_file is not None:
8637
+ sh_parts.extend(['-i', smt.key_file])
8638
+ addr = check.not_none(smt.host)
8639
+ if smt.username is not None:
8640
+ addr = f'{smt.username}@{addr}'
8641
+ sh_parts.append(addr)
8642
+
8643
+ async with self._pyremote_connector.connect(
8644
+ RemoteSpawning.Target(
8645
+ shell=' '.join(sh_parts),
8646
+ shell_quote=True,
8647
+ python=smt.python,
8648
+ ),
8649
+ self._bootstrap,
8650
+ ) as rce:
8651
+ yield rce
7247
8652
 
7248
8653
 
7249
8654
  ########################################
7250
- # ../deploy/inject.py
8655
+ # ../deploy/interp.py
7251
8656
 
7252
8657
 
7253
- def bind_deploy(
7254
- ) -> InjectorBindings:
7255
- lst: ta.List[InjectorBindingOrBindings] = [
7256
- bind_command(DeployCommand, DeployCommandExecutor),
7257
- ]
8658
+ ##
7258
8659
 
7259
- return inj.as_bindings(*lst)
8660
+
8661
+ @dc.dataclass(frozen=True)
8662
+ class InterpCommand(Command['InterpCommand.Output']):
8663
+ spec: str
8664
+ install: bool = False
8665
+
8666
+ @dc.dataclass(frozen=True)
8667
+ class Output(Command.Output):
8668
+ exe: str
8669
+ version: str
8670
+ opts: InterpOpts
8671
+
8672
+
8673
+ class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
8674
+ async def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
8675
+ i = InterpSpecifier.parse(check.not_none(cmd.spec))
8676
+ o = check.not_none(await DEFAULT_INTERP_RESOLVER.resolve(i, install=cmd.install))
8677
+ return InterpCommand.Output(
8678
+ exe=o.exe,
8679
+ version=str(o.version.version),
8680
+ opts=o.version.opts,
8681
+ )
7260
8682
 
7261
8683
 
7262
8684
  ########################################
7263
- # ../system/inject.py
8685
+ # ../targets/inject.py
7264
8686
 
7265
8687
 
7266
- def bind_system(
7267
- *,
7268
- system_config: SystemConfig,
7269
- ) -> InjectorBindings:
8688
+ def bind_targets() -> InjectorBindings:
7270
8689
  lst: ta.List[InjectorBindingOrBindings] = [
7271
- inj.bind(system_config),
8690
+ inj.bind(LocalManageTargetConnector, singleton=True),
8691
+ inj.bind(DockerManageTargetConnector, singleton=True),
8692
+ inj.bind(SshManageTargetConnector, singleton=True),
8693
+
8694
+ inj.bind(TypeSwitchedManageTargetConnector, singleton=True),
8695
+ inj.bind(ManageTargetConnector, to_key=TypeSwitchedManageTargetConnector),
7272
8696
  ]
7273
8697
 
7274
8698
  #
7275
8699
 
7276
- platform = system_config.platform or sys.platform
7277
- lst.append(inj.bind(platform, key=SystemPlatform))
8700
+ def provide_manage_target_connector_map(injector: Injector) -> ManageTargetConnectorMap:
8701
+ return ManageTargetConnectorMap({
8702
+ LocalManageTarget: injector[LocalManageTargetConnector],
8703
+ DockerManageTarget: injector[DockerManageTargetConnector],
8704
+ SshManageTarget: injector[SshManageTargetConnector],
8705
+ })
8706
+ lst.append(inj.bind(provide_manage_target_connector_map, singleton=True))
7278
8707
 
7279
8708
  #
7280
8709
 
7281
- if platform == 'linux':
7282
- lst.extend([
7283
- inj.bind(AptSystemPackageManager, singleton=True),
7284
- inj.bind(SystemPackageManager, to_key=AptSystemPackageManager),
7285
- ])
8710
+ return inj.as_bindings(*lst)
7286
8711
 
7287
- elif platform == 'darwin':
7288
- lst.extend([
7289
- inj.bind(BrewSystemPackageManager, singleton=True),
7290
- inj.bind(SystemPackageManager, to_key=BrewSystemPackageManager),
7291
- ])
7292
8712
 
7293
- #
8713
+ ########################################
8714
+ # ../deploy/inject.py
7294
8715
 
7295
- lst.extend([
7296
- bind_command(CheckSystemPackageCommand, CheckSystemPackageCommandExecutor),
7297
- ])
7298
8716
 
7299
- #
8717
+ def bind_deploy(
8718
+ *,
8719
+ deploy_config: DeployConfig,
8720
+ ) -> InjectorBindings:
8721
+ lst: ta.List[InjectorBindingOrBindings] = [
8722
+ inj.bind(deploy_config),
8723
+
8724
+ inj.bind(DeployAppManager, singleton=True),
8725
+ inj.bind(DeployGitManager, singleton=True),
8726
+ inj.bind(DeployVenvManager, singleton=True),
8727
+
8728
+ bind_command(DeployCommand, DeployCommandExecutor),
8729
+ bind_command(InterpCommand, InterpCommandExecutor),
8730
+ ]
8731
+
8732
+ if (dh := deploy_config.deploy_home) is not None:
8733
+ dh = os.path.abspath(os.path.expanduser(dh))
8734
+ lst.append(inj.bind(dh, key=DeployHome))
7300
8735
 
7301
8736
  return inj.as_bindings(*lst)
7302
8737
 
@@ -7310,9 +8745,13 @@ def bind_system(
7310
8745
 
7311
8746
  def bind_main(
7312
8747
  *,
7313
- main_config: MainConfig,
7314
- remote_config: RemoteConfig,
7315
- system_config: SystemConfig,
8748
+ main_config: MainConfig = MainConfig(),
8749
+
8750
+ deploy_config: DeployConfig = DeployConfig(),
8751
+ remote_config: RemoteConfig = RemoteConfig(),
8752
+ system_config: SystemConfig = SystemConfig(),
8753
+
8754
+ main_bootstrap: ta.Optional[MainBootstrap] = None,
7316
8755
  ) -> InjectorBindings:
7317
8756
  lst: ta.List[InjectorBindingOrBindings] = [
7318
8757
  inj.bind(main_config),
@@ -7321,7 +8760,9 @@ def bind_main(
7321
8760
  main_config=main_config,
7322
8761
  ),
7323
8762
 
7324
- bind_deploy(),
8763
+ bind_deploy(
8764
+ deploy_config=deploy_config,
8765
+ ),
7325
8766
 
7326
8767
  bind_remote(
7327
8768
  remote_config=remote_config,
@@ -7330,10 +8771,17 @@ def bind_main(
7330
8771
  bind_system(
7331
8772
  system_config=system_config,
7332
8773
  ),
8774
+
8775
+ bind_targets(),
7333
8776
  ]
7334
8777
 
7335
8778
  #
7336
8779
 
8780
+ if main_bootstrap is not None:
8781
+ lst.append(inj.bind(main_bootstrap))
8782
+
8783
+ #
8784
+
7337
8785
  def build_obj_marshaler_manager(insts: ObjMarshalerInstallers) -> ObjMarshalerManager:
7338
8786
  msh = ObjMarshalerManager()
7339
8787
  inst: ObjMarshalerInstaller
@@ -7363,8 +8811,12 @@ def main_bootstrap(bs: MainBootstrap) -> Injector:
7363
8811
 
7364
8812
  injector = inj.create_injector(bind_main( # noqa
7365
8813
  main_config=bs.main_config,
8814
+
8815
+ deploy_config=bs.deploy_config,
7366
8816
  remote_config=bs.remote_config,
7367
8817
  system_config=bs.system_config,
8818
+
8819
+ main_bootstrap=bs,
7368
8820
  ))
7369
8821
 
7370
8822
  return injector
@@ -7378,10 +8830,6 @@ class MainCli(ArgparseCli):
7378
8830
  @argparse_command(
7379
8831
  argparse_arg('--_payload-file'),
7380
8832
 
7381
- argparse_arg('-s', '--shell'),
7382
- argparse_arg('-q', '--shell-quote', action='store_true'),
7383
- argparse_arg('--python', default='python3'),
7384
-
7385
8833
  argparse_arg('--pycharm-debug-port', type=int),
7386
8834
  argparse_arg('--pycharm-debug-host'),
7387
8835
  argparse_arg('--pycharm-debug-version'),
@@ -7390,8 +8838,9 @@ class MainCli(ArgparseCli):
7390
8838
 
7391
8839
  argparse_arg('--debug', action='store_true'),
7392
8840
 
7393
- argparse_arg('--local', action='store_true'),
8841
+ argparse_arg('--deploy-home'),
7394
8842
 
8843
+ argparse_arg('target'),
7395
8844
  argparse_arg('command', nargs='+'),
7396
8845
  )
7397
8846
  async def run(self) -> None:
@@ -7402,6 +8851,10 @@ class MainCli(ArgparseCli):
7402
8851
  debug=bool(self.args.debug),
7403
8852
  ),
7404
8853
 
8854
+ deploy_config=DeployConfig(
8855
+ deploy_home=self.args.deploy_home,
8856
+ ),
8857
+
7405
8858
  remote_config=RemoteConfig(
7406
8859
  payload_file=self.args._payload_file, # noqa
7407
8860
 
@@ -7412,8 +8865,6 @@ class MainCli(ArgparseCli):
7412
8865
  ) if self.args.pycharm_debug_port is not None else None,
7413
8866
 
7414
8867
  timebomb_delay_s=self.args.remote_timebomb_delay_s,
7415
-
7416
- use_in_process_remote_executor=True,
7417
8868
  ),
7418
8869
  )
7419
8870
 
@@ -7427,6 +8878,11 @@ class MainCli(ArgparseCli):
7427
8878
 
7428
8879
  msh = injector[ObjMarshalerManager]
7429
8880
 
8881
+ ts = self.args.target
8882
+ if not ts.startswith('{'):
8883
+ ts = json.dumps({ts: {}})
8884
+ tgt: ManageTarget = msh.unmarshal_obj(json.loads(ts), ManageTarget)
8885
+
7430
8886
  cmds: ta.List[Command] = []
7431
8887
  cmd: Command
7432
8888
  for c in self.args.command:
@@ -7437,21 +8893,7 @@ class MainCli(ArgparseCli):
7437
8893
 
7438
8894
  #
7439
8895
 
7440
- async with contextlib.AsyncExitStack() as es:
7441
- ce: CommandExecutor
7442
-
7443
- if self.args.local:
7444
- ce = injector[LocalCommandExecutor]
7445
-
7446
- else:
7447
- tgt = RemoteSpawning.Target(
7448
- shell=self.args.shell,
7449
- shell_quote=self.args.shell_quote,
7450
- python=self.args.python,
7451
- )
7452
-
7453
- ce = await es.enter_async_context(injector[RemoteExecutionConnector].connect(tgt, bs)) # noqa
7454
-
8896
+ async with injector[ManageTargetConnector].connect(tgt) as ce:
7455
8897
  async def run_command(cmd: Command) -> None:
7456
8898
  res = await ce.try_execute(
7457
8899
  cmd,